diff --git a/src/main/java/fr/inra/oresing/rest/ApplicationResult.java b/src/main/java/fr/inra/oresing/rest/ApplicationResult.java new file mode 100644 index 0000000000000000000000000000000000000000..70da2822b084251d2eca1459de9a54b18ded695c --- /dev/null +++ b/src/main/java/fr/inra/oresing/rest/ApplicationResult.java @@ -0,0 +1,51 @@ +package fr.inra.oresing.rest; + +import lombok.Value; + +import java.util.Map; +import java.util.Set; + +@Value +public class ApplicationResult { + String id; + String name; + String title; + Map<String, Reference> references; + Map<String, DataType> dataTypes; + + @Value + public static class Reference { + String id; + String label; + Set<String> children; + Map<String, Column> columns; + + @Value + public static class Column { + String id; + String title; + boolean key; + String linkedTo; + } + } + + @Value + public static class DataType { + String id; + String label; + Map<String, Variable> variables; + + @Value + public static class Variable { + String id; + String label; + Map<String, Component> components; + + @Value + public static class Component { + String id; + String label; + } + } + } +} diff --git a/src/main/java/fr/inra/oresing/rest/GetReferenceResult.java b/src/main/java/fr/inra/oresing/rest/GetReferenceResult.java new file mode 100644 index 0000000000000000000000000000000000000000..d1488cc30339b532c7c736f93bb06c42731430e9 --- /dev/null +++ b/src/main/java/fr/inra/oresing/rest/GetReferenceResult.java @@ -0,0 +1,18 @@ +package fr.inra.oresing.rest; + +import lombok.Value; + +import java.util.Map; +import java.util.Set; + +@Value +public class GetReferenceResult { + Set<ReferenceValue> referenceValues; + + @Value + public static class ReferenceValue { + String hierarchicalKey; + String naturalKey; + Map<String, String> values; + } +} diff --git a/src/main/java/fr/inra/oresing/rest/OreSiResources.java b/src/main/java/fr/inra/oresing/rest/OreSiResources.java index 6be34e4b2e23db24a110b1784d92f5ec5e4e708c..b94fbf1098eb2138307e19e30dc7da710b71a645 100644 --- a/src/main/java/fr/inra/oresing/rest/OreSiResources.java +++ b/src/main/java/fr/inra/oresing/rest/OreSiResources.java @@ -1,9 +1,16 @@ package fr.inra.oresing.rest; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Maps; +import com.google.common.collect.Ordering; +import com.google.common.collect.TreeMultimap; import fr.inra.oresing.checker.CheckerException; import fr.inra.oresing.model.Application; import fr.inra.oresing.model.BinaryFile; +import fr.inra.oresing.model.Configuration; import fr.inra.oresing.model.ReferenceValue; import fr.inra.oresing.persistence.OreSiRepository; import org.springframework.beans.factory.annotation.Autowired; @@ -24,6 +31,7 @@ import org.springframework.web.util.UriUtils; import java.io.IOException; import java.net.URI; import java.nio.charset.Charset; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -81,9 +89,39 @@ public class OreSiResources { } @GetMapping(value = "/applications/{nameOrId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity<Application> getApplication(@PathVariable("nameOrId") String nameOrId) { + public ResponseEntity<ApplicationResult> getApplication(@PathVariable("nameOrId") String nameOrId) { Application application = service.getApplication(nameOrId); - return ResponseEntity.ok(application); + TreeMultimap<String, String> childrenPerReferences = TreeMultimap.create(); + application.getConfiguration().getCompositeReferences().values().forEach(compositeReferenceDescription -> { + ImmutableList<String> referenceTypes = compositeReferenceDescription.getComponents().stream() + .map(Configuration.CompositeReferenceComponentDescription::getReference) + .collect(ImmutableList.toImmutableList()); + ImmutableSortedSet<String> sortedReferenceTypes = ImmutableSortedSet.copyOf(Ordering.explicit(referenceTypes), referenceTypes); + sortedReferenceTypes.forEach(reference -> { + String child = sortedReferenceTypes.higher(reference); + if (child == null) { + // on est sur le dernier élément de la hiérarchie, pas de descendant + } else { + childrenPerReferences.put(reference, child); + } + }); + }); + Map<String, ApplicationResult.Reference> references = Maps.transformEntries(application.getConfiguration().getReferences(), (reference, referenceDescription) -> { + Map<String, ApplicationResult.Reference.Column> columns = Maps.transformEntries(referenceDescription.getColumns(), (column, columnDescription) -> new ApplicationResult.Reference.Column(column, column, referenceDescription.getKeyColumns().contains(column), null)); + Set<String> children = childrenPerReferences.get(reference); + return new ApplicationResult.Reference(reference, reference, children, columns); + }); + Map<String, ApplicationResult.DataType> dataTypes = Maps.transformEntries(application.getConfiguration().getDataTypes(), (dataType, dataTypeDescription) -> { + Map<String, ApplicationResult.DataType.Variable> variables = Maps.transformEntries(dataTypeDescription.getData(), (variable, variableDescription) -> { + Map<String, ApplicationResult.DataType.Variable.Component> components = Maps.transformEntries(variableDescription.getComponents(), (component, componentDescription) -> { + return new ApplicationResult.DataType.Variable.Component(component, component); + }); + return new ApplicationResult.DataType.Variable(variable, variable, components); + }); + return new ApplicationResult.DataType(dataType, dataType, variables); + }); + ApplicationResult applicationResult = new ApplicationResult(application.getId().toString(), application.getName(), application.getConfiguration().getApplication().getName(), references, dataTypes); + return ResponseEntity.ok(applicationResult); } @GetMapping(value = "/applications/{nameOrId}/configuration", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) @@ -137,12 +175,21 @@ public class OreSiResources { * @return un tableau de chaine */ @GetMapping(value = "/applications/{nameOrId}/references/{refType}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity<List<ReferenceValue>> listReferences( + public ResponseEntity<GetReferenceResult> listReferences( @PathVariable("nameOrId") String nameOrId, @PathVariable("refType") String refType, @RequestParam MultiValueMap<String, String> params) { List<ReferenceValue> list = service.findReference(nameOrId, refType, params); - return ResponseEntity.ok(list); + ImmutableSet<GetReferenceResult.ReferenceValue> referenceValues = list.stream() + .map(referenceValue -> + new GetReferenceResult.ReferenceValue( + referenceValue.getHierarchicalKey(), + referenceValue.getNaturalKey(), + referenceValue.getRefValues() + ) + ) + .collect(ImmutableSortedSet.toImmutableSortedSet(Comparator.comparing(GetReferenceResult.ReferenceValue::getHierarchicalKey))); + return ResponseEntity.ok(new GetReferenceResult(referenceValues)); } @GetMapping(value = "/applications/{nameOrId}/references/{refType}/{column}", produces = MediaType.APPLICATION_JSON_VALUE) diff --git a/src/test/java/fr/inra/oresing/rest/OreSiResourcesTest.java b/src/test/java/fr/inra/oresing/rest/OreSiResourcesTest.java index 0d5469b709960446e5ebce4f965a8b83a8f8120a..ed6123d073e4bd9f2e460228f50581cba1701822 100644 --- a/src/test/java/fr/inra/oresing/rest/OreSiResourcesTest.java +++ b/src/test/java/fr/inra/oresing/rest/OreSiResourcesTest.java @@ -5,7 +5,6 @@ import com.google.common.base.Charsets; import com.google.common.io.Resources; import com.jayway.jsonpath.JsonPath; import fr.inra.oresing.OreSiNg; -import fr.inra.oresing.model.Application; import fr.inra.oresing.model.OreSiUser; import fr.inra.oresing.persistence.AuthenticationService; import lombok.extern.slf4j.Slf4j; @@ -37,9 +36,8 @@ import javax.servlet.http.Cookie; import java.io.InputStream; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.Date; -import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -118,12 +116,11 @@ public class OreSiResourcesTest { .andExpect(jsonPath("$.id", Is.is(appId))) .andReturn().getResponse().getContentAsString(); - Application app2 = objectMapper.readValue(response, Application.class); + ApplicationResult applicationResult = objectMapper.readValue(response, ApplicationResult.class); - Date now = new Date(); - Assert.assertEquals("monsore", app2.getName()); - Assert.assertEquals(List.of("especes", "projet", "sites", "themes", "type de fichiers", "type_de_sites", "types_de_donnees_par_themes_de_sites_et_projet", "unites", "valeurs_qualitatives", "variables", "variables_et_unites_par_types_de_donnees"), app2.getReferenceType()); - Assert.assertEquals(List.of("pem"), app2.getDataType()); + Assert.assertEquals("monsore", applicationResult.getName()); + Assert.assertEquals(Set.of("especes", "projet", "sites", "themes", "type de fichiers", "type_de_sites", "types_de_donnees_par_themes_de_sites_et_projet", "unites", "valeurs_qualitatives", "variables", "variables_et_unites_par_types_de_donnees"), applicationResult.getReferences().keySet()); +// Assert.assertEquals(List.of("pem"), applicationResult.getDataType()); // Ajout de referentiel for (Map.Entry<String, String> e : fixtures.getMonsoreReferentielFiles().entrySet()) { @@ -148,8 +145,8 @@ public class OreSiResourcesTest { .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andReturn().getResponse().getContentAsString(); - List refs = objectMapper.readValue(getReferencesResponse, List.class); - Assert.assertEquals(9, refs.size()); + GetReferenceResult GetReferenceResult = objectMapper.readValue(getReferencesResponse, GetReferenceResult.class); + Assert.assertEquals(9, GetReferenceResult.getReferenceValues().size()); // ajout de data resource = getClass().getResource(fixtures.getPemDataResourceName()); @@ -373,8 +370,8 @@ public class OreSiResourcesTest { .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andReturn().getResponse().getContentAsString(); - List refs = objectMapper.readValue(getReferenceResponse, List.class); - Assert.assertEquals(103, refs.size()); + GetReferenceResult refs = objectMapper.readValue(getReferenceResponse, GetReferenceResult.class); + Assert.assertEquals(103, refs.getReferenceValues().size()); // Ajout de referentiel for (Map.Entry<String, String> e : fixtures.getAcbbReferentielFiles().entrySet()) { @@ -399,8 +396,8 @@ public class OreSiResourcesTest { .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andReturn().getResponse().getContentAsString(); - refs = objectMapper.readValue(getReferenceResponse, List.class); - Assert.assertEquals(103, refs.size()); + refs = objectMapper.readValue(getReferenceResponse, GetReferenceResult.class); + Assert.assertEquals(103, refs.getReferenceValues().size()); // ajout de data try (InputStream in = getClass().getResourceAsStream(fixtures.getFluxToursDataResourceName())) { diff --git a/ui2/src/components/common/CollapsibleTree.vue b/ui2/src/components/common/CollapsibleTree.vue new file mode 100644 index 0000000000000000000000000000000000000000..d12ccdda55e11176e2286d3b7d0582b152119bd8 --- /dev/null +++ b/ui2/src/components/common/CollapsibleTree.vue @@ -0,0 +1,137 @@ +<template> + <div> + <div + :class="`CollapsibleTree-header ${children && children.length !== 0 ? 'clickable' : ''} ${ + children && children.length !== 0 && displayChildren ? '' : 'mb-1' + }`" + :style="`background-color:rgba(240, 245, 245, ${1 - level / 2})`" + @click="displayChildren = !displayChildren" + > + <div class="CollapsibleTree-header-infos"> + <FontAwesomeIcon + v-if="children && children.length !== 0" + :icon="displayChildren ? 'caret-down' : 'caret-right'" + class="clickable mr-3" + /> + <div + class="link" + :style="`transform:translate(${level * 50}px);`" + @click="(event) => onClickLabelCb(event, label)" + > + {{ label }} + </div> + </div> + <div class="CollapsibleTree-buttons"> + <b-field class="file button is-small is-info" v-if="onUploadCb"> + <b-upload + v-model="refFile" + class="file-label" + accept=".csv" + @input="() => onUploadCb(label, refFile)" + > + <span class="file-name" v-if="refFile"> + {{ refFile.name }} + </span> + <span class="file-cta"> + <b-icon class="file-icon" icon="upload"></b-icon> + </span> + </b-upload> + </b-field> + <div v-for="button in buttons" :key="button.id"> + <b-button + :icon-left="button.iconName" + size="is-small" + @click="button.clickCb(label)" + class="ml-1" + :type="button.type" + > + {{ button.label }}</b-button + > + </div> + </div> + </div> + <div v-if="displayChildren"> + <CollapsibleTree + v-for="child in children" + :key="child.id" + :label="child.label" + :children="child.children" + :level="level + 1" + :onClickLabelCb="onClickLabelCb" + :onUploadCb="onUploadCb" + :buttons="buttons" + /> + </div> + </div> +</template> + +<script> +import { Component, Prop, Vue } from "vue-property-decorator"; +import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; + +@Component({ + components: { FontAwesomeIcon }, +}) +export default class CollapsibleTree extends Vue { + @Prop() label; + @Prop() children; + @Prop() level; + @Prop() onClickLabelCb; + @Prop() onUploadCb; + @Prop() buttons; + + displayChildren = false; + refFile = null; +} +</script> + +<style lang="scss" scoped> +$row-height: 40px; + +.CollapsibleTree-header { + display: flex; + align-items: center; + height: $row-height; + padding: 0.75rem; + justify-content: space-between; + + .file-icon { + margin-right: 0; + } + + .file-name { + border-top-style: none; + border-right-style: none; + border-bottom-style: none; + border-left-width: 2px; + border-radius: 0px; + opacity: 0.8; + background-color: rgba(250, 250, 250, 1); + + &:hover { + opacity: 1; + } + } +} + +.CollapsibleTree-header-infos { + display: flex; + align-items: center; +} + +.CollapsibleTree-buttons { + display: flex; + height: $row-height; + align-items: center; + + .file { + margin-bottom: 0; + + .file-cta { + height: 100%; + background-color: transparent; + border-color: transparent; + } + } +} +</style> diff --git a/ui2/src/components/common/SidePanel.vue b/ui2/src/components/common/SidePanel.vue new file mode 100644 index 0000000000000000000000000000000000000000..ff6565ff9d4b10d131ea69f9a38fa1d2cc77a371 --- /dev/null +++ b/ui2/src/components/common/SidePanel.vue @@ -0,0 +1,72 @@ +<template> + <div :class="`SidePanel ${leftAlign ? 'left-align' : 'right-align'} ${innerOpen ? 'open' : ''}`"> + <h1 class="title main-title">{{ title }}</h1> + <b-button class="SidePanel-close-button" icon-left="times" @click="innerOpen = false" /> + <slot></slot> + </div> +</template> + +<script> +import { Component, Prop, Vue, Watch } from "vue-property-decorator"; + +@Component({ + components: {}, +}) +export default class SidePanel extends Vue { + @Prop({ default: false }) leftAlign; + @Prop({ default: false }) open; + @Prop({ default: "" }) title; + @Prop() closeCb; + + innerOpen = false; + + created() { + this.innerOpen = this.open; + } + + @Watch("open") + onExternalOpenStateChanged(newVal) { + this.innerOpen = newVal; + } + + @Watch("innerOpen") + onInnerOpenStateChanged(newVal) { + this.closeCb(newVal); + } +} +</script> + +<style lang="scss" scoped> +.SidePanel { + background-color: $light; + z-index: 1; + position: absolute; + height: 100%; + top: 0; + width: 33%; + padding: $container-padding-vert 2.5rem; + transition: transform 250ms; + + &.right-align { + right: 0; + transform: translateX(100%); + &.open { + transform: translateX(0); + } + } + + &.left-align { + left: 0; + transform: translateX(-100%); + &.open { + transform: translateX(0); + } + } +} + +.SidePanel-close-button { + position: absolute; + top: 0; + right: 0; +} +</style> diff --git a/ui2/src/components/common/SubMenu.vue b/ui2/src/components/common/SubMenu.vue new file mode 100644 index 0000000000000000000000000000000000000000..2b5d444c67a87f5336f69ba0bc49dee4a3856a1e --- /dev/null +++ b/ui2/src/components/common/SubMenu.vue @@ -0,0 +1,53 @@ +<template> + <div class="SubMenu"> + <span class="SubMenu-root">{{ root }}</span> + <div v-for="(path, index) in paths" :key="path.label"> + <span class="SubMenu-path-separator mr-1 ml-1">/</span> + <span + @click="index !== paths.length - 1 ? path.clickCb() : ''" + :class="index !== paths.length - 1 ? 'link' : ''" + >{{ path.label }}</span + > + </div> + </div> +</template> + +<script> +import { Component, Prop, Vue } from "vue-property-decorator"; + +export class SubMenuPath { + label; + clickCb; + + constructor(label, clickCb) { + this.label = label; + this.clickCb = clickCb; + } +} + +@Component({ + components: {}, +}) +export default class SubMenu extends Vue { + @Prop() root; + @Prop() paths; +} +</script> + +<style lang="scss" scoped> +.SubMenu { + display: flex; + height: 40px; + background-color: $info-transparent; + align-items: center; + padding: 0.5rem $container-padding-hor; + width: calc(100% + 2 * #{$container-padding-hor}); + transform: translateX(-$container-padding-hor); +} + +.SubMenu-root { + color: $dark; + font-weight: 600; + font-size: 1.2em; +} +</style> diff --git a/ui2/src/components/references/ReferencesDetailsPanel.vue b/ui2/src/components/references/ReferencesDetailsPanel.vue new file mode 100644 index 0000000000000000000000000000000000000000..1fdae4b09e75aba1dd92e40572d6236a2c501088 --- /dev/null +++ b/ui2/src/components/references/ReferencesDetailsPanel.vue @@ -0,0 +1,57 @@ +<template> + <SidePanel + :open="open" + :leftAlign="leftAlign" + :title="reference && reference.label" + :closeCb="closeCb" + > + <div class="Panel-buttons"> + <b-button type="is-danger" icon-left="trash-alt" @click="askDeletionConfirmation">{{ + $t("referencesManagement.delete") + }}</b-button> + </div> + </SidePanel> +</template> + +<script> +import { AlertService } from "@/services/AlertService"; +import { Component, Prop, Vue } from "vue-property-decorator"; +import SidePanel from "../common/SidePanel.vue"; + +@Component({ + components: { SidePanel }, +}) +export default class ReferencesDetailsPanel extends Vue { + @Prop({ default: false }) leftAlign; + @Prop({ default: false }) open; + @Prop() reference; + @Prop() closeCb; + + alertService = AlertService.INSTANCE; + + askDeletionConfirmation() { + this.alertService.dialog( + this.$t("alert.warning"), + this.$t("alert.reference-deletion-msg", { label: this.reference.label }), + this.$t("alert.delete"), + "is-danger", + () => this.deleteReference() + ); + } + + deleteReference() { + console.log("DELETE", this.reference); + } +} +</script> + +<style lang="scss" scoped> +.Panel-buttons { + display: flex; + flex-direction: column; + + .button { + margin-bottom: 0.5rem; + } +} +</style> diff --git a/ui2/src/locales/en.json b/ui2/src/locales/en.json index 825e994c432e373607bf268ef87b6e2009643dc5..7a13a47c233853cb82f25ffe6af65b75f1d432a4 100644 --- a/ui2/src/locales/en.json +++ b/ui2/src/locales/en.json @@ -2,16 +2,17 @@ "titles": { "login-page": "Welcome to SI-ORE", "applications-page": "My applications", - "references-page": "My references", + "references-page": "{applicationName} references", + "references-data": "{refName} data", "application-creation": "Application creation" }, "login": { "signin": "Sign in", "signup": "Sign up", "login": "Login", - "login-placeholder": "Write down the login", + "login-placeholder": "Ex: michel", "pwd": "Password", - "pwd-placeholder": "Write down the password", + "pwd-placeholder": "Ex: xxxx", "pwd-forgotten": "Forgotten password ? " }, "validation": { @@ -26,7 +27,12 @@ "server-error": "A server error occured", "user-unknown": "Unknown user", "application-creation-success": "The app has been created!", - "application-validate-success": "The YAML file is valid!" + "application-validate-success": "The YAML file is valid!", + "warning": "Warning !", + "reference-deletion-msg": "You're about to delete the reference : {label}. Are you sure ?", + "delete": "Delete", + "reference-csv-upload-error": "An error occured while uploading the csv file", + "reference-updated": "Reference updated" }, "message": { "app-config-error": "Error in yaml file", @@ -45,7 +51,10 @@ "create": "Create application", "test": "Test", "name": "Application name", - "name-placeholder": "Write down the application name" + "name-placeholder": "Ex : olac", + "creation-date": "Creation date", + "actions": "Actions", + "references": "References" }, "errors": { "emptyFile": "File is empty", @@ -69,5 +78,13 @@ "unknownCheckerName": "For the validation rule : <code>{lineValidationRuleKey}</code>, '<code>{checkerName}</code>' is declared but is not a known checker", "csvBoundToUnknownVariable": "In the CSV format, header <code>{header}</code> is bound to unknown variable <code>{variable}</code>. Known variables: <code>{variables}</code>", "csvBoundToUnknownVariableComponent": "In the CSV format, header <code>{header}</code> is bound to <code>{variable}</code> but it has no <code>{component}</code> component. Known components: <code>{components}</code>" + }, + "referencesManagement": { + "actions": "Actions", + "consult": "Consult", + "download": "Download", + "delete": "Delete", + "references": "References", + "data": "Data" } -} \ No newline at end of file +} diff --git a/ui2/src/locales/fr.json b/ui2/src/locales/fr.json index 5d4b60692f7dbe1aa511ab875758c14271c8ee2c..2a24d21b6d9e1f718e6196a6ad7e26ca631e8f7a 100644 --- a/ui2/src/locales/fr.json +++ b/ui2/src/locales/fr.json @@ -2,16 +2,17 @@ "titles": { "login-page": "Bienvenue sur SI-ORE", "applications-page": "Mes applications", - "references-page": "Mes référentiels", + "references-page": "Référentiels de {applicationName}", + "references-data": "Données de {refName}", "application-creation": "Créer une application" }, "login": { "signin": "Se connecter", "signup": "Créer un compte", "login": "Identifiant", - "login-placeholder": "Entrer l'identifiant", + "login-placeholder": "Ex: michel", "pwd": "Mot de passe", - "pwd-placeholder": "Entrer le mot de passe", + "pwd-placeholder": "Ex: xxxx", "pwd-forgotten": "Mot de passe oublié ?" }, "validation": { @@ -26,7 +27,12 @@ "server-error": "Une erreur serveur est survenue", "user-unknown": "Identifiants inconnus", "application-creation-success": "L'application a été créée !", - "application-validate-success": "Le fichier YAML est valide !" + "application-validate-success": "Le fichier YAML est valide !", + "warning": "Attention !", + "reference-deletion-msg": "Vous allez supprimer le référentiel : {label}. Êtes-vous sûr ?", + "delete": "Supprimer", + "reference-csv-upload-error": "Une erreur s'est produite au téléversement du fichier csv", + "reference-updated": "Référentiel mis à jour" }, "message": { "app-config-error": "Erreur dans le fichier yaml", @@ -45,7 +51,10 @@ "create": "Créer l'application", "test": "Tester", "name": "Nom de l'application", - "name-placeholder": "Entrer le nom de l'application" + "name-placeholder": "Ex : olac", + "creation-date": "Date de création", + "actions": "Actions", + "references": "Référentiels" }, "errors": { "emptyFile": "Le fichier est vide", @@ -69,5 +78,13 @@ "unknownCheckerName": "Pour la règle de validation <code>{lineValidationRuleKey}</code>, '<code>{checkerName}</code>' est déclaré mais ce n’est pas un contrôle connu", "csvBoundToUnknownVariable": "Dans le format CSV, l’entête <code>{header}</code> est lié à la variable <code>{variable}</code> qui n’est pas connue. Variables connues <code>{variables}</code>", "csvBoundToUnknownVariableComponent": "Dans le format CSV, l’entête <code>{header}</code> est lié à la variable <code>{variable}</code> mais elle n’a pas de composant <code>{component}</code>. Composants connus <code>{components}</code>" + }, + "referencesManagement": { + "actions": "Actions", + "consult": "Consulter", + "download": "Télécharger", + "delete": "Supprimer", + "references": "Référentiels", + "data": "Données" } } diff --git a/ui2/src/main.js b/ui2/src/main.js index 47d3688f4a5e61d2c3c3d3ec9c4a9677c9876fc6..4b248832a2bd0cf07b8512ea6119bae512f51154 100644 --- a/ui2/src/main.js +++ b/ui2/src/main.js @@ -9,15 +9,24 @@ import { faAngleRight, faArrowDown, faArrowUp, + faCaretDown, + faCaretUp, faCheck, + faDownload, + faDraftingCompass, faExclamationCircle, faEye, faEyeSlash, faGlobe, faPlus, + faPoll, faSignOutAlt, + faTimes, + faTrashAlt, faUpload, + faWrench, faVial, + faCaretRight, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; library.add( @@ -33,7 +42,16 @@ library.add( faArrowDown, faAngleLeft, faAngleRight, - faVial + faWrench, + faPoll, + faDraftingCompass, + faCaretUp, + faCaretDown, + faTimes, + faTrashAlt, + faDownload, + faVial, + faCaretRight ); Vue.component("vue-fontawesome", FontAwesomeIcon); diff --git a/ui2/src/model/ApplicationResult.js b/ui2/src/model/ApplicationResult.js new file mode 100644 index 0000000000000000000000000000000000000000..835c10792d19f2ee46cb412971862e76c4c80e6b --- /dev/null +++ b/ui2/src/model/ApplicationResult.js @@ -0,0 +1,18 @@ +export class ApplicationResult { + id; + name; + title; + references = { + idRef: { + id: "", + label: "", + children: [], + columns: { + id: "", + title: "", + key: false, + linkedTo: "", + }, + }, + }; +} diff --git a/ui2/src/model/Button.js b/ui2/src/model/Button.js new file mode 100644 index 0000000000000000000000000000000000000000..5d3d08c397e269b3de9db1dd3f6132e53785fbe1 --- /dev/null +++ b/ui2/src/model/Button.js @@ -0,0 +1,15 @@ +export class Button { + id; + label; + iconName; + clickCb; + type; + + constructor(label, iconName, clickCb, type, id) { + this.label = label; + this.iconName = iconName; + this.clickCb = clickCb; + this.id = id ? id : label ? label : iconName; + this.type = type; + } +} diff --git a/ui2/src/router/index.js b/ui2/src/router/index.js index e72efccf6df1177cf727d5a01a2a5770cf0e13ac..0001e72b4f9fca083b5f531564657bb92b61caa6 100644 --- a/ui2/src/router/index.js +++ b/ui2/src/router/index.js @@ -1,8 +1,10 @@ import Vue from "vue"; import VueRouter from "vue-router"; import LoginView from "@/views/LoginView.vue"; -import ApplicationsView from "@/views/ApplicationsView.vue"; -import ApplicationCreationView from "@/views/ApplicationCreationView.vue"; +import ApplicationsView from "@/views/application/ApplicationsView.vue"; +import ApplicationCreationView from "@/views/application/ApplicationCreationView.vue"; +import ReferencesManagementView from "@/views/references/ReferencesManagementView.vue"; +import ReferenceTable from "@/views/references/ReferenceTableView.vue"; Vue.use(VueRouter); @@ -26,6 +28,17 @@ const routes = [ name: "Application creation", component: ApplicationCreationView, }, + { + path: "/applications/:applicationName/references", + name: "References management view", + component: ReferencesManagementView, + props: true, + }, + { + path: "/applications/:applicationName/references/:refId", + component: ReferenceTable, + props: true, + }, ]; const router = new VueRouter({ diff --git a/ui2/src/services/AlertService.js b/ui2/src/services/AlertService.js index be93def851e1ebb220f2661ba68ccf985883ff89..9064f6b9e753bdbaf55ecb312cb074038edec029 100644 --- a/ui2/src/services/AlertService.js +++ b/ui2/src/services/AlertService.js @@ -1,6 +1,6 @@ import { i18n } from "@/main"; import { BuefyTypes } from "@/utils/BuefyUtils"; -import { ToastProgrammatic } from "buefy"; +import { ToastProgrammatic, DialogProgrammatic } from "buefy"; const TOAST_INFO_DURATION = 3000; const TOAST_ERROR_DURATION = 8000; @@ -44,4 +44,18 @@ export class AlertService { toastServerError(error) { this.toastError(i18n.t("alert.server-error"), error); } + + dialog(title, message, confirmText, type, onConfirmCb) { + DialogProgrammatic.confirm({ + title: title, + message: message, + confirmText: confirmText, + type: type, + hasIcon: true, + cancelText: this.cancelMsg, + onConfirm: () => { + onConfirmCb(); + }, + }); + } } diff --git a/ui2/src/services/rest/ApplicationService.js b/ui2/src/services/rest/ApplicationService.js index 12df40ee045ad9c924c0c2b596c9f058f1586ebc..54869e111b8d90e025085683020dc895cf847ed3 100644 --- a/ui2/src/services/rest/ApplicationService.js +++ b/ui2/src/services/rest/ApplicationService.js @@ -17,6 +17,14 @@ export class ApplicationService extends Fetcher { return this.get("applications/"); } + async getApplication(name) { + return this.get("applications/" + name); + } + + async getDataset(dataset, applicationName) { + return this.get(`applications/${applicationName}/data/${dataset}`); + } + async validateConfiguration(applicationConfig) { return this.post("validate-configuration", { file: applicationConfig.file, diff --git a/ui2/src/services/rest/ReferenceService.js b/ui2/src/services/rest/ReferenceService.js new file mode 100644 index 0000000000000000000000000000000000000000..36c03c4469ebf9e489a4ef49f6c4d493c98575d1 --- /dev/null +++ b/ui2/src/services/rest/ReferenceService.js @@ -0,0 +1,19 @@ +import { Fetcher } from "../Fetcher"; + +export class ReferenceService extends Fetcher { + static INSTANCE = new ReferenceService(); + + constructor() { + super(); + } + + async getReferenceValues(applicationName, referenceId) { + return this.get(`applications/${applicationName}/references/${referenceId}`); + } + + async createReference(applicationName, referenceId, refFile) { + return this.post(`applications/${applicationName}/references/${referenceId}`, { + file: refFile, + }); + } +} diff --git a/ui2/src/style/_common.scss b/ui2/src/style/_common.scss index 0237eb0c731c052396e9b693594069ac7a7f7f0b..04baf8edb161acee274059879406507a78225e15 100644 --- a/ui2/src/style/_common.scss +++ b/ui2/src/style/_common.scss @@ -6,7 +6,7 @@ body { .title { color: $primary; - margin-top: 1.5rem; + margin-top: $title-margin-top; &.main-title { display: flex; @@ -19,6 +19,18 @@ a { color: $info; } +.clickable { + cursor: pointer; +} + +.link { + cursor: pointer; + &:hover { + color: $primary; + text-decoration: underline; + } +} + // Input style .input-field { diff --git a/ui2/src/style/_variables.scss b/ui2/src/style/_variables.scss index 4b6281368a8adcaf09b8e13c067224368c06178a..4abde752e91609d8fba1ef098c67839f4241010a 100644 --- a/ui2/src/style/_variables.scss +++ b/ui2/src/style/_variables.scss @@ -1,29 +1,33 @@ -/************************************************************************************************** -* Global app variables (defined in the app) * -***************************************************************************************************/ - -$font-family: "LiberationSans", Helvetica, Arial, sans-serif; - -$text-default-color: #2c3e50; -$light-text: rgb(230, 230, 230); -// InputWithValidation -$input-field-margin-bot: 1rem; -$input-help-margin-top: 0.25rem; - -// MenuView -$menu-height: 80px; - -/************************************************************************************************** -* Buefy/Bulma customizations * -* see all customizable variables here https://buefy.org/documentation/customization/ * -***************************************************************************************************/ - -// General variables -$primary: rgb(0,163,166); -$info: #4ec6c2; -$success: #bade81; -$warning: #ffec60; -$danger: #d13a18; -$dark: #4b6464; -$light: #aab7b7; -$family-primary: $font-family; +/************************************************************************************************** +* Global app variables (defined in the app) * +***************************************************************************************************/ + +$font-family: "LiberationSans", Helvetica, Arial, sans-serif; + +$text-default-color: #2c3e50; +$light-text: rgb(230, 230, 230); +$primary-slightly-transparent: rgba(0, 163, 166, 0.8); +$info-transparent: rgba(78, 198, 194, 0.3); + +// PageView +$container-padding-hor: 3rem; +$container-padding-vert: $container-padding-hor / 2; +$title-margin-top: 1.5rem; + +// MenuView +$menu-height: 80px; + +/************************************************************************************************** +* Buefy/Bulma customizations * +* see all customizable variables here https://buefy.org/documentation/customization/ * +***************************************************************************************************/ + +// General variables +$primary: rgb(0, 163, 166); +$info: rgb(78, 198, 194); +$success: #bade81; +$warning: #ffec60; +$danger: #d13a18; +$dark: #4b6464; +$light: rgb(202, 216, 216); +$family-primary: $font-family; diff --git a/ui2/src/utils/ConversionUtils.js b/ui2/src/utils/ConversionUtils.js new file mode 100644 index 0000000000000000000000000000000000000000..3e35a15cb941702fee16e8478e2eedd532c6d3f2 --- /dev/null +++ b/ui2/src/utils/ConversionUtils.js @@ -0,0 +1,25 @@ +export function convertReferencesToTrees(initialReference) { + const references = JSON.parse(JSON.stringify(initialReference)); + const parents = references.filter((ref) => { + return !references.some( + (r) => r.children && r.children.length !== 0 && r.children.some((c) => c === ref.id) + ); + }); + return replaceChildrenIdByObject(parents, references); +} + +function replaceChildrenIdByObject(references, initialRef) { + references.forEach((ref) => { + if (ref.children && ref.children.length !== 0) { + const children = ref.children.map((c) => { + const index = initialRef.findIndex((r) => r.id === c); + const [child] = initialRef.splice(index, 1); + return child; + }); + ref.children = replaceChildrenIdByObject(children, initialRef); + } else { + return ref; + } + }); + return references; +} diff --git a/ui2/src/views/ApplicationsView.vue b/ui2/src/views/ApplicationsView.vue deleted file mode 100644 index e6845c3e63616d50762039866580a31d6e885556..0000000000000000000000000000000000000000 --- a/ui2/src/views/ApplicationsView.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> - <div> - <PageView> - <h1 class="title main-title">{{ $t("titles.applications-page") }}</h1> - <div class="buttons"> - <b-button type="is-primary" @click="createApplication" icon-right="plus"> - {{ $t("applications.create") }} - </b-button> - </div> - <b-table - :data="applications" - :striped="true" - :isFocusable="true" - :isHoverable="true" - :sticky-header="true" - :paginated="true" - :per-page="15" - height="100%" - > - <b-table-column field="name" label="Name" sortable width="50%" v-slot="props"> - {{ props.row.name }} - </b-table-column> - <b-table-column field="creationDate" label="Creation Date" sortable v-slot="props"> - {{ new Date(props.row.creationDate) }} - </b-table-column> - </b-table> - </PageView> - </div> -</template> - -<script> -import { ApplicationService } from "@/services/rest/ApplicationService"; -import { Component, Vue } from "vue-property-decorator"; -import PageView from "./common/PageView.vue"; - -@Component({ - components: { PageView }, -}) -export default class ApplicationsView extends Vue { - applicationService = ApplicationService.INSTANCE; - - applications = []; - - created() { - this.init(); - } - - async init() { - this.applications = await this.applicationService.getApplications(); - } - - createApplication() { - this.$router.push("/applicationCreation"); - } -} -</script> diff --git a/ui2/src/views/ApplicationCreationView.vue b/ui2/src/views/application/ApplicationCreationView.vue similarity index 100% rename from ui2/src/views/ApplicationCreationView.vue rename to ui2/src/views/application/ApplicationCreationView.vue diff --git a/ui2/src/views/application/ApplicationsView.vue b/ui2/src/views/application/ApplicationsView.vue new file mode 100644 index 0000000000000000000000000000000000000000..bfc21d229422c9e842a03fc3aa9bd5d2d4982bc2 --- /dev/null +++ b/ui2/src/views/application/ApplicationsView.vue @@ -0,0 +1,71 @@ +<template> + <PageView> + <h1 class="title main-title">{{ $t("titles.applications-page") }}</h1> + <div class="buttons"> + <b-button type="is-primary" @click="createApplication" icon-right="plus"> + {{ $t("applications.create") }} + </b-button> + </div> + <b-table + :data="applications" + :striped="true" + :isFocusable="true" + :isHoverable="true" + :sticky-header="true" + :paginated="true" + :per-page="15" + height="100%" + > + <b-table-column field="name" :label="$t('applications.name')" sortable v-slot="props"> + {{ props.row.name }} + </b-table-column> + <b-table-column + field="creationDate" + :label="$t('applications.creation-date')" + sortable + v-slot="props" + > + {{ new Date(props.row.creationDate) }} + </b-table-column> + <b-table-column field="actions" :label="$t('applications.actions')" v-slot="props"> + <b-button icon-left="drafting-compass" @click="displayReferencesManagement(props.row)">{{ + $t("applications.references") + }}</b-button> + </b-table-column> + </b-table> + </PageView> +</template> + +<script> +import { ApplicationService } from "@/services/rest/ApplicationService"; +import { Component, Vue } from "vue-property-decorator"; +import PageView from "@/views/common/PageView.vue"; + +@Component({ + components: { PageView }, +}) +export default class ApplicationsView extends Vue { + applicationService = ApplicationService.INSTANCE; + + applications = []; + + created() { + this.init(); + } + + async init() { + this.applications = await this.applicationService.getApplications(); + } + + createApplication() { + this.$router.push("/applicationCreation"); + } + + displayReferencesManagement(application) { + if (!application) { + return; + } + this.$router.push("/applications/" + application.name + "/references"); + } +} +</script> diff --git a/ui2/src/views/common/MenuView.vue b/ui2/src/views/common/MenuView.vue index 5880b3ddd0269ef1694dac9141bce2a2bc4de8c6..b98c505e9e209d507046fd7afe6b20808669c246 100644 --- a/ui2/src/views/common/MenuView.vue +++ b/ui2/src/views/common/MenuView.vue @@ -1,39 +1,46 @@ <template> - <b-navbar class="menu-view"> - <template #start> - <b-navbar-item tag="router-link" :to="{ path: '/applications' }"> - {{ $t("menu.applications") }} - </b-navbar-item> - </template> - - <template #end> - <b-navbar-item tag="div"> - <div class="buttons"> - <b-button type="is-info" @click="logout" icon-right="sign-out-alt">{{ - $t("menu.logout") - }}</b-button> - </div> - </b-navbar-item> - <b-navbar-item tag="div"> - <b-field> - <b-select - v-model="chosenLocale" - :placeholder="$t('menu.language')" - icon="globe" - @input="setUserPrefLocale" - > - <option :value="locales.FRENCH">{{ $t("menu.french") }}</option> - <option :value="locales.ENGLISH">{{ $t("menu.english") }}</option> - </b-select> - </b-field> - </b-navbar-item> - <b-navbar-item href="https://www.inrae.fr/"> - <img class="logo_blanc" src="@/assets/logo-inrae_blanc.svg" /> - <img class="logo_vert" src="@/assets/Logo-INRAE.svg" /> - </b-navbar-item> - <img class="logo_rep" src="@/assets/Rep-FR-logo.svg" /> - </template> - </b-navbar> + <div class="menu-view-container"> + <b-navbar class="menu-view" v-if="open"> + <template #start> + <b-navbar-item tag="router-link" :to="{ path: '/applications' }"> + {{ $t("menu.applications") }} + </b-navbar-item> + </template> + + <template #end> + <b-navbar-item tag="div"> + <div class="buttons"> + <b-button type="is-info" @click="logout" icon-right="sign-out-alt">{{ + $t("menu.logout") + }}</b-button> + </div> + </b-navbar-item> + <b-navbar-item tag="div"> + <b-field> + <b-select + v-model="chosenLocale" + :placeholder="$t('menu.language')" + icon="globe" + @input="setUserPrefLocale" + > + <option :value="locales.FRENCH">{{ $t("menu.french") }}</option> + <option :value="locales.ENGLISH">{{ $t("menu.english") }}</option> + </b-select> + </b-field> + </b-navbar-item> + <b-navbar-item href="https://www.inrae.fr/"> + <img class="logo_blanc" src="@/assets/logo-inrae_blanc.svg" /> + <img class="logo_vert" src="@/assets/Logo-INRAE.svg" /> + </b-navbar-item> + <img class="logo_rep" src="@/assets/Rep-FR-logo.svg" /> + </template> + </b-navbar> + <FontAwesomeIcon + @click="open = !open" + :icon="open ? 'caret-up' : 'caret-down'" + class="clickable mr-3 menu-view-collapsible-icon" + /> + </div> </template> <script> @@ -43,9 +50,10 @@ import { LoginService } from "@/services/rest/LoginService"; import { UserPreferencesService } from "@/services/UserPreferencesService"; import { Locales } from "@/utils/LocaleUtils.js"; +import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; @Component({ - components: {}, + components: { FontAwesomeIcon }, }) export default class MenuView extends Vue { loginService = LoginService.INSTANCE; @@ -53,6 +61,7 @@ export default class MenuView extends Vue { locales = Locales; chosenLocale = ""; + open = false; created() { this.chosenLocale = this.userPreferencesService.getUserPrefLocale(); @@ -119,4 +128,23 @@ export default class MenuView extends Vue { } } } + +.menu-view-container { + line-height: 0; +} + +.menu-view-collapsible-icon { + width: 100%; + background-color: $primary-slightly-transparent; + height: 30px; + opacity: 0.8; + + &:hover { + opacity: 1; + } + + path { + fill: white; + } +} </style> diff --git a/ui2/src/views/common/PageView.vue b/ui2/src/views/common/PageView.vue index d2d33980dacafdb77698562f90609a4388dedc75..0da05950142cc9ee9275d2d48a619e0e1434db2b 100644 --- a/ui2/src/views/common/PageView.vue +++ b/ui2/src/views/common/PageView.vue @@ -1,7 +1,7 @@ <template> <div class="PageView"> <MenuView v-if="hasMenu" /> - <div class="container PageView-container"> + <div :class="`PageView-container ${hasMenu ? '' : 'noMenu'}`"> <slot></slot> </div> </div> @@ -32,9 +32,21 @@ export default class PageView extends Vue { <style lang="scss" scoped> .PageView { height: 100%; + &.with-submenu { + .PageView-container { + padding-top: 0rem; + } + } } .PageView-container { width: 100%; + height: calc(100% - #{$menu-height}); + padding: $container-padding-vert $container-padding-hor; + position: relative; + + &.noMenu { + height: 100%; + } } </style> diff --git a/ui2/src/views/references/ReferenceTableView.vue b/ui2/src/views/references/ReferenceTableView.vue new file mode 100644 index 0000000000000000000000000000000000000000..ac40540392a41e616205d67dc7c50eaa1b2a10f2 --- /dev/null +++ b/ui2/src/views/references/ReferenceTableView.vue @@ -0,0 +1,121 @@ +<template> + <PageView class="with-submenu"> + <SubMenu :root="application.title" :paths="subMenuPaths" /> + <h1 class="title main-title"> + {{ $t("titles.references-data", { refName: reference.label }) }} + </h1> + + <div v-if="reference && columns"> + <b-table + :data="tableValues" + :striped="true" + :isFocusable="true" + :isHoverable="true" + :sticky-header="true" + :paginated="true" + :per-page="15" + height="100%" + > + <b-table-column + v-for="column in columns" + :key="column.id" + :field="column.id" + :label="column.title" + sortable + :sticky="column.key" + v-slot="props" + > + {{ props.row[column.id] }} + </b-table-column> + </b-table> + </div> + </PageView> +</template> + +<script> +import SubMenu, { SubMenuPath } from "@/components/common/SubMenu.vue"; +import { ApplicationResult } from "@/model/ApplicationResult"; +import { AlertService } from "@/services/AlertService"; +import { ApplicationService } from "@/services/rest/ApplicationService"; +import { ReferenceService } from "@/services/rest/ReferenceService"; +import { Prop, Vue, Component } from "vue-property-decorator"; +import PageView from "../common/PageView.vue"; + +@Component({ + components: { PageView, SubMenu }, +}) +export default class ReferenceTableView extends Vue { + @Prop() applicationName; + @Prop() refId; + + alertService = AlertService.INSTANCE; + applicationService = ApplicationService.INSTANCE; + referenceService = ReferenceService.INSTANCE; + + application = new ApplicationResult(); + subMenuPaths = []; + reference = {}; + columns = []; + referenceValues = []; + tableValues = []; + + async created() { + await this.init(); + this.setInitialVariables(); + } + + async init() { + try { + this.application = await this.applicationService.getApplication(this.applicationName); + const references = await this.referenceService.getReferenceValues( + this.applicationName, + this.refId + ); + if (references) { + this.referenceValues = references.referenceValues; + } + } catch (error) { + this.alertService.toastServerError(); + } + } + + setInitialVariables() { + if (!this.application || !this.application.references) { + return; + } + + this.reference = Object.values(this.application.references).find( + (ref) => ref.id === this.refId + ); + + this.subMenuPaths = [ + new SubMenuPath(this.$t("referencesManagement.references").toLowerCase(), () => + this.$router.push(`/applications/${this.applicationName}/references`) + ), + new SubMenuPath(this.reference.label, () => + this.$router.push(`/applications/${this.applicationName}/references/${this.refId}`) + ), + ]; + + if (this.reference && this.reference.columns) { + this.columns = Object.values(this.reference.columns).sort((c1, c2) => { + if (c1.title < c2.title) { + return -1; + } + + if (c1.title > c2.title) { + return 1; + } + return 0; + }); + } + + console.log(this.columns); + + if (this.referenceValues) { + this.tableValues = Object.values(this.referenceValues).map((refValue) => refValue.values); + console.log(this.tableValues); + } + } +} +</script> diff --git a/ui2/src/views/references/ReferencesManagementView.vue b/ui2/src/views/references/ReferencesManagementView.vue new file mode 100644 index 0000000000000000000000000000000000000000..370caf82f7a4765cd49c7ef1b776e08ae4b72e9d --- /dev/null +++ b/ui2/src/views/references/ReferencesManagementView.vue @@ -0,0 +1,111 @@ +<template> + <PageView class="with-submenu"> + <SubMenu :root="application.title" :paths="subMenuPaths" /> + <h1 class="title main-title"> + {{ $t("titles.references-page", { applicationName: application.title }) }} + </h1> + <div> + <CollapsibleTree + v-for="ref in references" + :key="ref.id" + :label="ref.label" + :children="ref.children" + :level="0" + :onClickLabelCb="(event, label) => openRefDetails(event, label)" + :onUploadCb="(label, refFile) => uploadReferenceCsv(label, refFile)" + :buttons="buttons" + /> + <ReferencesDetailsPanel + :leftAlign="false" + :open="openPanel" + :reference="chosenRef" + :closeCb="(newVal) => (openPanel = newVal)" + /> + </div> + </PageView> +</template> + +<script> +import { Component, Prop, Vue } from "vue-property-decorator"; +import { convertReferencesToTrees } from "@/utils/ConversionUtils"; +import CollapsibleTree from "@/components/common/CollapsibleTree.vue"; +import ReferencesDetailsPanel from "@/components/references/ReferencesDetailsPanel.vue"; +import { ApplicationService } from "@/services/rest/ApplicationService"; +import { ReferenceService } from "@/services/rest/ReferenceService"; + +import PageView from "../common/PageView.vue"; +import { ApplicationResult } from "@/model/ApplicationResult"; +import SubMenu, { SubMenuPath } from "@/components/common/SubMenu.vue"; +import { AlertService } from "@/services/AlertService"; +import { Button } from "@/model/Button"; + +@Component({ + components: { CollapsibleTree, ReferencesDetailsPanel, PageView, SubMenu }, +}) +export default class ReferencesManagementView extends Vue { + @Prop() applicationName; + + applicationService = ApplicationService.INSTANCE; + referenceService = ReferenceService.INSTANCE; + alertService = AlertService.INSTANCE; + + references = []; + openPanel = false; + chosenRef = null; + application = new ApplicationResult(); + subMenuPaths = []; + buttons = [ + new Button( + this.$t("referencesManagement.consult"), + "eye", + (label) => this.consultReference(label), + "is-primary" + ), + new Button(this.$t("referencesManagement.download"), "download"), + ]; + + created() { + this.subMenuPaths = [ + new SubMenuPath(this.$t("referencesManagement.references").toLowerCase(), () => + this.$router.push(`/applications/${this.applicationName}/references`) + ), + ]; + this.init(); + } + + async init() { + try { + this.application = await this.applicationService.getApplication(this.applicationName); + if (!this.application || !this.application.id) { + return; + } + this.references = convertReferencesToTrees(Object.values(this.application.references)); + } catch (error) { + this.alertService.toastServerError(); + } + } + + openRefDetails(event, label) { + event.stopPropagation(); + this.openPanel = this.chosenRef && this.chosenRef.label === label ? !this.openPanel : true; + this.chosenRef = Object.values(this.application.references).find((ref) => ref.label === label); + } + + consultReference(label) { + const ref = Object.values(this.application.references).find((ref) => ref.label === label); + if (ref) { + this.$router.push(`/applications/${this.applicationName}/references/${ref.id}`); + } + } + + async uploadReferenceCsv(label, refFile) { + const reference = Object.values(this.application.references).find((ref) => ref.label === label); + try { + await this.referenceService.createReference(this.applicationName, reference.id, refFile); + this.alertService.toastSuccess(this.$t("alert.reference-updated")); + } catch (error) { + this.alertService.toastError(this.$t("alert.reference-csv-upload-error"), error); + } + } +} +</script>