Commit 0a6f063e authored by Jean Rabreau's avatar Jean Rabreau
Browse files

🔀Merge branch 'feature/teleport' into develop

parents 8f936912 df9023b4
Pipeline #62967 passed with stages
in 3 minutes and 56 seconds
......@@ -3,7 +3,7 @@ module.exports = {
env: {
node: true
},
'extends': [
extends: [
'plugin:vue/essential',
'eslint:recommended'
],
......
This diff is collapsed.
......@@ -6,13 +6,15 @@
<v-toolbar-title>{{ title }}</v-toolbar-title>
<v-spacer />
<v-btn
v-if="button.isDisplayed"
v-for="button in buttons"
:key="button.label"
tile
elevation="0"
color="secondary"
:to="button.to"
class="mr-2"
v-bind="button"
>
{{ button.text }}
{{ button.label }}
</v-btn>
</v-toolbar>
<v-divider />
......@@ -45,8 +47,8 @@
<div class="d-flex pa-2">
<div
class="d-flex flex-grow-1"
:class="item.to && item.to.length ? 'pointer': ''"
@click="item.to && item.to.length ? $router.push(item.to) : undefined"
:class="item.to ? 'pointer': ''"
@click="item.to ? $router.push(item.to) : undefined"
>
<div class="align-self-center mr-4">
<avatar
......@@ -143,9 +145,9 @@ export default {
type: String,
required: true,
},
button: {
type: Object,
required: true,
buttons: {
type: Array,
default: () => [],
},
items: {
type: Array,
......
......@@ -233,7 +233,33 @@ export const en_US = {
},
item: {
backToSet: "Back to set",
copyOf: "Copy of",
copy: {
inTarget: "in this set : \"{values}\"",
toSet: "Into \"{title}\" set",
move: "Move @:item.designate",
duplicate: "Create a copy",
title: "Copy @:item.designate",
here: "Copy here",
ignored: {
field : "This field will be ignored. | Those fields will be ignored.",
value: `This item value will be ignored, field is missing in this set.
| Those item values will be ignored, fields are missing in this set.`,
},
impossible: "Copy impossible, ",
required: `@:item.copy.impossible this field is required.
| @:item.copy.impossible those fields are required.`,
partial: "Warning: Only partial copy available.",
inProject: "Choose set",
projectsLoad: "Load other projets",
prefix: "Copy of ",
reason: {
unknownOption: `This option is unknown @:item.copy.inTarget
| Those options are unknown @:item.copy.inTarget`,
typeMismatch: "Target field type do not match with item data",
tooManyValues: "This field receive tow many values, field limit : {limit}",
missingValue: "Missing data."
}
},
count: "No item | 1 item | {count} items",
create: "Add item",
submitted: "Submitted by:",
......@@ -244,16 +270,20 @@ export const en_US = {
detail: "@:item.label @.lower:detail",
edit: "Item edition",
export: "Datacite export",
import: {
button: "CSV import",
success: "\"{filename}\" import succeed !",
created: "@:item.count ajouté à l'ensemble. | @:item.count ajoutés à l'ensemble.",
error: "Items haven't been created from file \"{filename}\".",
launch: "Launch"
},
file: "File",
files: "Files",
label: "Item",
metadata: "Metadata",
none: "No item.",
plural: "Items",
shareLink: "Visualize the @.lower:item.label in Pount",
update: "Update item",
updated: "The item has been updated",
viewer: "Viewer",
fields: {
title: {
label: "Title",
......@@ -306,6 +336,13 @@ export const en_US = {
empty: "None",
},
},
tabs: {
Copy: 'Copy',
Detail: "@:detail",
Files: "Files",
Metadata: "Metadata",
Viewer: "Viewer",
},
},
legal: {
headline: "Legal Informations",
......
......@@ -240,10 +240,36 @@ export const fr_FR = {
},
item: {
backToSet: "Revenir Ă  l'ensemble",
copyOf: "Copie de",
copy: {
inTarget: "dans cet ensemble : \"{values}\"",
toSet: "Vers le set \"{title}\"",
move: "DĂ©placer @:item.designate",
duplicate: "Créer une copie",
title: "Copier @:item.designate",
here: "Copier ici",
ignored: {
field : "Ce champ sera ignoré. | Ces champs seront ignorés.",
value: `Cette donnée de l'élément sera ignorée, le champ est manquant dans cet ensemble.
| Ces données de l'élément seront ignorées, les champs sont manquant dans cet ensemble.`,
},
impossible: "La copie est impossible, ",
required: `@:item.copy.impossible ce champ est requis.
| @:item.copy.impossible ces champs sont requis.`,
partial: "Attention : Seule une copie partielle est possible.",
inProject: "Choisir l'ensemble",
projectsLoad: "Charger les autres projets",
prefix: "Copie de ",
reason: {
unknownOption: `Cette option est inconnue @:item.copy.inTarget
| Ces options sont inconnues @:item.copy.inTarget`,
typeMismatch: "Le type du champ cible ne correspond pas avec la donnée de l'élément",
tooManyValues: "Ce champ reçoit trop de données, maximum : {limit}",
missingValue: "Donnée(s) manquante(s)"
}
},
count: "Aucun élément | 1 élément | {count} éléments",
create: "Nouvel élément",
submitted: "Déposé par :",
submitted: "Déposé par : ",
description: "Présentation",
designate: "cet élément",
destroy: "Supprimer l'élément",
......@@ -251,16 +277,20 @@ export const fr_FR = {
detail: "@:detail de l'@.lower:item.label ",
edit: "Editer l'élément",
export: "Export Datacite",
import: {
button: "Import CSV",
success: "Import du fichier \"{filename}\" réussi !",
created: "@:item.count ajoutés à l'ensemble.",
error: "Les objets n'ont pas pu être créés à partir du fichier \"{filename}\".",
launch: "Lancer l'import"
},
file: "Fichier",
files: "Fichiers",
label: "Élément",
metadata: "Métadonnées",
none: "Aucun élément.",
plural: "Éléments",
shareLink: "Visualiser l'@.lower:item.label dans Pount",
update: "Modifier l'élément",
updated: "L'élément a bien été mis à jour",
viewer: "Visionneuse",
fields: {
title: {
label: "Titre",
......@@ -313,6 +343,13 @@ export const fr_FR = {
empty: "Non versionné",
},
},
tabs: {
Copy: 'Copie',
Detail: "@:detail",
Files: "Fichiers",
Metadata: "Métadonnées",
Viewer: "Visionneuse",
}
},
legal: {
headline: "Mentions légales",
......
......@@ -276,6 +276,11 @@ export const routes = [
name: 'setItemCreate',
component: () => import(/* webpackChunkName "set" */ '@/views/items/ItemCreate'),
},
{
path: 'items/import',
name: 'setItemImport',
component: () => import(/* webpackChunkName "set" */ '@/views/items/ItemCsvImport'),
},
{
path: 'template/:templateId',
name: 'setTemplate',
......@@ -295,28 +300,39 @@ export const routes = [
{
path: 'detail',
name: 'itemDetail',
props: true,
component: () => import(/* webpackChunkName: "item" */ '@/views/items/ItemDetail'),
},
{
path: 'detail/edit',
name: 'itemEdit',
props: true,
component: () => import(/* webpackChunkName: "item" */ '@/views/items/ItemEdit'),
},
{
path: 'viewer',
name: 'itemViewer',
props: true,
component: () => import(/* webpackChunkName: "item" */ '@/views/items/ItemViewer'),
},
{
path: 'metadata',
name: 'itemMetadata',
props: true,
component: () => import(/* webpackChunkName: "item" */ '@/views/items/ItemMetadata'),
},
{
path: 'files',
name: 'itemFiles',
props: true,
component: () => import(/* webpackChunkName: "item" */ '@/views/items/ItemFiles'),
},
{
path: 'copy',
name: 'itemCopy',
props: true,
component: () => import(/* webpackChunkName: "item" */ '@/views/items/ItemCopy'),
},
],
},
{
......
......@@ -32,11 +32,15 @@ import {
* @property {string} url - s3 url
*/
/**
* @typedef {string|string[]|Array<string[]>} ItemValue
*/
/**
* @typedef {Object} MetaData
* @property {string} id
* @property {string} name
* @property {string|string[]} value
* @property {ItemValue} value
* @property {boolean} multiple
* @property {boolean} isOption
* @property {boolean} repeatable
......@@ -70,7 +74,7 @@ import {
* @property {boolean} canEdit - true if user is project manager or creator of this item
* @property {boolean} canBeShared - true if itself, its parents set and project are public
* @property {ItemCreator} creator
* @property {string} description
* @property {string | null} description
* @property {string} [id]
* @property {boolean} isPublic
* @property {MediaFile[]} mediaFiles
......@@ -78,7 +82,7 @@ import {
* @property {Object} set - parent set details
* @property {ThumbnailSource} thumbnails - urls of thumbnails
* @property {string} title - 255 chars max
* @property {FileViewer} viewer - viewer parameters
* @property {FileViewer | null} viewer - viewer parameters
*/
const initialItem = {
......@@ -143,8 +147,11 @@ const Item = {
getters: {
current: state => state.current,
itemId: state => state.current.id,
set: state => state.current.set,
templateId: state => state.current.set.templateId,
canEdit: (state) => state.current.canEdit,
projectId: state => state.current.set.projectId,
canEdit: state => state.current.canEdit,
metadata: state => state.current.metadata,
files: state => state.current.mediaFiles,
fileView: state => state.current.viewer.fileId
? state.current.mediaFiles.find(f => f.id === state.current.viewer.fileId)
......@@ -327,25 +334,48 @@ const Item = {
{root: true}
))
},
itemCSVImport({rootGetters}, file) {
const setId = rootGetters['set/setId']
const formData = new FormData()
formData.append("Content-Type", file.type)
formData.append("csv", file)
formData.append("set_id", setId)
return authority.post(
`items/import_csv/`,
formData
).then(response => response.data.createdItems)
},
/**
* Create a copy of current item in the same set
* changing its title and resetting associated files
*
* @param context
* @param {Object} [target]
* @param {string} target.setId
* @param {Metadata} target.metadata
* @returns {Promise<void | *>}
*/
itemDuplicate({commit, dispatch, getters}) {
const {title: oldTitle, description, metadata, set: {id: setId}} = getters['current']
const prefix = i18n.t('item.copyOf')
const title = `${prefix} ${oldTitle}`
itemDuplicate({commit, dispatch, getters}, target) {
const item = getters['current']
const {title: oldTitle, description} = item
const title = `${i18n.t('item.copy.prefix')}${oldTitle}`
const metadata = (target
? target.metadata
: item.metadata)
.map(d => d.name === 'title'
? { ...d, value: title}
: d
)
const setId = target ? target.setId : item.set.id
if (setId === undefined) {
throw new TypeError('setId is mandatory, see no orphan item policy')
}
const payload = {
title,
description,
metadata: metadata.map(
d => d.name === 'title'
? { ...d, value: title}
: d
),
metadata,
viewer: {},
setId,
}
......
......@@ -190,8 +190,12 @@ const Project = {
{root: true}
))
},
retrieveProjects({commit, dispatch}, {page = 1, size}) {
return authority.get( `projects/?page=${page}&page_size=${size > 0 ? size : 0}`)
retrieveProjects({commit, dispatch}, {manageOnly= false, page = 1, size}) {
let url = `projects/?page=${page}&page_size=${size > 0 ? size : 0}`
if (manageOnly) {
url += '&is_manager=true'
}
return authority.get(url)
.then(response => {
const [projects, pageCount, count] = getPaginatedResult(response.data, page, size)
commit('SET_CONTRIBUTIONS', projects)
......
......@@ -167,8 +167,9 @@ const Set = {
{root: true}
))
},
loadProjectSets({commit, dispatch, rootGetters}, {page = 1, size}) {
return authority.get(`sets/?project_id=${rootGetters['project/projectId']}&page=${page}&page_size=${size > 0 ? size : 0}`)
loadProjectSets({commit, dispatch, rootGetters}, {projectId, page = 1, size}) {
const projectFilter = projectId || rootGetters['project/projectId']
return authority.get(`sets/?project_id=${projectFilter}&page=${page}&page_size=${size > 0 ? size : 0}`)
.then(response => {
const [sets, pageCount, count] = getPaginatedResult(response.data, page, size)
commit('SET_PROJECT_SETS', sets)
......
......@@ -3,27 +3,30 @@ import i18n from '@/plugins/vue-i18n'
/**
* @typedef {Object} UsfLayout
* @typedef {Object} UsfFieldOption
* @property {string} id
* @property {boolean} [exist]
* @property {boolean} visible
* @property {boolean} [vertical]
* @property {boolean} [width]
* @property {string} component
* @property {string} parentId
* @property {UsfField[]} fields
*/
/**
* @typedef {Object} UsfInput
* @property {string} id
* @property {boolean} exist
* @property {boolean} visible
* @property {string} label
* @property {string} owner
* @property {string} value
*/
/**
* @typedef {UsfInput|UsfLayout} UsfField
* @typedef {Object} UsfField
* @property {string} id -
* @property {string} name -
* @property {string} component - "Usf" prefixed form builder name
* @property {boolean} exist -
* @property {boolean} visible -
* @property {string} parentId - parent id or "root"
* @property {UsfField[]} [fields] - layout children
* @property {UsfFieldOption[]} [options]
* @property {boolean} [repeatable] -
* @property {boolean} [maxRepeat] -
* @property {boolean} [multiple] -
* @property {boolean} [required] -
* @property {boolean} [vertical] - row direction
* @property {boolean} [width] - col width
*/
......@@ -32,12 +35,12 @@ import i18n from '@/plugins/vue-i18n'
* @property {string} id
* @property {string} name
* @property {number} scope
* @property {Object} creator
* @property {Object} project
* @property {Object | null} creator
* @property {Object} [project]
* @property {UsfField[]} fields
* @property {Object} messages
* @property {String} createdAt
* @property {String} updatedAt
* @property {String} [createdAt]
* @property {String} [updatedAt]
*/
/**
......@@ -73,6 +76,23 @@ const getPrivateFields = (fields, privateParent=false) => {
}, [])
}
/**
*
* @param {UsfField[]} fields
* @return {UsfField[]}
*/
const getInputFields = (fields) => {
return fields.reduce((names, field) => {
if (field.name) {
names.push(field)
}
if (field.fields) {
names.push(...getInputFields(field.fields))
}
return names
}, [])
}
export { TEMPLATE_SCOPE, LAYOUT_COMPONENTS, getPrivateFields }
......@@ -87,10 +107,10 @@ const Template = {
id: '',
name: '',
scope: TEMPLATE_SCOPE.SET,
fields: [{}],
creator: '',
fields: [],
creator: null,
messages: {
default: 'fr',
defaultLocale: 'fr',
locales: {
fr: {}
}
......@@ -115,6 +135,7 @@ const Template = {
},
getters: {
current: state => state.current,
inputFields: state => getInputFields(state.current.fields),
projectList: state => state.projectList,
publicList: state => state.publicList,
templateList: state => state.templateList,
......@@ -135,10 +156,6 @@ const Template = {
ADD_PROJECT_TEMPLATE(state, template) {
state.projectList = [ ...state.projectList, template]
},
UPDATE_PROJECT_TEMPLATE(state, template) {
const templateIndex = state.projectList.findIndex(t => t.id === template.id)
state.projectList.splice(templateIndex, 1, template)
},
},
actions: {
/**
......@@ -290,12 +307,7 @@ const Template = {
privateFields: getPrivateFields(templatePatch.fields)
})
.then(response => {
const template = response.data
if (template.scope === TEMPLATE_SCOPE.SET) {
commit('SET_CURRENT', response.data)
} else if (template.scope === TEMPLATE_SCOPE.PROJECT) {
commit('UPDATE_PROJECT_TEMPLATE', template)
}
commit('SET_CURRENT', response.data)
return dispatch(
'dialog/displayInfo',
i18n.t('template.updated'),
......
......@@ -2,7 +2,7 @@
<v-container>
<crud-list
:title="$t('project.plural')"
:button="crudListButton"
:buttons="crudListButtons"
:items="crudListItems"
:items-length="projectsLength"
:show-select="isCreator"
......@@ -32,12 +32,11 @@ export default {
pages: (state) => state.pageCount,
}),
...mapGetters('contrib', ['isCreator']),
crudListButton() {
return {
isDisplayed: this.isCreator,
to: '/projects/create',
text: this.$t('project.create'),
};
crudListButtons() {
return this.isCreator ? [{
to: {name: 'projectCreate'},
label: this.$t('project.create'),
}]: [];
},
crudListItems() {
return this.projects.map((project) => {
......@@ -63,7 +62,10 @@ export default {
label: isManager ? this.$t('project.manager') : this.$t('project.contributor'),
},
updatedAt,
to: `/projects/${id}/sets`,
to: {
name: 'projectSets',
params: {projectId: id}
},
}
});
},
......
......@@ -109,32 +109,17 @@ export default {
},
tabs() {
const tabs = [
{
id: 'detail',
label: this.$t('detail'),
to: `/items/${this.item.id}/detail`,
isDisplayed: true,
},
{
id: 'viewer',
label: this.$t('item.viewer'),
to: `/items/${this.item.id}/viewer`,
isDisplayed: true,
},
{
id: 'metadata',
label: this.$t('item.metadata'),
to: `/items/${this.item.id}/metadata`,
isDisplayed: this.item.canEdit,
},
{
id: 'files',
label: this.$t('item.files'),
to: `/items/${this.item.id}/files`,
isDisplayed: this.item.canEdit,
},
];
return tabs.filter((tab) => tab.isDisplayed);
'Detail',
'Viewer',