Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 2.11.0-privacyPolicy
  • APIv2
  • Docs/Setup
  • Experiment/fix-debugging
  • Experimental/Heinrichs-cypress
  • Feature/xxxx-turnOffDataPub
  • Fix/xxxx-ToS400Error
  • Fix/xxxx-migrateLogin
  • Fix/xxxx-tokenUploadButton
  • Hotfix/0038-correctDownload
  • Hotfix/1917-PublicFilesVisibility
  • Hotfix/1963-fixOrganizationField
  • Hotfix/2015-PublicFilesVisibility
  • Hotfix/2130-uiv2ContactChange
  • Hotfix/2144-invitationCall
  • Hotfix/2150-fixUpload
  • Hotfix/2160-userOrgsInst
  • Hotfix/2190-requiredFieldsForUserProfile
  • Hotfix/2196-RCVTableTranslation
  • Hotfix/2212-fixFiles
  • Hotfix/2226-userProfileSaveButton
  • Hotfix/2232-dependencyFix
  • Hotfix/2233-fixMe
  • Hotfix/2258-saveButtonWorksAsExpected
  • Hotfix/2296-selectedValuesNotReturned
  • Hotfix/2308-defaultLicense
  • Hotfix/2335-fixingSearchRCV
  • Hotfix/2353-dropShape
  • Hotfix/2370-fixDeleteButton
  • Hotfix/2378-linkedFix
  • Hotfix/2379-filesDragAndDrop
  • Hotfix/2382-guestStillBuggy
  • Hotfix/2384-guestsAndLinked
  • Hotfix/2427-adminTrouble
  • Hotfix/2459-EncodingPath
  • Hotfix/2465-orcidLink
  • Hotfix/2465-orcidLink-v1.25.1
  • Hotfix/2504-formGen
  • Hotfix/2541-resCreate
  • Hotfix/2601-correctMetadataIdentity
  • Hotfix/2611-feedback
  • Hotfix/2618-turtle
  • Hotfix/2681-validationErrors
  • Hotfix/2684-correctEncoding
  • Hotfix/2684-fixSubMetadata
  • Hotfix/2713-validateEntryName
  • Hotfix/2734-allowEmptyLicense
  • Hotfix/2765-encodingAgain
  • Hotfix/2852-adaptTextForToSUi
  • Hotfix/2853-optimizationV4
  • Hotfix/2943-reloadingResources
  • Hotfix/2943-searchHighlighting
  • Hotfix/2957-styleAndUpgrade
  • Hotfix/2971-fixTextInDataPub
  • Hotfix/2989-cookieLength
  • Hotfix/662-keepSidebarExpanded
  • Hotfix/xxxx-correctLinking
  • Hotfix/xxxx-folderRecursive
  • Hotfix/xxxx-fullscreenCss
  • Hotfix/xxxx-homepageDisplay
  • Hotfix/xxxx-liveReleaseFixes
  • Hotfix/xxxx-partnerProjects
  • Hotfix/xxxx-workingFileIndex
  • Issue/1782-structualDataIntegration
  • Issue/1792-newMetadataStructure
  • Issue/1822-coscineUIv2App
  • Issue/1824-componentsUIv2
  • Issue/1824-routerAdditions
  • Issue/1825-codeQualityPipelines
  • Issue/1833-newLogin
  • Issue/1843-multipleFilesValidation
  • Issue/1860-searchScoping
  • Issue/1861-searchMetadata
  • Issue/1862-searchFacets
  • Issue/1863-paginationForSearch
  • Issue/1926-userProfile
  • Issue/1927-projectAppMigration
  • Issue/1928-sidebarmenuAddition
  • Issue/1929-vuexToPinia
  • Issue/1938-internalHandling
  • Issue/1951-quotaImplementation
  • Issue/1953-owlImports
  • Issue/1957-resourceAppMigration
  • Issue/1957-resourceAppMigrationNew
  • Issue/1962-SearchAppUI2
  • Issue/1964-tokenExpiryUIv2
  • Issue/1965-userListMigration
  • Issue/1970-breadcrumbs
  • Issue/1971-projectEditCreateMigration
  • Issue/1972-homeDepot
  • Issue/1974-shibbolethLogout
  • Issue/1976-resouceCreationVaildEmail
  • Issue/1979-supportAdminUIv2Migration
  • Issue/1980-userManagement
  • Issue/1985-adaptSidebar
  • Issue/2002-migrateResourceCreate
  • Issue/2003-resourceSettings
  • Issue/2008-quotaManagement
  • Issue/2011-pathConfig
  • Issue/2016-BannerMigration
  • 1.28.0-pilot
  • v1.0.0
  • v1.1.0
  • v1.10.0
  • v1.10.1
  • v1.10.2
  • v1.10.3
  • v1.11.0
  • v1.11.1
  • v1.11.2
  • v1.11.3
  • v1.11.4
  • v1.11.5
  • v1.11.6
  • v1.11.7
  • v1.12.0
  • v1.13.0
  • v1.14.0
  • v1.14.1
  • v1.14.2
  • v1.14.3
  • v1.15.0
  • v1.15.1
  • v1.16.0
  • v1.16.1
  • v1.16.2
  • v1.16.3
  • v1.17.0
  • v1.17.1
  • v1.17.2
  • v1.18.0
  • v1.18.1
  • v1.19.0
  • v1.2.0
  • v1.20.0
  • v1.20.1
  • v1.20.2
  • v1.20.3
  • v1.20.4
  • v1.20.5
  • v1.21.0
  • v1.22.0
  • v1.22.1
  • v1.22.2
  • v1.23.0
  • v1.23.1
  • v1.23.2
  • v1.23.3
  • v1.23.4
  • v1.23.5
  • v1.23.6
  • v1.23.6-patch-2417-2427
  • v1.24.0
  • v1.24.1
  • v1.25.0
  • v1.25.1
  • v1.26.0
  • v1.26.1
  • v1.27.0
  • v1.27.1
  • v1.27.1-pilot
  • v1.28.0
  • v1.29.0
  • v1.29.1
  • v1.29.2
  • v1.3.0
  • v1.30.0
  • v1.30.1
  • v1.30.2
  • v1.31.0
  • v1.32.0
  • v1.4.0
  • v1.4.1
  • v1.5.0
  • v1.6.0
  • v1.6.1
  • v1.6.2
  • v1.7.0
  • v1.8.0
  • v1.8.1
  • v1.8.2
  • v1.9.0
  • v2.0.0
  • v2.1.0
  • v2.10.0
  • v2.10.1
  • v2.11.0
  • v2.12.0
  • v2.12.1
  • v2.12.2
  • v2.12.3
  • v2.12.4
  • v2.12.5
  • v2.13.0
  • v2.13.1
  • v2.13.2
  • v2.13.3
  • v2.13.4
  • v2.14.0
  • v2.15.0
200 results

Target

Select target project
  • coscine/frontend/apps/ui
1 result
Select Git revision
  • 2.11.0-privacyPolicy
  • APIv2
  • Docs/Setup
  • Experiment/fix-debugging
  • Experimental/Heinrichs-cypress
  • Feature/xxxx-turnOffDataPub
  • Fix/xxxx-ToS400Error
  • Fix/xxxx-migrateLogin
  • Fix/xxxx-tokenUploadButton
  • Hotfix/0038-correctDownload
  • Hotfix/1917-PublicFilesVisibility
  • Hotfix/1963-fixOrganizationField
  • Hotfix/2015-PublicFilesVisibility
  • Hotfix/2130-uiv2ContactChange
  • Hotfix/2144-invitationCall
  • Hotfix/2150-fixUpload
  • Hotfix/2160-userOrgsInst
  • Hotfix/2190-requiredFieldsForUserProfile
  • Hotfix/2196-RCVTableTranslation
  • Hotfix/2212-fixFiles
  • Hotfix/2226-userProfileSaveButton
  • Hotfix/2232-dependencyFix
  • Hotfix/2233-fixMe
  • Hotfix/2258-saveButtonWorksAsExpected
  • Hotfix/2296-selectedValuesNotReturned
  • Hotfix/2308-defaultLicense
  • Hotfix/2335-fixingSearchRCV
  • Hotfix/2353-dropShape
  • Hotfix/2370-fixDeleteButton
  • Hotfix/2378-linkedFix
  • Hotfix/2379-filesDragAndDrop
  • Hotfix/2382-guestStillBuggy
  • Hotfix/2384-guestsAndLinked
  • Hotfix/2427-adminTrouble
  • Hotfix/2459-EncodingPath
  • Hotfix/2465-orcidLink
  • Hotfix/2465-orcidLink-v1.25.1
  • Hotfix/2504-formGen
  • Hotfix/2541-resCreate
  • Hotfix/2601-correctMetadataIdentity
  • Hotfix/2611-feedback
  • Hotfix/2618-turtle
  • Hotfix/2681-validationErrors
  • Hotfix/2684-correctEncoding
  • Hotfix/2684-fixSubMetadata
  • Hotfix/2713-validateEntryName
  • Hotfix/2734-allowEmptyLicense
  • Hotfix/2765-encodingAgain
  • Hotfix/2852-adaptTextForToSUi
  • Hotfix/2853-optimizationV4
  • Hotfix/2943-reloadingResources
  • Hotfix/2943-searchHighlighting
  • Hotfix/2957-styleAndUpgrade
  • Hotfix/2971-fixTextInDataPub
  • Hotfix/2989-cookieLength
  • Hotfix/662-keepSidebarExpanded
  • Hotfix/xxxx-correctLinking
  • Hotfix/xxxx-folderRecursive
  • Hotfix/xxxx-fullscreenCss
  • Hotfix/xxxx-homepageDisplay
  • Hotfix/xxxx-liveReleaseFixes
  • Hotfix/xxxx-partnerProjects
  • Hotfix/xxxx-workingFileIndex
  • Issue/1782-structualDataIntegration
  • Issue/1792-newMetadataStructure
  • Issue/1822-coscineUIv2App
  • Issue/1824-componentsUIv2
  • Issue/1824-routerAdditions
  • Issue/1825-codeQualityPipelines
  • Issue/1833-newLogin
  • Issue/1843-multipleFilesValidation
  • Issue/1860-searchScoping
  • Issue/1861-searchMetadata
  • Issue/1862-searchFacets
  • Issue/1863-paginationForSearch
  • Issue/1926-userProfile
  • Issue/1927-projectAppMigration
  • Issue/1928-sidebarmenuAddition
  • Issue/1929-vuexToPinia
  • Issue/1938-internalHandling
  • Issue/1951-quotaImplementation
  • Issue/1953-owlImports
  • Issue/1957-resourceAppMigration
  • Issue/1957-resourceAppMigrationNew
  • Issue/1962-SearchAppUI2
  • Issue/1964-tokenExpiryUIv2
  • Issue/1965-userListMigration
  • Issue/1970-breadcrumbs
  • Issue/1971-projectEditCreateMigration
  • Issue/1972-homeDepot
  • Issue/1974-shibbolethLogout
  • Issue/1976-resouceCreationVaildEmail
  • Issue/1979-supportAdminUIv2Migration
  • Issue/1980-userManagement
  • Issue/1985-adaptSidebar
  • Issue/2002-migrateResourceCreate
  • Issue/2003-resourceSettings
  • Issue/2008-quotaManagement
  • Issue/2011-pathConfig
  • Issue/2016-BannerMigration
  • 1.28.0-pilot
  • v1.0.0
  • v1.1.0
  • v1.10.0
  • v1.10.1
  • v1.10.2
  • v1.10.3
  • v1.11.0
  • v1.11.1
  • v1.11.2
  • v1.11.3
  • v1.11.4
  • v1.11.5
  • v1.11.6
  • v1.11.7
  • v1.12.0
  • v1.13.0
  • v1.14.0
  • v1.14.1
  • v1.14.2
  • v1.14.3
  • v1.15.0
  • v1.15.1
  • v1.16.0
  • v1.16.1
  • v1.16.2
  • v1.16.3
  • v1.17.0
  • v1.17.1
  • v1.17.2
  • v1.18.0
  • v1.18.1
  • v1.19.0
  • v1.2.0
  • v1.20.0
  • v1.20.1
  • v1.20.2
  • v1.20.3
  • v1.20.4
  • v1.20.5
  • v1.21.0
  • v1.22.0
  • v1.22.1
  • v1.22.2
  • v1.23.0
  • v1.23.1
  • v1.23.2
  • v1.23.3
  • v1.23.4
  • v1.23.5
  • v1.23.6
  • v1.23.6-patch-2417-2427
  • v1.24.0
  • v1.24.1
  • v1.25.0
  • v1.25.1
  • v1.26.0
  • v1.26.1
  • v1.27.0
  • v1.27.1
  • v1.27.1-pilot
  • v1.28.0
  • v1.29.0
  • v1.29.1
  • v1.29.2
  • v1.3.0
  • v1.30.0
  • v1.30.1
  • v1.30.2
  • v1.31.0
  • v1.32.0
  • v1.4.0
  • v1.4.1
  • v1.5.0
  • v1.6.0
  • v1.6.1
  • v1.6.2
  • v1.7.0
  • v1.8.0
  • v1.8.1
  • v1.8.2
  • v1.9.0
  • v2.0.0
  • v2.1.0
  • v2.10.0
  • v2.10.1
  • v2.11.0
  • v2.12.0
  • v2.12.1
  • v2.12.2
  • v2.12.3
  • v2.12.4
  • v2.12.5
  • v2.13.0
  • v2.13.1
  • v2.13.2
  • v2.13.3
  • v2.13.4
  • v2.14.0
  • v2.15.0
200 results
Show changes
Commits on Source (5)
Showing
with 3765 additions and 40 deletions
packageExtensions:
"@vue/cli-service@*":
peerDependencies:
"@vue/cli-plugin-eslint": "*"
"@vue/cli-plugin-router": "*"
"@vue/cli-plugin-typescript": "*"
"fork-ts-checker-webpack-plugin@*":
dependencies:
"vue-template-compiler": "*"
......
{
"name": "ui",
"version": "1.4.1",
"version": "1.5.0",
"private": true,
"scripts": {
"dev": "vite",
......@@ -11,12 +11,14 @@
},
"dependencies": {
"@coscine/api-client": "^1.5.1",
"@coscine/form-generator": "^1.18.0",
"@vueuse/core": "^6.5.3",
"axios": "^0.26.1",
"bootstrap": "^4.6.1",
"bootstrap-icons": "^1.8.1",
"bootstrap-vue": "^2.21.2",
"core-js": "^3.21.1",
"file-saver": "^2.0.5",
"http-status-codes": "^2.2.0",
"jose": "^4.6.0",
"jquery": "^3.6.0",
......@@ -24,6 +26,7 @@
"lodash": "^4.17.21",
"moment": "^2.29.1",
"pinia": "^2.0.12",
"rdf-validate-shacl": "^0.4.3",
"sass": "^1.49.9",
"semantic-release": "^19.0.2",
"uuid": "^8.3.2",
......@@ -43,7 +46,9 @@
"@semantic-release/gitlab": "^7.0.4",
"@semantic-release/npm": "^8.0.3",
"@semantic-release/release-notes-generator": "^10.0.3",
"@types/file-saver": "^2.0.5",
"@types/lodash": "^4.14.178",
"@types/rdf-validate-shacl": "^0.4.0",
"@types/uuid": "^8.3.4",
"@types/vue-select": "^3.16.0",
"@types/vuelidate": "^0.7.15",
......
......@@ -52,8 +52,9 @@ export default defineComponent({
},
});
</script>
<style>
.mandatory .col-form-label:after {
<style scoped>
.mandatory >>> .col-form-label:after {
content: " *";
color: #a70619;
}
......
......@@ -14,7 +14,10 @@ import { useProjectStore } from "@/modules/project/store";
// import the main store
import { useMainStore } from "@/store/index";
import type { ResourceObject } from "@coscine/api-client/dist/types/Coscine.Api.Resources";
import type { VisitedResourceObject } from "./types";
import type { ResourceTypeInformation } from "@coscine/api-client/dist/types/Coscine.Api.Resources";
import VueI18n from "vue-i18n";
import { cloneDeep } from "lodash";
export default defineComponent({
setup() {
......@@ -24,21 +27,35 @@ export default defineComponent({
return { mainStore, resourceStore, projectStore };
},
i18n: { messages: ResourceI18nMessages },
i18n: { messages: cloneDeep(ResourceI18nMessages) },
created() {
this.initialize();
},
computed: {
resource(): ResourceObject | null {
resource(): VisitedResourceObject | null {
return this.resourceStore.currentResource;
},
resourceTypeInformation(): null | undefined | ResourceTypeInformation {
if (this.resourceStore.resourceTypes && this.resource) {
return this.resourceStore.resourceTypes.find(
(resourceType) => resourceType.id === this.resource?.type?.id
);
}
return null;
},
moduleIsReady(): boolean {
return true;
},
},
watch: {
resourceTypeInformation() {
this.setI18n();
},
},
methods: {
async initialize() {
// Resource may be unset (e.g. when entering from a direct link)
......@@ -46,6 +63,61 @@ export default defineComponent({
this.resource,
this.$router.currentRoute
);
if (this.resource) {
if (!this.resourceStore.currentFullApplicationProfile) {
this.resourceStore.retrieveApplicationProfile(this.resource);
}
if (
!this.resourceStore.currentUsedQuota &&
this.resourceTypeInformation?.isQuotaAdjustable
) {
this.resourceStore.retrieveUsedQuota(this.resource);
}
}
this.setI18n();
},
setI18n() {
this.$i18n.mergeLocaleMessage("de", cloneDeep(ResourceI18nMessages.de));
this.$i18n.mergeLocaleMessage("en", cloneDeep(ResourceI18nMessages.en));
if (
this.resourceTypeInformation &&
this.resourceTypeInformation.displayName
) {
if (ResourceI18nMessages.de.resourceType) {
const germanResourceTypeMessageObject = ResourceI18nMessages.de
.resourceType as VueI18n.LocaleMessageObject;
if (
germanResourceTypeMessageObject[
this.resourceTypeInformation.displayName
]
) {
const resourceTypeDefinition = germanResourceTypeMessageObject[
this.resourceTypeInformation.displayName
] as VueI18n.LocaleMessageObject;
this.$i18n.mergeLocaleMessage(
"de",
cloneDeep(resourceTypeDefinition)
);
}
}
if (ResourceI18nMessages.en.resourceType) {
const englishResourceTypeMessageObject = ResourceI18nMessages.en
.resourceType as VueI18n.LocaleMessageObject;
if (
englishResourceTypeMessageObject[
this.resourceTypeInformation.displayName
]
) {
const resourceTypeDefinition = englishResourceTypeMessageObject[
this.resourceTypeInformation.displayName
] as VueI18n.LocaleMessageObject;
this.$i18n.mergeLocaleMessage(
"en",
cloneDeep(resourceTypeDefinition)
);
}
}
}
},
},
});
......
<template>
<div class="DataSource">
<FilesViewHeader
@clickFileSelect="clickFileSelect"
:isUploading="isUploading"
v-model="filter"
/>
<b-row>
<b-table
id="resourceViewTable"
:fields="headers"
:items="folderContents"
:busy="isBusy"
:locale="$i18n.locale"
:filter="filter"
:filter-included-fields="filterFields"
selectable
striped
bordered
outlined
hover
small
responsive
head-variant="dark"
class="adaptTable"
ref="adaptTable"
@row-selected="onRowSelected"
show-empty
:empty-text="$parent.$parent.$t('page.resource.emptyTableText')"
:empty-filtered-text="
$parent.$parent.$t('page.resource.emptyFilterText')
"
>
<div slot="table-busy" class="text-center text-danger my-2">
<b-spinner class="align-middle"></b-spinner>
<strong style="margin-left: 1%">{{
$parent.$parent.$t("page.resource.loading")
}}</strong>
</div>
<template v-slot:head(name)="row">
<b-form-checkbox
v-model="selectAll"
@change="allSelect(row)"
></b-form-checkbox>
<span>{{ $parent.$parent.$t("page.resource.fileName") }}</span>
</template>
<template v-slot:head(lastModified)="row">
<span>{{ row.label }}</span>
</template>
<template v-slot: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>
<template v-slot:head(addColumn)>
<b-dropdown size="sm" right :no-caret="true" id="addColumnDropDown">
<template v-slot:button-content>
<b-icon icon="arrow-down" />
</template>
<b-form-checkbox
v-for="column in columns"
v-model="column.active"
:key="column.value"
:value="column.value"
stacked
>
{{ column.label }}
</b-form-checkbox>
</b-dropdown>
</template>
<template v-slot:cell(name)="row">
<span class="checkFile">
<b-form-checkbox
class="tableCheck"
v-model="row.rowSelected"
@change="select(row)"
/>
<b-icon
:icon="row.item.isFolder ? 'folder' : 'file-earmark'"
></b-icon>
</span>
<a
v-if="!editableDataUrl"
class="dataSourceItem"
:href="'#' + row.item.absolutePath"
@click="
row.item.isFolder ? openFolder(row.item) : openFile(row.item)
"
>{{ row.item.name }}</a
>
<a v-else>{{ row.item.name }}</a>
<b-dropdown
class="dotMenu"
left
variant="link"
toggle-class="text-decoration-none"
size="sm"
:no-caret="true"
:disabled="editableDataUrl && resource.archived"
>
<template v-slot:button-content> ... </template>
<b-dropdown-item
v-if="!editableDataUrl"
@click="
row.item.isFolder ? openFolder(row.item) : openFile(row.item)
"
>{{
$parent.$parent.$t("page.resource.metadataManagerBtnDownload")
}}</b-dropdown-item
>
<b-dropdown-item
@click="deleteFile(row.item)"
:disabled="resource.archived"
>{{ $t("buttons.delete") }}</b-dropdown-item
>
</b-dropdown>
</template>
<template v-slot:cell(size)="row">
{{ renderSize(row.item) }}
</template>
<template v-slot:cell(lastModified)="row">
{{ renderDate(row.item) }}
</template>
</b-table>
</b-row>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, reactive } from "vue-demi";
// import the store for current module
import { useResourceStore } from "../store";
import { useProjectStore } from "@/modules/project/store";
// import the main store
import { useMainStore } from "@/store/index";
import FilesViewHeader from "./FilesViewHeader.vue";
import type {
FileInformation,
FolderContent,
FolderInformation,
ReadOnlyFolderInformation,
} from "../utils/EntryDefinition";
import MetadataManagerUtil from "../utils/MetadataManagerUtil";
import fileSaver from "file-saver";
import type { ResourceTypeInformation } from "@coscine/api-client/dist/types/Coscine.Api.Resources";
import type {
ApplicationProfile,
Metadata,
VisitedResourceObject,
} from "../types";
import type { BFormRow, BTable, BvTableField } from "bootstrap-vue";
import { FileUtil } from "../utils/FileUtil";
import { v4 as uuidv4 } from "uuid";
interface CustomTableField extends BvTableField {
key: string;
active: boolean;
}
export default defineComponent({
setup() {
const mainStore = useMainStore();
const resourceStore = useResourceStore();
const projectStore = useProjectStore();
return { mainStore, resourceStore, projectStore };
},
computed: {
applicationProfile(): ApplicationProfile | null {
return this.resourceStore.currentFullApplicationProfile;
},
resource(): null | VisitedResourceObject {
return this.resourceStore.currentResource;
},
resourceTypeInformation(): null | undefined | ResourceTypeInformation {
if (this.resourceStore.resourceTypes && this.resource) {
return this.resourceStore.resourceTypes.find(
(resourceType) => resourceType.id === this.resource?.type?.id
);
}
return null;
},
editableDataUrl(): boolean {
return (
this.resourceTypeInformation?.resourceContent?.metadataView
?.editableDataUrl === 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;
},
visibleColumns(): Array<string> {
let visibleColumns: Array<string> = [];
const collectedColumns =
this.resourceTypeInformation?.resourceContent?.entriesView?.columns
?.always;
if (collectedColumns) {
visibleColumns = collectedColumns;
}
return visibleColumns;
},
},
data() {
return {
defaultHeaders: [
{
label: this.$parent.$parent.$t("page.resource.fileName").toString(),
key: "name",
sortable: true,
active: true,
},
{
label: this.$parent.$parent
.$t("page.resource.lastModified")
.toString(),
key: "lastModified",
sortable: true,
active: true,
},
{
label: this.$parent.$parent.$t("page.resource.size").toString(),
key: "size",
sortable: true,
active: true,
},
] as Array<CustomTableField>,
columns: [] as CustomTableField[],
isBusy: true,
selectAll: false,
filter: "",
folderPath: [] as string[],
selectableFiles: [] as FolderContent[],
};
},
components: {
FilesViewHeader,
},
props: {
folderContents: {
default() {
return [];
},
type: Array as PropType<FolderContent[]>,
},
currentFolder: {
default: "/",
type: String,
},
fileListEdit: {
default() {
return [];
},
type: Array as PropType<FolderContent[]>,
},
isUploading: Boolean,
},
watch: {
applicationProfile() {
this.getColumns();
},
headers() {
this.saveInLocalStorage();
},
resource() {
this.getData();
},
},
created() {
this.isBusy = true;
this.getColumns();
if (this.resource) {
this.getData();
}
},
methods: {
getColumns() {
this.columns.length = 0;
// keys a, b, name, active
if (
this.applicationProfile &&
this.applicationProfile.length > 0 &&
this.applicationProfile[0]["@graph"]
) {
const entries = this.applicationProfile[0]["@graph"] as Metadata[];
const oldColumns = this.loadFromLocalStorage();
for (const entry of entries) {
if (
typeof entry !== "string" &&
Object.prototype.hasOwnProperty.call(
entry,
"http://www.w3.org/ns/shacl#name"
)
) {
if (
Object.prototype.hasOwnProperty.call(
entry,
"https://purl.org/coscine/invisible"
) &&
typeof entry["https://purl.org/coscine/invisible"][0] !==
"string" &&
!Array.isArray(entry["https://purl.org/coscine/invisible"][0]) &&
entry["https://purl.org/coscine/invisible"][0]["@value"] === "1"
) {
continue;
}
for (const name of entry[
"http://www.w3.org/ns/shacl#name"
] as Metadata[]) {
if (
typeof name !== "string" &&
name["@language"] === this.$i18n.locale &&
typeof entry["http://www.w3.org/ns/shacl#path"][0] !==
"string" &&
!Array.isArray(entry["http://www.w3.org/ns/shacl#path"][0])
) {
const identifier =
entry["http://www.w3.org/ns/shacl#path"][0]["@id"];
// read the active value from the localStorage
let activeColumn = false;
for (const oldColumn of oldColumns) {
if (oldColumn.key === identifier) {
activeColumn = oldColumn.active;
}
}
this.columns.push(
reactive({
label: name["@value"] as string,
key: identifier as string,
sortable: true,
active: activeColumn,
formatter: (value, key, item: FolderContent) => {
const metadata = item.metadata;
if (metadata && metadata[key]) {
const formatEntry = metadata[key][0];
if (
typeof formatEntry !== "string" &&
!Array.isArray(formatEntry)
) {
return formatEntry["value"];
}
}
return "";
},
})
);
}
}
}
}
}
},
getData() {
// Doesn't end with '/' => Probably a file
let currentFolder = this.currentFolder;
if (!currentFolder.endsWith("/")) {
// TODO: Change to open modal
this.openFile({
id: uuidv4(),
isFolder: false,
name: currentFolder.substring(currentFolder.lastIndexOf("/") + 1),
path: currentFolder,
absolutePath: currentFolder,
version: `${+new Date()}`,
size: 0,
metadata: {},
});
currentFolder = currentFolder.substring(
0,
currentFolder.lastIndexOf("/") + 1
);
}
if (currentFolder !== "/") {
let combine = "";
const folderSplit = currentFolder.split("/");
for (const folderKey in folderSplit) {
if (Object.prototype.hasOwnProperty.call(folderSplit, folderKey)) {
const folder = folderSplit[folderKey];
if (combine !== "" && folder === "") {
continue;
}
combine += folder + "/";
this.folderPath.push(combine);
}
}
this.folderPath.pop();
if (this.folderPath.length > 0) {
this.$emit("currentFolder", this.folderPath.pop());
} else {
this.$emit("currentFolder", "");
}
} else {
this.$emit("currentFolder", "");
}
this.openFolder({
id: uuidv4(),
isFolder: true,
name: currentFolder.substring(currentFolder.lastIndexOf("/") + 1),
path: currentFolder,
absolutePath: currentFolder,
metadata: {},
});
},
saveInLocalStorage() {
if (this.columns.length > 0 && this.resource) {
this.resource.storedColumns = JSON.stringify(this.columns);
}
},
loadFromLocalStorage() {
let element = null;
if (this.resource && this.resource.storedColumns) {
element = this.resource.storedColumns;
}
let json: Array<CustomTableField> = [];
if (element !== null) {
json = JSON.parse(element);
}
return json;
},
removeColumn(row: BFormRow) {
for (const column of this.columns) {
if (column.key === row.column) {
column.active = false;
}
}
},
async deleteFile(file: FileInformation) {
this.$emit("waitingForResponse", true);
await this.resourceStore.deleteFile(this.resource, file.absolutePath);
this.$emit("waitingForResponse", false);
location.reload();
},
async openFolder(folder: FolderInformation) {
this.isBusy = true;
// Empty file list, since the context changes
// (Other workaround would be to keep the editing files open)
this.$emit("fileListEdit", []);
// Show location change
window.location.hash = folder.absolutePath;
const response = await this.resourceStore.getMetadata(
this.resource,
folder.absolutePath
);
const tmpFolder: FolderContent[] = [];
if (folder.name === "..") {
this.$emit("currentFolder", this.folderPath.pop());
} else if (this.currentFolder !== "") {
this.folderPath.push(this.currentFolder);
this.$emit("currentFolder", folder.absolutePath);
} else {
this.$emit("currentFolder", folder.absolutePath);
}
if (this.folderPath.length > 0) {
const path = this.folderPath[this.folderPath.length - 1];
const navigateUpFolder: ReadOnlyFolderInformation = {
id: uuidv4(),
isFolder: true,
name: "..",
absolutePath: path,
path: path,
readOnly: true,
metadata: {},
};
tmpFolder.push(navigateUpFolder);
}
for (const obj of response.data.fileStorage) {
const newEntry: FileInformation = {
id: uuidv4(),
isFolder: obj.IsFolder,
name: obj.Name,
path: obj.Path,
absolutePath: obj.Path,
lastModified: obj.Modified,
created: obj.Created,
size: obj.Size,
version: `${+new Date()}`,
metadata: {},
};
const resultArray = MetadataManagerUtil.filterMetadataStorage(
response.data.metadataStorage,
newEntry.absolutePath
);
if (resultArray.length > 0) {
const result = resultArray[0][Object.keys(resultArray[0])[0]];
newEntry.metadata = result as Metadata;
}
tmpFolder.push(newEntry);
}
this.isBusy = false;
this.$emit("folderContents", tmpFolder);
},
async openFile(file: FileInformation) {
const response = await this.resourceStore.getFile(
this.resource,
file.absolutePath,
true
);
if (response !== null) {
fileSaver.saveAs(response, file.name);
}
window.location.hash = this.currentFolder;
},
onRowSelected(items: FolderContent[]) {
this.$emit("showDetail", items.length > 0);
this.selectAll = items.length === this.folderContents.length;
// Filter out folders like ".."
this.selectableFiles = items.filter((item) => !item.readOnly);
this.updateFileListEdit();
},
allSelect(row: BTable) {
if (this.selectAll) {
row.selectAllRows();
} else {
row.clearSelected();
}
},
select(row: { rowSelected: boolean; index: number }) {
if (row.rowSelected) {
(this.$refs.adaptTable as BTable).selectRow(row.index);
} else {
(this.$refs.adaptTable as BTable).unselectRow(row.index);
}
},
clickFileSelect() {
this.$emit("clickFileSelect");
},
updateFileListEdit() {
const newFileListEdit = [] as FolderContent[];
for (const currentFileEdit of this.fileListEdit) {
const selectedFile = this.selectableFiles.find(
(f) =>
this.currentFolder === currentFileEdit.path &&
f.name === currentFileEdit.name
);
if (selectedFile) {
newFileListEdit.push(currentFileEdit);
}
}
for (const currentSelectableFile of this.selectableFiles) {
const selectableFile = currentSelectableFile;
const fileEdit = newFileListEdit.find(
(f) => f.path === this.currentFolder && f.name === selectableFile.name
);
if (!fileEdit) {
MetadataManagerUtil.loadMetadata(
(loadedMetadata) => {
newFileListEdit.push({
id: uuidv4(),
path: this.currentFolder,
version: `${+new Date()}`,
uploading: false,
name: selectableFile.name,
metadata: loadedMetadata,
isFolder: selectableFile.isFolder,
absolutePath: selectableFile.absolutePath,
info:
selectableFile.isFolder === false
? selectableFile.info
: undefined,
size:
selectableFile.isFolder === false ? selectableFile.size : 0,
});
},
selectableFile,
this.resource
);
}
}
this.$emit("fileListEdit", newFileListEdit);
if (this.selectableFiles.length > 0) {
this.$emit("showDetail", true);
} else {
this.$emit("showDetail", false);
}
},
renderDate(item: FolderContent): string {
if (item.lastModified === null || item.lastModified === undefined) {
return "";
}
const date = item.lastModified;
const dateObject = new Date(date);
return dateObject.toLocaleDateString(this.$i18n.locale);
},
renderSize(item: FolderContent): string {
if (!item.isFolder) {
return FileUtil.formatBytes(item.size);
}
return "";
},
},
});
</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 {
margin-left: 14px;
margin-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;
cursor: pointer;
}
#resourceViewTable tr:hover .dotMenu button {
font-weight: bold;
margin: 0;
}
#resourceViewTable .dotMenu {
position: absolute;
right: 0px;
margin-top: -5px;
}
#resourceViewTable .dotMenu .dropdown-item {
margin: 1px;
}
</style>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#resourceViewTable {
overflow: visible;
}
#resourceViewTable >>> .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;
}
#resourceViewTable th .additionalColumnHeader svg {
cursor: pointer;
}
.adaptTable {
overflow: auto;
position: absolute;
top: 65px;
bottom: 0px;
left: 10px;
right: 10px;
width: auto;
}
.rightFloating {
float: right;
}
.dataSourceItem {
vertical-align: sub;
}
.DataSource {
height: inherit;
margin-top: 7px;
}
.tableCheck {
text-align: center;
margin-right: -0.5em;
position: absolute;
top: 15px;
left: 17px;
cursor: pointer;
}
#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>
<template>
<b-row class="headerRow">
<div class="col-sm-7 file-view-header">
<span>
<p
class="h4"
v-if="
resource &&
resource.type &&
resource.type.displayName &&
resource.displayName
"
>
{{
$parent.$parent.$parent.$t(
"ResourceTypes." + resource.type.displayName + ".displayName"
)
}}: {{ resource.displayName }}
</p>
</span>
<b-icon
id="resourceDetails"
icon="info-circle"
:title="$parent.$parent.$parent.$t('page.resource.info')"
/>
<b-popover
v-if="resource"
over
custom-class="b-popover"
target="resourceDetails"
triggers="hover focus"
placement="bottom"
>
<template v-slot:title
><b>{{ resource.displayName }}</b></template
>
<div v-if="resource.displayName">
<span
><b
>{{ $parent.$parent.$parent.$t("page.resource.displayName") }}:
</b> </span
><span>{{ resource.displayName }}</span>
</div>
<div v-if="resource.pid">
<span
><b
>{{ $parent.$parent.$parent.$t("page.resource.PID") }}:
</b> </span
><span>{{ resource.pid }}</span>
</div>
<div v-if="resource.description">
<span
><b
>{{ $parent.$parent.$parent.$t("page.resource.description") }}:
</b> </span
><span>{{ resource.description }}</span
><br />
</div>
<div v-if="resource.disciplines && resource.disciplines.length > 0">
<span
><b
>{{ $parent.$parent.$parent.$t("page.resource.disciplines") }}:
</b>
</span>
<ul>
<li
v-for="discipline in resource.disciplines"
v-bind:key="discipline.id"
>
<div v-if="$i18n.locale === 'de'">
{{ discipline.displayNameDe }}
</div>
<div v-else>
{{ discipline.displayNameEn }}
</div>
</li>
</ul>
</div>
<div v-if="resource.keywords && resource.keywords.length > 0">
<span
><b
>{{ $parent.$parent.$parent.$t("page.resource.keywords") }}:
</b> </span
><br />
<ul>
<li v-for="keyword in resource.keywords" v-bind:key="keyword">
{{ keyword }}
</li>
</ul>
</div>
<div v-if="resource.visibility">
<span
><b
>{{ $parent.$parent.$parent.$t("page.resource.visibility") }}:
</b> </span
><span>{{ resource.visibility.displayName }}</span
><br />
</div>
<div v-if="resource.license">
<span
><b
>{{
$parent.$parent.$parent.$t("page.resource.resourceLicense")
}}:
</b> </span
><span>{{ resource.license.displayName }}</span
><br />
</div>
<div v-if="resource.usageRights">
<span
><b
>{{ $parent.$parent.$parent.$t("page.resource.usageRights") }}:
</b> </span
><span>{{ resource.usageRights }}</span
><br />
</div>
</b-popover>
<b-button
@click="edit"
:title="$parent.$parent.$parent.$t('page.resource.edit')"
class="btn btn-sm"
v-if="canEditResource"
>
<b-icon
icon="pencil-fill"
:title="$parent.$parent.$parent.$t('page.resource.edit')"
/>
</b-button>
<b-button
@click="upload"
:title="$parent.$parent.$parent.$t('page.resource.upload')"
class="btn btn-sm"
:disabled="isUploading || readOnly || (resource && resource.archived)"
>
<b-icon
icon="plus"
:title="$parent.$parent.$parent.$t('page.resource.upload')"
/>
</b-button>
<span v-if="resource && resource.archived" class="badgeWrap">
<b-badge pill variant="warning">{{ $t("default.archived") }}</b-badge>
</span>
</div>
<div class="col-sm-3">
<div
v-if="maxSize !== undefined && used !== undefined"
class="progressContainer"
>
<b-progress :max="maxSize">
<b-progress-bar :value="used">
<span v-if="used / maxSize >= 0.5">
<strong
>{{ formatBytes(used) }} / {{ formatBytes(maxSize) }}</strong
>
</span>
</b-progress-bar>
<b-progress-bar :value="maxSize - used" variant="secondary">
<span v-if="used / maxSize < 0.5">
<strong
>{{ formatBytes(used) }} / {{ formatBytes(maxSize) }}</strong
>
</span>
</b-progress-bar>
</b-progress>
</div>
</div>
<div class="col-sm-2 searchColumn">
<b-input-group>
<b-form-input
type="search"
id="filterInput"
:placeholder="
$parent.$parent.$parent.$t('page.resource.typeToSearch')
"
:value="value"
@input="$emit('input', $event)"
></b-form-input>
</b-input-group>
</div>
</b-row>
</template>
<script lang="ts">
import { defineComponent } from "vue-demi";
// import the store for current module
import { useResourceStore } from "../store";
import { useProjectStore } from "@/modules/project/store";
// import the user store
import { useUserStore } from "@/modules/user/store";
// import the main store
import { useMainStore } from "@/store/index";
import { FileUtil } from "../utils/FileUtil";
import {
ProjectObject,
ProjectQuotaReturnObject,
UserObject,
} from "@coscine/api-client/dist/types/Coscine.Api.Project";
import router from "@/router";
import type { ResourceTypeInformation } from "@coscine/api-client/dist/types/Coscine.Api.Resources";
import type { VisitedResourceObject } from "../types";
export default defineComponent({
setup() {
const mainStore = useMainStore();
const resourceStore = useResourceStore();
const projectStore = useProjectStore();
const userStore = useUserStore();
return { mainStore, resourceStore, projectStore, userStore };
},
computed: {
isOwner(): boolean | undefined {
return this.projectStore.currentUserRoleIsOwner;
},
project(): null | ProjectObject {
return this.projectStore.currentProject;
},
quotas(): ProjectQuotaReturnObject[] | null {
return this.projectStore.currentQuotas;
},
resource(): null | VisitedResourceObject {
return this.resourceStore.currentResource;
},
resourceTypeInformation(): null | undefined | ResourceTypeInformation {
if (this.resourceStore.resourceTypes && this.resource) {
return this.resourceStore.resourceTypes.find(
(resourceType) => resourceType.id === this.resource?.type?.id
);
}
return null;
},
user(): null | UserObject {
return this.userStore.user;
},
canEditResource(): boolean {
if (this.resource && this.user) {
return this.resource.creator === this.user.id || this.isOwner === true;
}
return false;
},
readOnly(): boolean {
return this.resourceTypeInformation?.resourceContent?.readOnly === true;
},
maxSize(): number | undefined {
if (
this.resource &&
this.resource.resourceTypeOption &&
this.resource.resourceTypeOption.Size !== undefined
) {
return this.resource.resourceTypeOption.Size * 1024 * 1024 * 1024;
}
return undefined;
},
used(): number | undefined {
if (this.resourceStore.currentUsedQuota) {
return this.resourceStore.currentUsedQuota;
}
return undefined;
},
},
name: "Header",
props: {
isUploading: Boolean,
value: String,
},
methods: {
edit() {
if (
this.resource &&
this.resource.id &&
this.project &&
this.project.slug
) {
router.push({
name: "resource-settings",
params: {
slug: this.project.slug,
guid: this.resource.id,
},
});
}
},
upload() {
this.$emit("clickFileSelect");
},
formatBytes(bytes: number) {
return FileUtil.formatBytes(bytes);
},
},
});
</script>
<style scoped>
.file-view-header {
padding-left: 25px;
}
.searchColumn {
padding-right: 25px;
}
.file-view-header >>> p.h4 {
display: inline-block;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - 11rem);
}
.file-view-header >>> svg {
vertical-align: top;
margin-left: 3px;
margin-right: 3px;
}
.file-view-header >>> button {
vertical-align: top;
margin: 0 4px 0px 4px;
}
.btn-sm {
min-width: 0;
width: 30px;
padding: 2px;
}
#resourceDetails:hover {
cursor: pointer;
}
.progress {
height: 1.15rem;
max-width: 15rem;
margin: 5px auto;
}
.progressContainer {
margin: 0 auto;
}
.b-popover {
max-width: 100%;
}
.badgeWrap >>> .badge {
vertical-align: top;
height: 1.3rem;
}
#filterInput {
max-height: 30px;
}
</style>
<template>
<b-container id="detail-view">
<MetadataManagerHeader
v-if="resource"
:editableDataUrl="editableDataUrl"
:isUploading="isUploading"
:readOnly="readOnly"
:resource="resource"
:showDetail="showDetail"
:shownFiles="shownFiles"
@download="download"
@selectFiles="selectFiles"
@showModalDeleteFolderContents="showModalDeleteFolderContents"
/>
<MetadataManagerTable
:currentFileId="currentFileId"
:currentFolderContent="currentFolderContent"
:editableDataUrl="editableDataUrl"
:editableKey="editableKey"
:showDetail="showDetail"
:shownFiles="shownFiles"
@changeMetadata="changeMetadata"
@loadAllFilesTab="loadAllFilesTab"
@removeElement="removeElement"
/>
<b-row id="metadataManagerMetadataSection">
<b-col>
<MetadataManagerFileInformation
v-if="
showDetail &&
fileListEdit.length > 0 &&
currentFileId >= 0 &&
currentFileId < fileListEdit.length
"
:currentFileId="currentFileId"
:fileListEdit="fileListEdit"
/>
<MetadataManagerSpecialProperties
v-if="
shownFiles.length > 0 &&
currentFileId >= 0 &&
resource &&
currentFolderContent
"
:currentFolderContent="currentFolderContent"
:editableDataUrl="editableDataUrl"
:editableKey="editableKey"
:readOnly="readOnly"
:resource="resource"
/>
<span
v-if="resource && resource.applicationProfile !== ''"
class="generatedFormSpan"
>
<FormGenerator
class="generatedForm"
:key="formGeneratorKey"
:applicationProfileId="resource.applicationProfile"
:fixedValues="resource.fixedValues"
:formData="currentMetadata"
:SHACLDefinition="applicationProfileString"
:disabledMode="resource.archived"
:classReceiver="receiveClass"
:userReceiver="async () => user"
mimeType="application/ld+json"
@isValid="isValid"
/>
</span>
</b-col>
</b-row>
<MetadataManagerFooter
:isUploading="isUploading"
:numberOfCurrentlyProcessedFiles="numberOfCurrentlyProcessedFiles"
:progressStatus="progressStatus"
:saveButtonDisabled="saveButtonDisabled"
:showDetail="showDetail"
:totalNumberOfCurrentlyProcessedFiles="
totalNumberOfCurrentlyProcessedFiles
"
@update="update"
@uploadPreparation="uploadPreparation"
/>
<ValidationPopover :valid="valid" :validationResults="validationResults" />
<SaveDuplicateFilesModal
:visible="saveDuplicateFilesModalVisible"
:uploadFileListReplaceFiles="uploadFileListReplaceFiles"
@close="hideModalSaveDuplicateFiles"
@skip="skipModalSaveDuplicateFiles"
@ok="overwriteModalSaveDuplicateFiles"
/>
<DeleteFolderContentsModal
:visible="deleteFolderContentsModalVisible"
:shownFiles="shownFiles"
@close="hideModalDeleteFolderContents"
@ok="deleteModalDeleteFolderContents"
/>
</b-container>
</template>
<script lang="ts">
import { defineComponent, PropType, VNode } from "vue-demi";
// import the store for current module
import { useResourceStore } from "../store";
import { useProjectStore } from "@/modules/project/store";
import { useUserStore } from "@/modules/user/store";
// import the main store
import { useMainStore } from "@/store/index";
import fileSaver from "file-saver";
import MetadataManagerUtil from "../utils/MetadataManagerUtil";
import MetadataManagerHeader from "./metadata/MetadataManagerHeader.vue";
import MetadataManagerTable from "./metadata/MetadataManagerTable.vue";
import MetadataManagerFileInformation from "./metadata/MetadataManagerFileInformation.vue";
import MetadataManagerSpecialProperties from "./metadata/MetadataManagerSpecialProperties.vue";
import MetadataManagerFooter from "./metadata/MetadataManagerFooter.vue";
import DeleteFolderContentsModal from "./modals/DeleteFolderContentsModal.vue";
import SaveDuplicateFilesModal from "./modals/SaveDuplicateFilesModal.vue";
import ValidationPopover from "./popovers/ValidationPopover.vue";
import type { FileInformation, FolderContent } from "../utils/EntryDefinition";
import "@/plugins/form-generator";
import { cloneDeep } from "lodash";
import type {
ResourceObject,
ResourceTypeInformation,
} from "@coscine/api-client/dist/types/Coscine.Api.Resources";
import type { UserObject } from "@coscine/api-client/dist/types/Coscine.Api.User";
import type { BilingualLabels } from "@coscine/api-client/dist/types/Coscine.Api.Metadata";
import type ValidationReport from "rdf-validate-shacl/src/validation-report";
import { v4 as uuidv4 } from "uuid";
import type { ValidationResult } from "rdf-validate-shacl/src/validation-report";
import type { ApplicationProfile, Metadata } from "../types";
export default defineComponent({
setup() {
const mainStore = useMainStore();
const resourceStore = useResourceStore();
const projectStore = useProjectStore();
const userStore = useUserStore();
return { mainStore, resourceStore, projectStore, userStore };
},
components: {
MetadataManagerHeader,
MetadataManagerTable,
MetadataManagerFileInformation,
MetadataManagerSpecialProperties,
MetadataManagerFooter,
DeleteFolderContentsModal,
SaveDuplicateFilesModal,
ValidationPopover,
},
computed: {
applicationProfile(): ApplicationProfile | null {
return this.resourceStore.currentFullApplicationProfile;
},
applicationProfileString(): string | null {
if (this.applicationProfile) {
return JSON.stringify(this.applicationProfile);
}
return null;
},
resource(): null | ResourceObject {
return this.resourceStore.currentResource;
},
user(): null | UserObject {
return this.userStore.user;
},
resourceTypeInformation(): null | undefined | ResourceTypeInformation {
if (this.resourceStore.resourceTypes && this.resource) {
return this.resourceStore.resourceTypes.find(
(resourceType) => resourceType.id === this.resource?.type?.id
);
}
return null;
},
editableDataUrl(): boolean {
return (
this.resourceTypeInformation?.resourceContent?.metadataView
?.editableDataUrl === true
);
},
editableKey(): boolean {
return (
this.resourceTypeInformation?.resourceContent?.metadataView
?.editableKey === true
);
},
readOnly(): boolean {
return this.resourceTypeInformation?.resourceContent?.readOnly === true;
},
currentFolderContent(): FolderContent | undefined {
return this.shownFiles[this.currentFileId];
},
currentMetadata(): Metadata {
if (
this.currentFileId >= 0 &&
this.currentFileId < this.shownFiles.length
) {
if (
this.currentFolderContent &&
this.currentFolderContent.metadata !== undefined
) {
return this.currentFolderContent.metadata;
} else {
return {};
}
} else {
return this.metadataTemplate;
}
},
formGeneratorKey(): string {
return (
"" +
this.showDetail +
(this.fileListUpload.length !== 0) +
this.currentFileId +
(this.currentFolderContent
? this.currentFolderContent.name + this.currentFolderContent.id
: "")
);
},
metadataTemplate(): Metadata {
if (this.resource && this.resource.applicationProfile) {
return {
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type": [
{ type: "uri", value: this.resource.applicationProfile },
],
};
} else {
return {};
}
},
saveButtonDisabled(): boolean {
return (
(this.resource && this.resource.archived) ||
!this.valid ||
this.isUploading ||
(this.currentFolderContent?.isFolder === false &&
((this.editableDataUrl && !this.currentFolderContent.dataUrl) ||
(this.editableKey && !this.currentFolderContent.name)))
);
},
shownFiles(): FolderContent[] {
let shownFiles: FolderContent[] = [];
if (this.showDetail) {
shownFiles = this.fileListEdit;
} else if (this.fileListUpload.length !== 0) {
shownFiles = this.fileListUpload;
}
return shownFiles;
},
},
data() {
return {
numberOfCurrentlyProcessedFiles: 0,
totalNumberOfCurrentlyProcessedFiles: 0,
uploadDuplicates: true,
currentFileId: -1,
uploadFileListNewFiles: [] as FileInformation[],
uploadFileListReplaceFiles: [] as FileInformation[],
fileListError: [] as FolderContent[],
progressStatus: 0,
valid: false,
validationResults: [] as ValidationResult[],
deleteFolderContentsModalVisible: false,
saveDuplicateFilesModalVisible: false,
};
},
props: {
showDetail: Boolean,
isUploading: Boolean,
fileListEdit: {
default() {
return [];
},
type: Array as PropType<FolderContent[]>,
},
fileListUpload: {
default() {
return [];
},
type: Array as PropType<FileInformation[]>,
},
folderContents: {
default() {
return [];
},
type: Array as PropType<FolderContent[]>,
},
currentFolder: {
default: "/",
type: String,
},
},
created() {
this.getOptions();
},
watch: {
fileListUpload() {
this.getOptions();
},
fileListEdit() {
this.getOptions();
},
},
methods: {
async receiveClass(className: string): Promise<BilingualLabels> {
return await this.resourceStore.getClass(className);
},
applyMetadataTemplate() {
const keyList = Object.keys(this.metadataTemplate);
if (keyList.length > 0) {
for (const key of keyList) {
const entry = this.metadataTemplate[key][0];
if (
typeof entry !== "string" &&
!Array.isArray(entry) &&
entry["value"] !== ""
) {
for (const currentFile of this.shownFiles) {
const innerKeyList = Object.keys(entry);
const file = currentFile;
if (file.metadata === undefined) {
file.metadata = {};
}
const entryList: Metadata[] = [];
entryList.push({});
for (const innerKey of innerKeyList) {
entryList[0][innerKey] = entry[innerKey];
}
file.metadata[key] = entryList;
}
}
}
}
},
isValid(valid: ValidationReport) {
this.valid = valid.conforms;
this.validationResults = valid.results;
},
loadAllFilesTab() {
this.currentFileId = -1;
},
changeMetadata(index: number) {
if (this.currentFileId === -1) {
this.applyMetadataTemplate();
}
this.currentFileId = index;
},
removeElement(index: number, count = 1) {
if (this.showDetail) {
this.$emit("removeSelection", index, count);
} else {
this.$emit("removeElement", index, count);
}
},
adjustRemainingFiles(file: FolderContent, error = false, reload = false) {
this.numberOfCurrentlyProcessedFiles -= 1;
this.progressStatus = 0;
if (!file.isFolder) {
file.uploading = false;
}
if (error) {
this.fileListError.push(file);
}
if (this.numberOfCurrentlyProcessedFiles === 0) {
this.uploadDuplicates = true;
this.$emit("isUploading", false);
this.cleanup();
if (reload) {
location.reload();
} else {
const numberOfSuccessfulUploadedFiles =
this.totalNumberOfCurrentlyProcessedFiles -
this.fileListError.length;
if (this.fileListError.length > 0) {
const h = this.$createElement;
const content = [];
content.push(
"" +
this.$parent.$parent.$t(
"page.resource.toastSavingFailedBodyTop"
)
);
content.push(h("br"));
for (const fileWithError of this.fileListError) {
content.push("" + fileWithError.name);
content.push(h("br"));
}
content.push(
"" +
this.$parent.$parent.$t(
"page.resource.toastSavingFailedBodyBottom"
)
);
const vNodesMsg = h("span", {}, content);
this.makeToast(
"" +
this.$parent.$parent.$t("page.resource.toastSavingFailedTitle"),
vNodesMsg,
true
);
}
if (numberOfSuccessfulUploadedFiles > 0) {
this.makeToast(
"" +
this.$parent.$parent.$t(
"page.resource.toastSavingSuccessfulTitle"
),
"" +
this.$parent.$parent.$t(
"page.resource.toastSavingSuccessfulBody"
) +
numberOfSuccessfulUploadedFiles
);
}
}
}
},
initRemainingFiles(numberOfFiles: number) {
this.fileListError = [];
this.numberOfCurrentlyProcessedFiles = numberOfFiles;
this.totalNumberOfCurrentlyProcessedFiles = numberOfFiles;
for (const currentFile of this.shownFiles) {
if (
(this.uploadDuplicates ||
this.folderContents.find((x) => x.name === currentFile.name) ===
undefined) &&
!currentFile.isFolder
) {
currentFile.uploading = true;
}
}
if (this.numberOfCurrentlyProcessedFiles > 0) {
this.$emit("isUploading", true);
}
},
async uploadPreparation() {
this.uploadFileListNewFiles = [];
this.uploadFileListReplaceFiles = [];
for (const fileToUpload of this.fileListUpload) {
if (
this.folderContents.find((x) => x.name === fileToUpload.name) ===
undefined
) {
this.uploadFileListNewFiles.push(fileToUpload);
} else {
this.uploadFileListReplaceFiles.push(fileToUpload);
}
}
if (this.uploadFileListReplaceFiles.length > 0) {
this.showModalSaveDuplicateFiles();
} else {
await this.upload();
}
},
async upload() {
if (this.currentFileId === -1) {
this.applyMetadataTemplate();
}
if (this.uploadDuplicates) {
this.initRemainingFiles(this.fileListUpload.length);
for (const fileToUpload of this.fileListUpload) {
await this.uploadFile(fileToUpload);
}
} else {
this.initRemainingFiles(this.uploadFileListNewFiles.length);
for (const fileToUpload of this.uploadFileListNewFiles) {
await this.uploadFile(fileToUpload);
}
}
},
async uploadFile(file: FileInformation) {
const result = await this.resourceStore.storeMetadata(
this.resource,
file.absolutePath,
this.removeEmptyValues(file.metadata)
);
if (result) {
if (!this.editableDataUrl && file.info) {
await this.handleUploadContent(file.info, file);
} else if (file.dataUrl !== undefined) {
this.fillData(file);
const blob = new Blob([file.dataUrl], { type: "plain/text" });
await this.handleUploadContent(blob, file);
}
} else {
this.adjustRemainingFiles(file, true);
}
},
fillData(file: FileInformation) {
if (!file.dataUrl) {
file.dataUrl = "";
}
file.info = new File([file.dataUrl], file.name, {
type: "text/plain",
});
},
async handleUploadContent(contents: Blob, file: FileInformation) {
this.progressStatus = 0;
const result = await this.resourceStore.storeFile(
this.resource,
file.absolutePath,
[contents],
{
onUploadProgress: (progressEvent: ProgressEvent) => {
this.progressStatus = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
},
}
);
if (!result) {
this.adjustRemainingFiles(file, true);
}
const entry = this.folderContents.find((x) => x.name === file.name);
if (entry === undefined) {
const newRow: FileInformation = {
id: uuidv4(),
isFolder: false,
name: file.name,
absolutePath: file.absolutePath,
lastModified: new Date().toString(),
created: new Date().toString(),
size: file.size,
path: file.path,
info: file.info,
metadata: {},
version: `${+new Date()}`,
};
MetadataManagerUtil.copyMetadata(file.metadata, newRow, false);
this.$emit("folderContents", [...this.folderContents, newRow]);
} else {
entry.lastModified = new Date().toString();
entry.created = new Date().toString();
if (!entry.isFolder) {
entry.size = file.size;
}
MetadataManagerUtil.copyMetadata(file.metadata, entry, false);
}
this.adjustRemainingFiles(file);
},
async download() {
if (this.showDetail) {
for (const editableFile of this.shownFiles) {
const response = await this.resourceStore.getFile(
this.resource,
editableFile.absolutePath,
true
);
if (response) {
fileSaver.saveAs(response, editableFile.name);
window.location.hash = window.location.hash.substring(
0,
window.location.hash.lastIndexOf("/")
);
}
}
}
},
selectFiles() {
this.$emit("clickFileSelect");
},
async update() {
if (this.currentFileId === -1) {
this.applyMetadataTemplate();
}
this.initRemainingFiles(this.fileListEdit.length);
for (const editableFile of this.fileListEdit) {
const result = await this.resourceStore.storeMetadata(
this.resource,
editableFile.absolutePath,
this.removeEmptyValues(editableFile.metadata)
);
if (result) {
const tmp = this.folderContents.find(
(x) => x.name === editableFile.name
);
if (
this.editableDataUrl &&
!editableFile.isFolder &&
editableFile.dataUrl !== undefined
) {
this.fillData(editableFile);
const blob = new Blob([editableFile.dataUrl], {
type: "plain/text",
});
await this.handleUploadContent(blob, editableFile);
} else {
MetadataManagerUtil.copyMetadata(editableFile.metadata, tmp, false);
this.adjustRemainingFiles(editableFile);
}
} else {
this.adjustRemainingFiles(editableFile, true);
}
}
},
removeEmptyValues(metadata: Metadata) {
const refinedMetadata = cloneDeep(metadata);
// remove empty nodes
for (const node in refinedMetadata) {
const property = refinedMetadata[node];
if (
property.length === 0 ||
typeof property[0] === "string" ||
Array.isArray(property[0]) ||
property[0]["value"] === undefined ||
property[0]["value"] === null ||
(typeof property[0]["value"] === "string" &&
property[0]["value"].trim() === "")
) {
delete refinedMetadata[node];
}
}
return refinedMetadata;
},
async deleteFolderContents() {
this.initRemainingFiles(this.fileListEdit.length);
for (const fileToDelete of this.fileListEdit) {
await this.deleteFolderContent(fileToDelete);
}
this.cleanup();
},
async deleteFolderContent(file: FolderContent) {
let failure = !(await this.resourceStore.deleteFile(
this.resource,
file.absolutePath
));
this.adjustRemainingFiles(file, failure, true);
},
async getOptions() {
if (this.shownFiles.length === 1) {
this.currentFileId = 0;
}
if (this.editableDataUrl) {
for (const file of this.shownFiles) {
const element = file;
if (!element.isFolder) {
if (element.requesting || element.path === "") {
return;
}
element.requesting = true;
const response = await this.resourceStore.getFile(
this.resource,
element.absolutePath
);
this.$set(element, "dataUrl", response);
element.requesting = false;
}
}
}
},
cleanup() {
this.$emit("emptyFileLists");
},
makeToast(
givenTitle = "Title",
text: string | VNode | VNode[] = "Message",
error = false
) {
if (error) {
this.$bvToast.toast(text, {
title: givenTitle,
noAutoHide: true,
variant: "warning",
toaster: "b-toaster-bottom-right",
});
} else {
this.$bvToast.toast(text, {
title: givenTitle,
toaster: "b-toaster-bottom-right",
noCloseButton: true,
});
}
},
showModalDeleteFolderContents() {
this.deleteFolderContentsModalVisible = true;
},
hideModalDeleteFolderContents() {
this.deleteFolderContentsModalVisible = false;
},
async deleteModalDeleteFolderContents() {
this.deleteFolderContentsModalVisible = false;
await this.deleteFolderContents();
},
showModalSaveDuplicateFiles() {
this.saveDuplicateFilesModalVisible = true;
},
hideModalSaveDuplicateFiles() {
this.saveDuplicateFilesModalVisible = false;
},
overwriteModalSaveDuplicateFiles() {
this.uploadDuplicates = true;
this.saveDuplicateFilesModalVisible = false;
this.upload();
},
skipModalSaveDuplicateFiles() {
this.uploadDuplicates = false;
this.saveDuplicateFilesModalVisible = false;
this.upload();
},
},
});
</script>
<style scoped>
.generatedForm {
margin-top: 10px;
}
#detail-view {
flex-direction: column;
display: flex;
max-height: 100%;
}
#metadataManagerMetadataSection {
overflow-x: hidden;
overflow-y: auto;
margin-bottom: 42px;
padding-right: 0px;
margin-right: 20px;
}
#metadataManagerMetadataSection >>> .generatedFormSpan {
width: 100%;
}
#metadataManagerMetadataSection >>> .generatedForm {
width: 100%;
}
#URLBtn {
min-width: 0px;
}
</style>
<template>
<span>
<coscine-form-group
:label="$parent.$parent.$parent.$t('page.resource.infoFileName')"
>
<div class="fileInfoField">
{{
!fileListEdit[currentFileId].name
? $parent.$parent.$parent.$t("page.resource.infoFileNoInformation")
: fileListEdit[currentFileId].name
}}
</div>
</coscine-form-group>
<coscine-form-group
:label="$parent.$parent.$parent.$t('page.resource.infoFileLastModified')"
>
<div class="fileInfoField">
{{
!fileListEdit[currentFileId].lastModified
? $parent.$parent.$parent.$t("page.resource.infoFileNoInformation")
: new Date(
fileListEdit[currentFileId].lastModified
).toLocaleDateString($i18n.locale)
}}
</div>
</coscine-form-group>
<coscine-form-group
:label="$parent.$parent.$parent.$t('page.resource.infoFileCreated')"
>
<div class="fileInfoField">
{{
!fileListEdit[currentFileId].created
? $parent.$parent.$parent.$t("page.resource.infoFileNoInformation")
: new Date(fileListEdit[currentFileId].created).toLocaleDateString(
$i18n.locale
)
}}
</div>
</coscine-form-group>
<coscine-form-group
:label="$parent.$parent.$parent.$t('page.resource.infoFileSize')"
>
<div class="fileInfoField">
{{
fileListEdit[currentFileId].size === undefined ||
fileListEdit[currentFileId].size === null
? $parent.$parent.$parent.$t("page.resource.infoFileNoInformation")
: fileListEdit[currentFileId].size + " Bytes"
}}
</div>
</coscine-form-group>
</span>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue-demi";
import type { FolderContent } from "../../utils/EntryDefinition";
export default defineComponent({
props: {
currentFileId: {
required: true,
type: Number,
},
fileListEdit: {
required: true,
type: Array as PropType<FolderContent[]>,
},
},
});
</script>
<style scoped>
.fileInfoField {
padding-top: calc(0.375rem + 1px);
padding-bottom: calc(0.375rem + 1px);
}
</style>
<template>
<b-row id="metadataManagerButtonRowBottom">
<span class="processStatement" v-if="isUploading"
>{{
totalNumberOfCurrentlyProcessedFiles - numberOfCurrentlyProcessedFiles
}}/{{ totalNumberOfCurrentlyProcessedFiles }}</span
>
<div
id="metadataManagerButtonRowBottomContainer"
:class="saveButtonDisabled ? 'buttondisabled' : ''"
>
<b-col>
<b-progress
v-if="isUploading"
class="metadataManagerButtonRowProgress"
:value="progressStatus"
max="100"
show-progress
animated
></b-progress>
</b-col>
<b-col id="metadataManagerButtonCol">
<b-button
id="metadataManagerButtonRowBottomSave"
class="metadataManagerButtonRowBottomSave"
variant="primary"
@click="
isUploading
? undefined
: !showDetail
? $emit('uploadPreparation')
: $emit('update')
"
:disabled="saveButtonDisabled"
><b-spinner label="Spinning" v-show="isUploading"></b-spinner
>{{
isUploading
? $parent.$parent.$parent.$t(
"page.resource.metadataManagerBtnSaving"
)
: !showDetail
? $parent.$parent.$parent.$t(
"page.resource.metadataManagerBtnUpload"
)
: $parent.$parent.$parent.$t(
"page.resource.metadataManagerBtnUpdate"
)
}}</b-button
>
</b-col>
</div>
</b-row>
</template>
<script lang="ts">
import { defineComponent } from "vue-demi";
export default defineComponent({
props: {
isUploading: {
required: true,
type: Boolean,
},
numberOfCurrentlyProcessedFiles: {
required: true,
type: Number,
},
progressStatus: {
required: true,
type: Number,
},
showDetail: {
required: true,
type: Boolean,
},
saveButtonDisabled: {
required: true,
type: Boolean,
},
totalNumberOfCurrentlyProcessedFiles: {
required: true,
type: Number,
},
},
});
</script>
<style scoped>
#metadataManagerButtonRowBottom {
position: absolute;
bottom: 45px;
width: 100%;
}
#metadataManagerButtonRowBottom >>> #metadataManagerButtonRowBottomContainer {
display: inline-flex;
position: absolute;
right: 0px;
width: auto;
height: 33px;
}
#metadataManagerButtonRowBottom
>>> #metadataManagerButtonRowBottomContainer.buttondisabled {
cursor: not-allowed;
}
#metadataManagerButtonRowBottom
>>> .metadataManagerButtonRowBottomSave.disabled {
pointer-events: none;
}
#metadataManagerButtonRowBottom >>> .metadataManagerButtonRowProgress {
width: 350px;
margin-top: 13px;
}
#metadataManagerButtonRowBottom >>> .processStatement {
position: absolute;
top: 13px;
left: 20px;
}
button >>> span.spinner-border {
height: 15px;
width: 15px;
margin-right: 4px;
}
</style>
<template>
<b-row id="metadataManagerButtonRowTop" align-h="between">
<b-col>
<b-button
id="buttonSelectFiles"
variant="secondary"
@click="$emit('selectFiles')"
:placeholder="
$parent.$parent.$parent.$t(
'page.resource.metadataManagerBtnSelectFiles'
)
"
:disabled="isUploading || readOnly || (resource && resource.archived)"
autofocus
>{{
$parent.$parent.$parent.$t(
"page.resource.metadataManagerBtnSelectFiles"
)
}}
</b-button>
</b-col>
<b-col>
<b-input-group id="metadataManagerDropDownMenu" class="float-right">
<b-dropdown
size="sm"
right
id="addColumnDropDown"
:disabled="
!showDetail ||
readOnly ||
(resource && resource.archived) ||
shownFiles.length === 0
"
>
<b-dropdown-item
v-if="showDetail"
@click="$emit('showModalDeleteFolderContents')"
>{{ $t("buttons.delete") }}</b-dropdown-item
>
</b-dropdown>
<b-button
v-if="!editableDataUrl"
@click="$emit('download')"
:disabled="!showDetail || shownFiles.length === 0"
>{{
$parent.$parent.$parent.$t(
"page.resource.metadataManagerBtnDownload"
)
}}</b-button
>
</b-input-group>
</b-col>
</b-row>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue-demi";
import type { ResourceObject } from "@coscine/api-client/dist/types/Coscine.Api.Resources";
import type { FolderContent } from "../../utils/EntryDefinition";
export default defineComponent({
props: {
editableDataUrl: {
required: true,
type: Boolean,
},
isUploading: {
required: true,
type: Boolean,
},
readOnly: {
required: true,
type: Boolean,
},
resource: {
required: true,
type: Object as PropType<ResourceObject>,
},
showDetail: {
required: true,
type: Boolean,
},
shownFiles: {
required: true,
type: Array as PropType<FolderContent[]>,
},
},
});
</script>
<style scoped>
#metadataManagerButtonRowTop {
margin-bottom: 5px;
}
#metadataManagerDropDownMenu {
margin-right: 5px;
width: auto;
}
</style>
<template>
<div>
<span v-if="editableDataUrl">
<coscine-form-group
:mandatory="true"
labelFor="dataUrl"
:label="$parent.$parent.$parent.$t('page.resource.dataUrl')"
>
<b-input-group>
<b-form-input
id="dataUrl"
@input="updateDataUrl(currentFolderContent, $event)"
:value="currentFolderContent.dataUrl"
:placeholder="$parent.$parent.$parent.$t('page.resource.dataUrl')"
:disabled="resource.archived || readOnly"
/>
<b-button id="URLBtn" @click="openURL" :disabled="!isValidUrl"
><b-icon-link-45deg />
</b-button>
</b-input-group>
</coscine-form-group>
</span>
<span v-if="editableKey">
<coscine-form-group
:mandatory="true"
labelFor="metadataKey"
:label="$parent.$parent.$parent.$t('page.resource.metadataKey')"
>
<b-form-input
@change="updateAbsolutePath(currentFolderContent, $event)"
:value="currentFolderContent.name"
:placeholder="$parent.$parent.$parent.$t('page.resource.metadataKey')"
:disabled="resource.archived || readOnly"
/>
</coscine-form-group>
</span>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue-demi";
import type {
FileInformation,
FolderContent,
} from "../../utils/EntryDefinition";
import type { ResourceObject } from "@coscine/api-client/dist/types/Coscine.Api.Resources";
export default defineComponent({
computed: {
isValidUrl(): boolean {
if (
!this.currentFolderContent ||
(this.currentFolderContent && this.currentFolderContent.isFolder)
) {
return false;
}
const dataUrl = this.currentFolderContent.dataUrl;
if (dataUrl === undefined) {
return false;
}
let url: URL;
const validUrl =
/^(http[s]?:\/\/){0,1}(www\.){0,1}[a-zA-Z0-9.-]+\.[a-zA-Z]{2,5}[.]{0,1}/;
try {
if (dataUrl.includes("://")) {
url = new URL(dataUrl);
} else {
url = new URL("http://" + dataUrl);
}
} catch {
return false;
}
return validUrl.test(url.toString());
},
},
props: {
currentFolderContent: {
required: true,
type: [Object] as PropType<FolderContent>,
},
editableDataUrl: {
required: true,
type: Boolean,
},
editableKey: {
required: true,
type: Boolean,
},
readOnly: {
required: true,
type: Boolean,
},
resource: {
required: true,
type: [Object] as PropType<ResourceObject>,
},
},
methods: {
openURL() {
if (
this.editableDataUrl &&
this.currentFolderContent &&
!this.currentFolderContent.isFolder
) {
let url = this.currentFolderContent.dataUrl;
if (url !== undefined && !url.includes("://")) {
url = "http://" + url;
}
const win = window.open(url, "_blank");
if (win !== null) {
win.focus();
}
}
},
updateDataUrl(entry: FileInformation, value: string) {
entry.dataUrl = value;
entry.size = value.length;
},
updateAbsolutePath(entry: FolderContent, value: string) {
entry.name = value;
entry.path = entry.path !== "" ? entry.path : "/";
entry.absolutePath = entry.path + entry.name;
},
},
});
</script>
<template>
<div>
<b-row
id="metadataManagerShownFilesTable"
v-show="showDetail || !editableDataUrl || !editableKey"
>
<b-col>
<b-input-group v-for="(item, index) in shownFiles" :key="index">
<b-button
@click="$emit('changeMetadata', index)"
variant="outline-secondary"
:pressed="currentFileId === index"
>{{ index + 1 }}</b-button
>
<b-button
@click="$emit('changeMetadata', index)"
class="metadataManagerFileListFileName"
:pressed="false"
variant="outline-secondary"
>{{ item.name
}}<b-spinner v-show="item.uploading" label="Spinning"></b-spinner
></b-button>
<b-button
class="deleteFolderContentFromList"
@click="$emit('removeElement', index)"
variant="outline-secondary"
:disabled="item.uploading"
>X</b-button
>
</b-input-group>
</b-col>
</b-row>
<b-row id="metadataManagerQuickAccess" v-show="shownFiles.length > 1">
<b-col>
<b-button
@click="$emit('loadAllFilesTab')"
:pressed="currentFileId === -1"
variant="outline-secondary"
>{{ $parent.$parent.$t("page.resource.allFiles") }}</b-button
>
<span v-if="shownFiles.length > 1 && shownFiles.length <= 10">
<b-button
v-for="(item, index) in shownFiles"
:key="index"
:pressed="currentFileId === index"
@click="$emit('changeMetadata', index)"
variant="outline-secondary"
class="metadataTabButton"
>{{ index + 1 }}</b-button
>
</span>
</b-col>
</b-row>
<b-row id="metadataManagerFileName" class="text-center">
<b-col>
<span
class="showSpaces"
v-if="
currentFileId >= 0 &&
currentFileId < shownFiles.length &&
currentFolderContent
"
>
{{ currentFolderContent.name }}
</span>
<span v-else-if="shownFiles.length > 0">{{
$parent.$parent.$t("page.resource.allFiles")
}}</span>
</b-col>
</b-row>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue-demi";
import type { FolderContent } from "../../utils/EntryDefinition";
export default defineComponent({
props: {
currentFileId: {
required: true,
type: Number,
},
currentFolderContent: {
type: [Object, undefined] as PropType<FolderContent | undefined>,
},
editableDataUrl: {
required: true,
type: Boolean,
},
editableKey: {
required: true,
type: Boolean,
},
showDetail: {
required: true,
type: Boolean,
},
shownFiles: {
required: true,
type: Array as PropType<FolderContent[]>,
},
},
});
</script>
<style scoped>
.metadataManagerFileListFileName {
text-overflow: ellipsis;
}
#metadataManagerFileName {
margin-top: 5px;
left: 0;
right: 0;
}
#metadataManagerFileName >>> span {
text-align: center;
font-weight: bold;
width: 100%;
}
#metadataManagerShownFilesTable {
margin-right: -10px;
}
#metadataManagerShownFilesTable >>> .col {
overflow-x: hidden;
overflow-y: auto;
max-height: 150px;
padding-right: 0px;
margin-right: 15px;
}
#metadataManagerShownFilesTable >>> .input-group {
flex-wrap: initial;
}
#metadataManagerShownFilesTable >>> .metadataManagerFileListFileName {
width: inherit;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
text-transform: none;
}
#metadataManagerShownFilesTable >>> .spinner-border {
margin-left: 5px;
}
#metadataManagerShownFilesTable >>> .deleteFolderContentFromList {
color: red;
}
#metadataManagerQuickAccess {
left: 0;
right: 0;
}
#metadataManagerQuickAccess >>> .btn {
margin-top: 5px;
margin-right: 5px;
}
.metadataTabButton {
max-width: inherit;
min-width: inherit;
width: inherit;
}
.showSpaces {
white-space: pre;
}
button >>> span.spinner-border {
height: 15px;
width: 15px;
margin-right: 4px;
}
</style>
<template>
<b-modal
id="modalDeleteFolderContents"
:visible="visible"
size="lg"
hide-footer
:title="
$parent.$parent.$parent.$t(
'page.resource.modalDeleteFolderContentsHeader'
)
"
@hidden="$emit('close', $event.target.value)"
@ok="$emit('ok', $event.target.value)"
@cancel="$emit('close', $event.target.value)"
@shown="$emit('shown', $event.target.value)"
>
<div>
{{
$parent.$parent.$parent.$t(
"page.resource.modalDeleteFolderContentsBody"
)
}}
</div>
<br />
<div
v-for="(item, index) in shownFiles"
:key="index"
class="font-weight-bold"
>
{{ item.name }}
</div>
<br />
<b-button
class="float-left"
@click="$emit('close', $event.target.value)"
autofocus
>{{ $t("buttons.cancel") }}</b-button
>
<b-button
class="float-right"
variant="danger"
@click="$emit('ok', $event.target.value)"
>{{ $t("buttons.delete") }}</b-button
>
</b-modal>
</template>
<script lang="ts">
import { defineComponent } from "vue-demi";
export default defineComponent({
props: {
shownFiles: {
required: true,
type: Array,
},
visible: {
default: false,
type: Boolean,
},
},
});
</script>
<template>
<b-modal
id="modalSaveDuplicateFiles"
:visible="visible"
size="lg"
hide-footer
:title="
$parent.$parent.$parent.$t('page.resource.modalSaveDuplicateFilesHeader')
"
@hidden="$emit('close', $event.target.value)"
@ok="$emit('ok', $event.target.value)"
@cancel="$emit('close', $event.target.value)"
@shown="$emit('shown', $event.target.value)"
>
<div>
{{
$parent.$parent.$parent.$t("page.resource.modalSaveDuplicateFilesBody")
}}
</div>
<br />
<div
v-for="(item, index) in uploadFileListReplaceFiles"
:key="index"
class="font-weight-bold"
>
{{ item.name }}
</div>
<br />
<div>
<b-button
class="float-left"
@click="$emit('close', $event.target.value)"
autofocus
>{{
$parent.$parent.$parent.$t(
"page.resource.modalSaveDuplicateFilesBtnCancel"
)
}}</b-button
>
<b-button
class="modalButtonFloatCenter"
@click="$emit('skip', $event.target.value)"
>{{
$parent.$parent.$parent.$t(
"page.resource.modalSaveDuplicateFilesBtnSkip"
)
}}</b-button
>
<b-button
class="float-right"
style="justify-content: flex-end"
@click="$emit('ok', $event.target.value)"
>{{
$parent.$parent.$parent.$t(
"page.resource.modalSaveDuplicateFilesBtnOverwrite"
)
}}</b-button
>
</div>
</b-modal>
</template>
<script lang="ts">
import { defineComponent } from "vue-demi";
export default defineComponent({
props: {
uploadFileListReplaceFiles: {
required: true,
type: Array,
},
visible: {
default: false,
type: Boolean,
},
},
});
</script>
<style scoped>
.modalButtonFloatCenter {
position: absolute;
left: 36%;
right: 33%;
white-space: nowrap;
}
</style>
<template>
<b-popover
v-if="valid || validationResults.length === 0 ? false : true"
:disabled="valid || validationResults.length === 0"
over
target="metadataManagerButtonCol"
triggers="hover focus"
placement="top"
>
<template v-slot:title
><b>{{
$parent.$parent.$parent.$t("page.resource.validationErrors")
}}</b></template
>
<div>
<ul>
<li
v-for="validationResult of validationResults"
:key="validationResult.term.value"
>
{{ validationResult.path.value }}
<ul>
<li
v-for="singleMessage of validationResult.message"
:key="singleMessage.value"
>
{{ singleMessage.value }}
</li>
</ul>
</li>
</ul>
</div>
</b-popover>
</template>
<script lang="ts">
import type { ValidationResult } from "rdf-validate-shacl/src/validation-report";
import { defineComponent, PropType } from "vue-demi";
export default defineComponent({
props: {
valid: {
required: true,
type: Boolean,
},
validationResults: {
required: true,
type: Array as PropType<ValidationResult[]>,
},
},
});
</script>
......@@ -13,12 +13,174 @@ export default {
"Das ist die @:page.createResource.title des Coscine UIv2 Apps",
},
resource: {
title: "Resourceseite",
description: "Das ist die @:page.resource.title des Coscine UIv2 Apps",
resources: "Ressourcen",
upload: "Dateien hochladen",
download: "Herunterladen",
edit: "Ressourcenkonfiguration bearbeiten",
info: "Informationen",
canDropFile: "Sie können Ihre Datei hier ablegen!",
loading: "Laden...",
typeToSearch: "Suchen...",
clear: "Leeren",
perPage: "Pro Seite",
more: "Mehr",
fileName: "Name",
lastModified: "Geändert",
actions: "Aktionen",
resourceName: "Ressourcenname",
displayName: "Anzeigename",
description: "Ressourcenbeschreibung",
disciplines: "Disziplin",
keywords: "Ressourcenschlagwörter",
visibility: "Sichtbarkeit",
license: "Lizenz",
usageRights: "Verwendungsrechte",
PID: "Persistente ID",
occupiedBytes: "Belegter Speicher",
noData: "Diese Ressource enthält keine Daten",
emptyTableText: "Diese Ressource enthält keine Dateien.",
emptyFilterText:
"Keine Dateien gefunden die mit Ihrer Anfrage übereinstimmen.",
metadataHeadline: "Metadaten:",
metadataManagerTitleUpload: "Daten hochladen:",
metadataManagerTitleEdit: "Editieren:",
metadataManagerSelectLabel: "Einträge auswählen:",
metadataManagerBadFileName:
"Ungültiger Dateiname. Folgende Zeichen sind nicht zulässig: /:?*<>|",
metadataManagerBtnUpload: "Hochladen",
metadataManagerBtnSelectFiles: "Dateien auswählen",
metadataManagerBtnDownload: "Herunterladen",
metadataManagerBtnReuse: "Wiederverwenden",
metadataManagerBtnMove: "Verschieben",
metadataManagerBtnPublish: "Veröffentlichen",
metadataManagerBtnArchive: "Archivieren",
metadataManagerBtnUpdate: "Aktualisieren",
metadataManagerBtnSaving: "Speichern...",
infoFileType: "Dateiart",
infoFileTypeFolder: "Ordner",
infoFileTypeFile: "Dateien",
infoFileName: "Dateiname",
infoFileAbsolutePath: "Dateipfad",
infoFileLastModified: "Zuletzt geändert",
infoFileCreated: "Erstellt",
infoFileSize: "Dateigröße",
infoFileContent: "Inhalt",
infoFileFiles: "Dateien",
infoFileNoInformation: "Keine Information",
tableIsEmpty:
"Es existieren aktuell noch keine Einträge für diese Ressource.",
metadataManager: "Metadaten Manager",
allFiles: "Alle Dateien",
validationErrors: "Validierungsfehler",
modalSaveDuplicateFilesHeader: "Dateien ersetzen oder überspringen",
modalSaveDuplicateFilesBody:
"Es sind bereits Dateien mit dem gleichen Namen vorhanden. Diese Dateien werden überschrieben:",
modalSaveDuplicateFilesBtnCancel: "HOCHLADEN ABBRECHEN",
modalSaveDuplicateFilesBtnSkip: "DIESE DATEIEN ÜBERSPRINGEN",
modalSaveDuplicateFilesBtnOverwrite: "ÜBERSCHREIBEN",
modalDeleteFolderContentsHeader: "Löschen von Dateien und Metadaten",
modalDeleteFolderContentsBody:
"Sind Sie sicher, dass Sie die folgenen Dateien und Metadaten löschen wollen:",
modalLeavingPageHeader: "Dateien werden hochgeladen",
modalLeavingPageBodyTop: "Diese Dateien werden momentan hochgeladen:",
modalLeavingPageBodyBottom:
"Sind Sie sicher, dass Sie den den Prozess abbrechen wollen?",
modalLeavingPageBtnStay: "AUF SEITE BLEIBEN",
modalLeavingPageBtnLeave: "SEITE VERLASSEN",
toastSavingSuccessfulTitle: "Speichern erfolgreich",
toastSavingSuccessfulBody: "Zahl der erfolgreich gespeicherten Dateien: ",
toastSavingFailedTitle: "Speichern fehlgeschlagen",
toastSavingFailedBodyTop:
"Es ist ein Fehler beim Speichern der folgenden Dateien aufgetreten:",
toastSavingFailedBodyBottom: "Bitte versuchen Sie es erneut.",
dataUrl: "Daten URL",
metadataKey: "Eintragsname",
size: "Dateigröße",
},
settings: {
title: "Settings-Seite",
description: "Das ist die @:page.settings.title des Coscine UIv2 Apps",
},
},
resourceType: {
linked: {
page: {
resource: {
upload: "Einträge speichern",
download: "Öffnen",
noData: "Diese Ressource enthält keine Einträge",
emptyTableText: "Diese Ressource enthält keine Einträge.",
emptyFilterText:
"Keine Einträge gefunden die mit Ihrer Anfrage übereinstimmen.",
metadataManagerTitleUpload: "Einträge speichern:",
metadataManagerSelectLabel: "Einträge auswählen:",
metadataManagerBadFileName:
"Ungültiger Eintrag. Folgende Zeichen sind nicht zulässig: /:?*<>|",
metadataManagerBtnDownload: "Öffnen",
metadataManagerBtnUpload: "Speichern",
metadataManagerBtnSelectFiles: "Neuer Eintrag",
infoFileType: "Eintrag",
infoFileTypeFolder: "Ordner",
infoFileTypeFile: "Einträge",
infoFileName: "Eintragname",
infoFileAbsolutePath: "Eintragpfad",
infoFileSize: "Eintraggröße",
infoFileFiles: "Einträge",
tableIsEmpty:
"Es existieren aktuell noch keine Einträge für diese Ressource.",
allFiles: "Alle Einträge",
modalSaveDuplicateFilesHeader: "Einträge ersetzen oder überspringen",
modalSaveDuplicateFilesBody:
"Es sind bereits Einträge mit dem gleichen Namen vorhanden. Diese Einträge werden überschrieben:",
modalSaveDuplicateFilesBtnCancel: "SPEICHERN ABBRECHEN",
modalSaveDuplicateFilesBtnSkip: "DIESE EINTRÄGE ÜBERSPRINGEN",
modalDeleteFolderContentsHeader:
"Löschen von Einträgen und Metadaten",
modalDeleteFolderContentsBody:
"Sind Sie sicher, dass Sie die folgenen Einträge und Metadaten löschen wollen:",
modalLeavingPageHeader: "Einträge werden hochgeladen",
modalLeavingPageBodyTop:
"Diese Einträge werden momentan hochgeladen:",
toastSavingSuccessfulTitle: "Speichern erfolgreich",
toastSavingSuccessfulBody:
"Zahl der erfolgreich gespeicherten Einträge: ",
toastSavingFailedTitle: "Speichern fehlgeschlagen",
toastSavingFailedBodyTop:
"Es ist ein Fehler beim Speichern der folgenden Einträge aufgetreten:",
},
},
},
} as VueI18n.LocaleMessageObject,
} as VueI18n.LocaleMessageObject;
......@@ -13,12 +13,167 @@ export default {
"This is the @:page.createResource.title for the Coscine UIv2 App",
},
resource: {
title: "Resource Page",
description: "This is the @:page.resource.title for the Coscine UIv2 App",
resources: "Resources",
upload: "Upload Files",
download: "Download",
edit: "Edit Resource Configuration",
info: "Information",
canDropFile: "You can drop your file here!",
loading: "Loading...",
typeToSearch: "Type to search",
clear: "Clear",
perPage: "Per page",
more: "More",
fileName: "Name",
lastModified: "Modified",
actions: "Actions",
resourceName: "Resource Name",
displayName: "Display Name",
description: "Resource Description",
disciplines: "Discipline",
keywords: "Resource Keywords",
visibility: "Visibility",
license: "License",
usageRights: "Usage Rights",
PID: "Persistent ID",
occupiedBytes: "Occupied Storage Space",
noData: "This resource contains no data.",
emptyTableText: "This resource contains no files.",
emptyFilterText: "No files found matching your request.",
metadataHeadline: "Metadata:",
metadataManagerTitleUpload: "Upload Data:",
metadataManagerTitleEdit: "Edit:",
metadataManagerSelectLabel: "Selected Entry(s):",
metadataManagerBadFileName:
"Invalid file name. The following characters are not permissible: /:?*<>|",
metadataManagerBtnUpload: "Upload",
metadataManagerBtnSelectFiles: "Select Files",
metadataManagerBtnDownload: "Download",
metadataManagerBtnReuse: "Reuse",
metadataManagerBtnMove: "Move",
metadataManagerBtnPublish: "Publish",
metadataManagerBtnArchive: "Archive",
metadataManagerBtnUpdate: "Update",
metadataManagerBtnSaving: "Saving...",
infoFileType: "File Type",
infoFileTypeFolder: "Folder",
infoFileTypeFile: "File",
infoFileName: "File name",
infoFileAbsolutePath: "File Path",
infoFileLastModified: "Last modified",
infoFileCreated: "Created",
infoFileSize: "File Size",
infoFileContent: "Content",
infoFileFiles: "Files",
infoFileNoInformation: "No Information",
tableIsEmpty: "There are currently no entities for this resource.",
metadataManager: "Metadata Manager",
allFiles: "All Files",
validationErrors: "Validation Errors",
modalSaveDuplicateFilesHeader: "Replace or skip duplicate files",
modalSaveDuplicateFilesBody:
"One or more files with the same name already exist. Uploading will result in overwriting the following file(s):",
modalSaveDuplicateFilesBtnCancel: "CANCEL UPLOAD",
modalSaveDuplicateFilesBtnSkip: "SKIP DUPLICATE FILES",
modalSaveDuplicateFilesBtnOverwrite: "OVERWRITE",
modalDeleteFolderContentsHeader: "Delete files and metadata",
modalDeleteFolderContentsBody:
"Are you sure, you want to delete the following files and metadata:",
modalLeavingPageHeader: "Upload in progress",
modalLeavingPageBodyTop: "These files are currently uploading:",
modalLeavingPageBodyBottom:
"Are you sure you want to cancel the process?",
modalLeavingPageBtnStay: "STAY ON PAGE",
modalLeavingPageBtnLeave: "LEAVE CURRENT PAGE",
toastSavingSuccessfulTitle: "Saving file(s) successful",
toastSavingSuccessfulBody: "Number of files saved: ",
toastSavingFailedTitle: "Saving file(s) failed",
toastSavingFailedBodyTop:
"An error occured while saving the following files:",
toastSavingFailedBodyBottom: "Please try again.",
dataUrl: "Data URL",
metadataKey: "Entry Name",
size: "File Size",
},
settings: {
title: "Settings Page",
description: "This is the @:page.settings.title for the Coscine UIv2 App",
},
},
resourceType: {
linked: {
page: {
resource: {
upload: "Save Entries",
download: "Open",
noData: "This resource contains no entries.",
emptyTableText: "This resource contains no entries.",
emptyFilterText: "No entries found matching your request.",
metadataManagerTitleUpload: "Save Entry:",
metadataManagerSelectLabel: "Selected Entry(s):",
metadataManagerBadFileName:
"Invalid entry name. The following characters are not permissible: /:?*<>|",
metadataManagerBtnDownload: "Open",
metadataManagerBtnUpload: "Save",
metadataManagerBtnSelectFiles: "New Entry",
infoFileType: "Entry Type",
infoFileTypeFolder: "Folder",
infoFileTypeFile: "Entry",
infoFileName: "Entry name",
infoFileAbsolutePath: "Entry Path",
infoFileSize: "Entry Size",
infoFileFiles: "Entries",
tableIsEmpty: "There are currently no entries for this resource.",
allFiles: "All Entries",
modalSaveDuplicateFilesHeader: "Replace or skip duplicate entries",
modalSaveDuplicateFilesBody:
"One or more entries with the same name already exist. Saving will result in overwriting the following entry(s):",
modalSaveDuplicateFilesBtnCancel: "CANCEL SAVING",
modalSaveDuplicateFilesBtnSkip: "SKIP DUPLICATE ENTRIES",
modalDeleteFolderContentsHeader: "Delete entries and metadata",
modalDeleteFolderContentsBody:
"Are you sure, you want to delete the following entries and metadata:",
modalLeavingPageHeader: "Saving in progress",
modalLeavingPageBodyTop: "These entries are currently saving:",
toastSavingSuccessfulTitle: "Saving entries(s) successful",
toastSavingSuccessfulBody: "Number of entries saved: ",
toastSavingFailedTitle: "Saving entry(s) failed",
toastSavingFailedBodyTop:
"An error occured while saving the following entries:",
},
},
},
} as VueI18n.LocaleMessageObject,
} as VueI18n.LocaleMessageObject;
<template>
<div>
<section
class="container flex flex-col items-center px-5 py-12 mx-auto text-gray-600 body-font md:flex-row"
<div
id="resourcecontentview"
@dragenter="dragEnter"
@dragleave="dragLeave"
@dragend="dragLeave"
@dragover.prevent=""
@drop.prevent="uploadDrop"
>
<div>
<CoscineHeadline :headline="$parent.$t('page.resource.title')" />
<p class="mb-8 leading-relaxed dark:text-white">
{{ $parent.$t("page.resource.description") }}
</p>
<img
alt="From Coscine Old"
src="@/assets/images/Project-Id-R-Guid.png"
<div class="droppable" v-if="showDroppable && fileAddable">
<p class="droppableText">{{ $parent.$t("page.resource.canDropFile") }}</p>
</div>
<coscine-headline
v-show="!isFullscreen"
:headline="$parent.$t('page.resource.resources')"
/>
<b-form-file
ref="fileTrigger"
multiple
@input="fileListUploadSelected"
class="mt-3"
plain
></b-form-file>
<span id="filesViewSpan">
<div id="filesViewCard" :class="isFullscreen == false ? 'card' : ''">
<div :class="isFullscreen == false ? 'card-body' : ''">
<FilesView
ref="filesView"
:folderContents="folderContents"
:currentFolder="currentFolder"
:isUploading="isUploading"
:fileListEdit="fileListEdit"
@showDetail="setShowDetail"
@currentFolder="setCurrentFolder"
@folderContents="setFolder"
@fileListEdit="setFileListEdit"
@clickFileSelect="clickFileSelect"
@waitingForResponse="setWaitingForResponse"
/>
</div>
</div>
</span>
<div
id="metadataManagerDiv"
:class="isMetadataManagerHidden == true ? 'hiddenMetadataManager' : ''"
>
<b-button
v-show="isFullscreen"
squared
id="metadataManagerToggleFullscreen"
@click="toggleMenu()"
><span>{{
$parent.$t("page.resource.metadataManager")
}}</span></b-button
>
<div class="card">
<div class="card-body">
<MetadataManager
:showDetail="showDetail"
:fileListEdit="fileListEdit"
:fileListUpload="fileListUpload"
:folderContents="folderContents"
:currentFolder="currentFolder"
:isUploading="isUploading"
@emptyFileLists="emptyFileLists"
@folderContents="setFolder"
@removeElement="removeElement"
@removeSelection="removeSelection"
@isUploading="setIsUploading"
@clickFileSelect="clickFileSelect"
/>
<div
id="toggleFullscreenButton"
:class="
isMetadataManagerHidden == true ? '' : 'hiddenMetadataManager'
"
>
<button
v-if="isFullscreen"
class="btn btn-secondary"
type="button"
@click="toggleFullscreen"
>
<b-icon icon="fullscreen-exit" />
</button>
<button
v-else
type="button"
class="btn btn-secondary"
@click="toggleFullscreen"
>
<b-icon icon="fullscreen" />
</button>
</div>
</div>
</section>
</div>
</div>
<LoadingSpinner :isWaitingForResponse="isWaitingForResponse" />
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue-demi";
import CoscineHeadline from "@/components/CoscineHeadline.vue";
import type Vue from "vue";
// import the store for current module
import { useResourceStore } from "../store";
......@@ -27,6 +109,24 @@ import { useProjectStore } from "@/modules/project/store";
// import the main store
import { useMainStore } from "@/store/index";
import FilesView from "../components/FilesView.vue";
import MetadataManager from "../components/MetadataManager.vue";
import type {
FileInformation,
FolderContent,
FolderInformation,
} from "../utils/EntryDefinition";
import { v4 as uuidv4 } from "uuid";
import type { ProjectObject } from "@coscine/api-client/dist/types/Coscine.Api.Project";
import type {
ResourceObject,
ResourceTypeInformation,
} from "@coscine/api-client/dist/types/Coscine.Api.Resources";
import type { BFormFile, BTable } from "bootstrap-vue";
export default defineComponent({
setup() {
const mainStore = useMainStore();
......@@ -36,8 +136,381 @@ export default defineComponent({
return { mainStore, resourceStore, projectStore };
},
computed: {
project(): null | ProjectObject {
return this.projectStore.currentProject;
},
resource(): null | ResourceObject {
return this.resourceStore.currentResource;
},
resourceTypeInformation(): null | undefined | ResourceTypeInformation {
if (this.resourceStore.resourceTypes && this.resource) {
return this.resourceStore.resourceTypes.find(
(resourceType) => resourceType.id === this.resource?.type?.id
);
}
return null;
},
fileAddable(): boolean {
return (
!this.resourceTypeInformation?.resourceContent?.readOnly &&
!this.resourceTypeInformation?.resourceContent?.metadataView
?.editableDataUrl
);
},
showDroppable(): boolean {
return this.dragCounter > 0;
},
},
data() {
return {
isWaitingForResponse: false,
fileListEdit: [] as FolderContent[],
showDetail: false,
fileListUpload: [] as FileInformation[],
folderContents: [] as FolderContent[],
currentFolder:
window.location.hash.indexOf("#") !== -1
? window.location.hash.substring(1)
: "/",
dragCounter: 0,
isFullscreen: false,
isMetadataManagerHidden: false,
isUploading: false,
};
},
watch: {
resourceTypeInformation() {
this.emptyFileLists();
},
},
mounted() {
this.$nextTick(() => {
window.addEventListener("resize", this.getWindowWidth);
this.getWindowWidth();
});
this.emptyFileLists();
},
methods: {
clickFileSelect() {
this.showDetail = false;
if (
this.resourceTypeInformation?.resourceContent?.metadataView
?.editableDataUrl
) {
this.emptyFileLists();
} else {
(this.$refs.fileTrigger as BFormFile).$el.dispatchEvent(
new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true,
})
);
}
},
dragEnter() {
this.dragCounter++;
},
dragLeave() {
this.dragCounter--;
},
emptyFileLists() {
this.removeSelection(0, this.fileListEdit.length);
this.fileListEdit.length = 0;
this.fileListUpload.length = 0;
this.showDetail = false;
this.initializeForResourceType();
},
fileListUploadSelected(selectedFiles: File[] | File) {
if (!Array.isArray(selectedFiles)) {
selectedFiles = [selectedFiles];
}
this.emptyFileLists();
for (const file of selectedFiles) {
this.fileListUpload.push({
id: uuidv4(),
path: this.currentFolder,
version: `${+new Date()}`,
uploading: false,
info: file,
name: file.name,
size: file.size,
lastModified: "" + file.lastModified,
metadata: {},
isFolder: false,
absolutePath: this.currentFolder + file.name,
});
}
(
(this.$refs.filesView as Vue).$refs.adaptTable as BTable
).clearSelected();
this.showDetail = false;
},
initializeForResourceType() {
if (
this.resourceTypeInformation?.resourceContent?.metadataView
?.editableDataUrl
) {
this.fileListUpload.push({
id: uuidv4(),
path: "",
version: `${+new Date()}`,
uploading: false,
name: "",
isFolder: false,
absolutePath: this.currentFolder,
size: 0,
metadata: {},
dataUrl: "",
});
}
},
uploadDrop(ev: DragEvent) {
if (this.fileAddable) {
this.dragCounter = 0;
if (ev?.dataTransfer?.items) {
for (const item of ev.dataTransfer.items) {
if (item.kind === "file") {
const file = item.getAsFile();
if (file !== null) {
this.fileListUploadSelected(file);
}
}
}
}
}
},
setShowDetail(newShowDetail: boolean) {
this.showDetail = newShowDetail;
},
setWaitingForResponse(newIsWaitingForResponse: boolean) {
this.isWaitingForResponse = newIsWaitingForResponse;
},
setIsUploading(newIsUploading: boolean) {
this.isUploading = newIsUploading;
},
setCurrentFolder(newCurrentFolder: string) {
this.currentFolder = newCurrentFolder;
},
setFolder(newFolder: FolderInformation[]) {
this.folderContents = newFolder;
},
setFileListEdit(newFileListEdit: FileInformation[]) {
this.fileListEdit = newFileListEdit;
},
removeElement(index: number, count: number) {
this.fileListUpload.splice(index, count);
},
removeSelection(index: number, count: number) {
const selectionRemovable: FolderContent[] = this.fileListEdit.splice(
index,
count
);
const table = (this.$refs.filesView as Vue).$refs.adaptTable as BTable;
for (let i = 0; i < table.items.length; i++) {
const item = (table.items as FolderContent[])[i];
if (
selectionRemovable.filter((entry) => entry.name === item.name)
.length > 0
) {
table.unselectRow(i);
}
}
},
setFullscreen(newIsFullscreen: boolean) {
this.isFullscreen = newIsFullscreen;
if (newIsFullscreen) {
document.body.classList.add("fullscreen");
this.mainStore.sidebarActive = false;
} else {
document.body.classList.remove("fullscreen");
this.isMetadataManagerHidden = false;
}
},
toggleFullscreen() {
this.setFullscreen(!this.isFullscreen);
},
toggleMenu() {
this.isMetadataManagerHidden = !this.isMetadataManagerHidden;
},
getWindowWidth() {
if (document.documentElement.clientWidth < 1250) {
this.setFullscreen(true);
} else {
this.setFullscreen(false);
}
},
},
components: {
CoscineHeadline,
FilesView,
MetadataManager,
},
});
</script>
<!-- FullScreen + Global Styles -->
<style>
body.fullscreen #resourcecontentview {
position: absolute;
top: 110px;
left: 50px;
right: 0px;
bottom: 0px;
height: auto;
}
body.fullscreen #resourcecontentview .DataSource .headerRow {
margin-left: 0px;
margin-right: 0px;
}
body.fullscreen #metadataManagerDiv {
position: fixed;
bottom: 0px;
top: 110px;
right: 0px;
}
body.fullscreen #metadataManagerDiv .card {
border: 1px solid rgba(0, 0, 0, 0);
height: 100%;
}
body.fullscreen #metadataManagerDiv.hiddenMetadataManager {
right: -600px !important;
}
body.fullscreen #resourcecontentview #filesViewSpan {
margin-right: 25px;
right: 0px;
}
body.fullscreen #toggleFullscreenButton {
left: -68px;
bottom: 4px;
}
#resourcecontentview #filesViewSpan,
body:not(.fullscreen) #resourcecontentview #metadataManagerDiv,
#resourcecontentview .card,
#resourcecontentview .card-body {
height: 100%;
}
#addColumnDropDown button {
padding: 0;
margin: 0;
}
#addColumnDropDown,
#addColumnDropDown__BV_toggle_,
#addColumnDropDown button {
width: 30px;
max-width: 30px;
}
#addColumnDropDown__BV_toggle_ {
min-width: 30px;
}
#addColumnDropDown ul {
padding-left: 10px;
}
#addColumnDropDown .custom-control {
margin-right: 10px;
}
#metadataManagerDiv .input-group button {
border-radius: 0px;
}
#metadataManagerDiv .input-group button:first-child {
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
#metadataManagerDiv .input-group button:last-child {
border-top-right-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
}
</style>
<style scoped>
input[type="file"] {
display: none;
font-size: 0px;
position: absolute;
left: 0;
top: 0;
opacity: 0;
}
#resourcecontentview {
position: relative;
height: calc(100vh - 170px);
margin-bottom: 20px;
}
#resourcecontentview >>> #filesViewSpan {
position: absolute;
right: 620px;
left: 0px;
}
#metadataManagerDiv {
position: absolute;
right: 10px;
width: 600px;
background-color: rgb(253, 252, 251);
-webkit-transition: all 0.3s linear;
transition: all 0.3s linear;
}
#metadataManagerToggleFullscreen {
margin-left: -32px;
line-height: 11px;
position: absolute;
top: -3px;
bottom: 0px;
width: 2em;
min-width: 2em;
padding: 0px;
}
#metadataManagerToggleFullscreen >>> span {
display: inline-block;
transform: rotate(-90deg);
-webkit-transform: rotate(-90deg);
-moz-transform: rotate(-90deg);
-ms-transform: rotate(-90deg);
-o-transform: rotate(-90deg);
white-space: nowrap;
margin-left: -54px;
}
#toggleFullscreenButton {
position: absolute;
left: -44px;
bottom: 4px;
background-color: transparent;
-webkit-transition: all 0.3s linear;
-moz-transition: all 0.3s linear;
-ms-transition: all 0.3s linear;
-o-transition: all 0.3s linear;
transition: all 0.3s linear;
}
#toggleFullscreenButton >>> button {
padding: 3px;
min-width: auto;
}
.droppable {
cursor: pointer;
position: fixed; /* Sit on top of the page content */
width: 100%; /* Full width (cover the whole page) */
height: 100%; /* Full height (cover the whole page) */
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5); /* Black background with opacity */
z-index: 2; /* Specify a stack order in case you're using a different order for other elements */
color: white;
vertical-align: middle;
text-align: center;
font-size: 42pt;
}
.droppableText {
margin-top: 9em;
margin-left: 4em;
}
#resourcecontentview >>> .card-body {
padding-top: 8px;
padding-bottom: 8px;
padding-left: 0px;
padding-right: 0px;
}
</style>
import { defineStore } from "pinia";
import { ResourceState } from "./types";
import type {
ApplicationProfile,
ResourceState,
VisitedResourceObject,
} from "./types";
import { StatusCodes } from "http-status-codes";
import { reactive } from "vue-demi";
import { ResourceApi, ResourceTypeApi } from "@coscine/api-client";
import {
BlobApi,
MetadataApi,
ResourceApi,
ResourceTypeApi,
TreeApi,
} from "@coscine/api-client";
import type { ResourceObject } from "@coscine/api-client/dist/types/Coscine.Api.Resources";
import type { Route } from "vue-router/types/router";
import { useLocalStorage } from "@vueuse/core";
import type { BilingualLabels } from "@coscine/api-client/dist/types/Coscine.Api.Metadata";
/*
Store variable name is "this.<id>Store"
......@@ -20,6 +32,7 @@ export const useResourceStore = defineStore({
--------------------------------------------------------------------------------------
*/
state: (): ResourceState => ({
classes: {},
currentId: null,
resourceTypes: null,
visitedResources: {},
......@@ -35,13 +48,29 @@ export const useResourceStore = defineStore({
:label = "this.resourceStore.<getter_name>;
*/
getters: {
currentResource(): ResourceObject | null {
currentFullApplicationProfile(): ApplicationProfile | null {
if (this.currentId) {
return this.visitedResources[this.currentId].fullApplicationProfile;
} else {
return null;
}
},
currentResource(): VisitedResourceObject | null {
if (this.currentId) {
return this.visitedResources[this.currentId];
} else {
return null;
}
},
currentUsedQuota(): number | null {
if (this.currentId) {
return this.visitedResources[this.currentId].usedQuota;
} else {
return null;
}
},
},
/*
--------------------------------------------------------------------------------------
......@@ -64,6 +93,30 @@ export const useResourceStore = defineStore({
}
},
async retrieveApplicationProfile(resource: VisitedResourceObject) {
if (resource.applicationProfile) {
const apiResponse = await MetadataApi.metadataGetProfile(
resource.applicationProfile
);
if (apiResponse.status === StatusCodes.OK) {
resource.fullApplicationProfile = apiResponse.data;
} else {
// Handle other Status Codes
}
}
},
async retrieveUsedQuota(resource: VisitedResourceObject) {
if (resource.id) {
const apiResponse = await BlobApi.blobGetQuota(resource.id);
if (apiResponse.status === StatusCodes.OK) {
resource.usedQuota = Number(apiResponse.data.data.usedSizeByte);
} else {
// Handle other Status Codes
}
}
},
async retrieveResourceTypes() {
const apiResponse =
await ResourceTypeApi.resourceTypeGetEnabledResourceTypes();
......@@ -98,11 +151,135 @@ export const useResourceStore = defineStore({
if (resource && resource.id) {
if (!this.visitedResources[resource.id]) {
// Important! Keep object assignment reactive()
const visitedResource: ResourceObject = reactive(resource);
const visitedResource: VisitedResourceObject = reactive({
...resource,
fullApplicationProfile: null,
usedQuota: null,
storedColumns: useLocalStorage(
`coscine.rcv.storedColumns.${resource.id}`,
null
),
});
this.visitedResources[resource.id] = visitedResource;
}
}
},
async deleteFile(
resource: ResourceObject | null,
absoluteFilePath: string
) {
if (resource && resource.id) {
try {
await BlobApi.blobDeleteFileWithParameter(
resource.id,
absoluteFilePath
);
return true;
} catch {
return false;
}
}
return false;
},
async getClass(className: string): Promise<BilingualLabels> {
if (!this.classes[className]) {
try {
const response = await MetadataApi.metadataGetClassInstances(
className
);
this.classes[className] = response.data;
} catch {
return {};
}
}
return this.classes[className];
},
async getFile(
resource: ResourceObject | null,
absoluteFilePath: string,
asBlob = false
) {
if (resource && resource.id) {
try {
const response = await BlobApi.blobGetFileWithParameter(
resource.id,
absoluteFilePath,
asBlob
? {
responseType: "blob",
}
: undefined
);
return response.data;
} catch {
return null;
}
}
return null;
},
async getMetadata(
resource: ResourceObject | null,
absoluteFilePath: string
) {
if (resource && resource.id) {
try {
const response = await TreeApi.treeGetMetadataWithParameter(
resource.id,
absoluteFilePath
);
return response.data;
} catch {
return null;
}
}
return null;
},
async storeFile(
resource: ResourceObject | null,
absoluteFilePath: string,
files: Blob[],
options?: unknown
) {
if (resource && resource.id) {
try {
await BlobApi.blobUploadFileWithParameter(
resource.id,
absoluteFilePath,
files,
options
);
return true;
} catch {
return false;
}
}
return false;
},
async storeMetadata(
resource: ResourceObject | null,
absoluteFilePath: string,
body: unknown
) {
if (resource && resource.id) {
try {
await TreeApi.treeStoreMetadataForFileWithParameter(
resource.id,
absoluteFilePath,
{ data: body }
);
return true;
} catch {
return false;
}
}
return false;
},
},
});
......