Commit 6f3fdc32 authored by Julien BOUYER's avatar Julien BOUYER
Browse files

Merge branch 'feature/UNOTOPLYS-171_reordonner_ecrans' into 'develop'

UNOTOPLYS-171 feat(ordre écran) : réordonner les écrans;

See merge request !64
parents 951f85fb f1e0bd1f
This diff is collapsed.
...@@ -135,6 +135,11 @@ ...@@ -135,6 +135,11 @@
<artifactId>hibernate-types-52</artifactId> <artifactId>hibernate-types-52</artifactId>
<version>${hibernate-types.version}</version> <version>${hibernate-types.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
<version>1.13</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
...@@ -389,6 +394,10 @@ ...@@ -389,6 +394,10 @@
<groupId>com.vladmihalcea</groupId> <groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId> <artifactId>hibernate-types-52</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>
......
...@@ -106,10 +106,34 @@ public interface ScreenRepository extends JpaRepository<Screen, Long> { ...@@ -106,10 +106,34 @@ public interface ScreenRepository extends JpaRepository<Screen, Long> {
* @param formId un identifiant de formulaire * @param formId un identifiant de formulaire
* @param index un index (position) d'écran * @param index un index (position) d'écran
*/ */
@Query( @Query(value = "UPDATE screen SET index = (index - 1) WHERE index > :index AND form_id = :formId", nativeQuery = true)
value = "UPDATE screen SET index = (index - 1) WHERE index > :index AND form_id = :formId",
nativeQuery = true
)
@Modifying(flushAutomatically = true, clearAutomatically = true) @Modifying(flushAutomatically = true, clearAutomatically = true)
void decreaseScreenIndex(@Param("formId") Long formId, @Param("index") Integer index); void decreaseScreenIndex(@Param("formId") Long formId, @Param("index") Integer index);
/**
* Déplace vers le bas tous les écrans dont l'index est >= nouvel index et < ancien index
* @param oldIndex l'ancienne position de l'écran
* @param newIndex la nouvelle position de l'écran
*/
@Query(value = "UPDATE screen SET index = index + 1 WHERE index >= :newIndex AND index < :oldIndex", nativeQuery = true)
@Modifying
void moveUp(@Param("oldIndex") Integer oldIndex, @Param("newIndex") Integer newIndex);
/**
* Déplace vers le haut tous les écrans dont l'index est > ancien index et <= nouvel index
* @param oldIndex l'ancienne position de l'écran
* @param newIndex la nouvelle position de l'écran
*/
@Query(value = "UPDATE screen SET index = index - 1 WHERE index > :oldIndex AND index <= :newIndex", nativeQuery = true)
@Modifying
void moveDown(@Param("oldIndex") Integer oldIndex, @Param("newIndex") Integer newIndex);
/**
* Change l'index
* @param id l'id d'un écran
* @param newIndex son nouvel index
*/
@Query(value = "UPDATE screen SET index = :newIndex WHERE id = :id", nativeQuery = true)
@Modifying(flushAutomatically = true, clearAutomatically = true)
void updateIndex(@Param("id") Long id, @Param("newIndex") Integer newIndex);
} }
...@@ -12,6 +12,11 @@ import org.springframework.security.access.AccessDeniedException; ...@@ -12,6 +12,11 @@ import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.fge.jsonpatch.JsonPatch;
import com.github.fge.jsonpatch.JsonPatchException;
import com.unantes.orientactive.domain.Screen; import com.unantes.orientactive.domain.Screen;
import com.unantes.orientactive.domain.User; import com.unantes.orientactive.domain.User;
import com.unantes.orientactive.repository.ScreenRepository; import com.unantes.orientactive.repository.ScreenRepository;
...@@ -213,4 +218,55 @@ public class ScreenService { ...@@ -213,4 +218,55 @@ public class ScreenService {
public Optional<ScreenDTO> findScreenByIndexAndFormId(final Integer index, final Long formId) { public Optional<ScreenDTO> findScreenByIndexAndFormId(final Integer index, final Long formId) {
return screenRepository.findScreenByIndexAndFormId(index, formId).map(screenMapper::toDto); return screenRepository.findScreenByIndexAndFormId(index, formId).map(screenMapper::toDto);
} }
/**
* Modifie les élements de l'écran ciblés par le patch. Si l'index est modifié : met à jour les écrans impactés.
* @param id un identifiant d'écran en base de donnée
* @param patch le patch à appliquer
* @return
*/
public ScreenDTO patch(final Long id, final JsonPatch patch) {
final ScreenDTO oldScreen = findOne(id).orElseThrow(() -> new EntityNotFoundException(id));
final ObjectMapper objectMapper = new ObjectMapper();
final JsonNode screenNode = objectMapper.convertValue(oldScreen, JsonNode.class);
try {
final JsonNode nodePatched = patch.apply(screenNode);
final ScreenDTO patchedScreen = objectMapper.treeToValue(nodePatched, ScreenDTO.class);
updateScreenOrder(oldScreen, patchedScreen);
return patchedScreen;
} catch (JsonPatchException | JsonProcessingException e) {
throw new IllegalStateException(e);
}
}
/**
* Met à jour si nécessaire l'ordre d'un écran et ceux des écrans impactés
* @param oldScreen l'ancien écran
* @param patchedScreen l'écran modifié
*/
public void updateScreenOrder(final ScreenDTO oldScreen, final ScreenDTO patchedScreen) {
final Integer newIndex = patchedScreen.getIndex();
final Integer oldIndex = oldScreen.getIndex();
if (!newIndex.equals(oldIndex)) {
updateScreenOrder(oldScreen.getId(), oldIndex, newIndex);
}
}
/**
* Met à jour l'ordre d'un écran et ceux des écrans impactés
* @param id l'ancien écran
* @param oldIndex l'écran modifié
* @param newIndex l'écran modifié
*/
private void updateScreenOrder(final Long id, final Integer oldIndex, final Integer newIndex) {
if (oldIndex < newIndex) {
screenRepository.moveDown(oldIndex, newIndex);
screenRepository.updateIndex(id, newIndex);
} else if (oldIndex > newIndex) {
screenRepository.moveUp(oldIndex, newIndex);
screenRepository.updateIndex(id, newIndex);
} else {
log.debug("L'index de l'écran id = {} n'a pas été modifié", id);
}
}
} }
...@@ -7,6 +7,7 @@ import javax.validation.constraints.NotNull; ...@@ -7,6 +7,7 @@ import javax.validation.constraints.NotNull;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.unantes.orientactive.converter.bean.Item; import com.unantes.orientactive.converter.bean.Item;
import com.unantes.orientactive.validation.CheckJson; import com.unantes.orientactive.validation.CheckJson;
...@@ -52,6 +53,7 @@ public class ScreenDTO implements Serializable { ...@@ -52,6 +53,7 @@ public class ScreenDTO implements Serializable {
private List<NextScreenExpressionDTO> nextScreenExpressions; private List<NextScreenExpressionDTO> nextScreenExpressions;
@JsonIgnore
public Integer getNextIndex() { public Integer getNextIndex() {
return index + 1; return index + 1;
} }
......
package com.unantes.orientactive.service.dto;
/**
* Représente l'index d'un écran
*/
public class ScreenIndexDTO {
private Long id;
private int index;
public int getIndex() {
return index;
}
public void setIndex(final int index) {
this.index = index;
}
public Long getId() {
return id;
}
public void setId(final Long id) {
this.id = id;
}
}
package com.unantes.orientactive.web.rest; package com.unantes.orientactive.web.rest;
import com.github.fge.jsonpatch.JsonPatch;
import com.unantes.orientactive.domain.User; import com.unantes.orientactive.domain.User;
import com.unantes.orientactive.navigation.exception.ScreenNotFoundException; import com.unantes.orientactive.navigation.exception.ScreenNotFoundException;
import com.unantes.orientactive.service.ScreenService; import com.unantes.orientactive.service.ScreenService;
...@@ -14,6 +15,7 @@ import org.springframework.http.ResponseEntity; ...@@ -14,6 +15,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
...@@ -108,6 +110,11 @@ public class ScreenResource { ...@@ -108,6 +110,11 @@ public class ScreenResource {
.body(result); .body(result);
} }
@PatchMapping(path = "/screens/{id}", consumes = "application/json-patch+json")
public ResponseEntity<ScreenDTO> patch(@PathVariable Long id, @RequestBody JsonPatch patch) {
return ResponseEntity.ok().body(screenService.patch(id, patch));
}
/** /**
* {@code GET /screens/:id} : get the "id" screen. * {@code GET /screens/:id} : get the "id" screen.
* *
......
import Vue from 'vue';
import { Component, Prop } from 'vue-property-decorator';
import { IScreen } from '@/shared/model/screen.model';
/**
* Composant d'affichage d'une liste de IScreen avec possibilité de déplacer les éléments par drag-n-drop.
*/
@Component
export default class DndList extends Vue {
@Prop()
public screenList: any[];
@Prop()
public idScreen: number;
private currentList: any[];
constructor() {
super();
// La liste passée en prop n'est pas utilisée directement car le dnd doit pouvoir la mettre à jour.
// On la copie dans une data qui est bindée à la vue par le méthode set et get
this.currentList = this.screenList;
}
public set list(screenList: any[]) {
this.currentList = screenList;
}
public get list(): any[] {
return this.currentList;
}
public get options() {
return {
dropzoneSelector: 'ol',
draggableSelector: 'li',
excludeOlderBrowsers: true,
showDropzoneAreas: true,
};
}
public isCurrentPanel(panel: any): boolean {
return this.idScreen == panel.id;
}
/**
* Emet un évènement à la sélection d'un Screen indiquant .
* @param {IScreen} panelScreen écran sélectionné dans le panneau
* @returns {void}
*/
public selectScreen(panelScreen: IScreen): void {
this.$emit('selectScreen', panelScreen);
}
/**
* Méthode appelée après le drag-n-drop d'un élément sur sa nouvelle position.
* Calcule le nouvel index de l'élément et émet un événement indiquant le nouvel index pour l'écran.
* @param {CustomEvent} e événement émis après le déplacement d'un évènement
*/
public reordered(e: CustomEvent) {
const selectedElm = e.detail.items[0];
const screenIdx = this.screenList.findIndex(s => s.id === Number.parseInt(selectedElm.getAttribute('data-screenid'), 10));
const screen = this.screenList[screenIdx];
// L'index donné est celui dans le tableau des screens (0 à n), or à l'affichage, on démarre à 1
let newIndex = e.detail.index;
if (newIndex <= screenIdx) {
// Si le nouvel index est inférieur à l'index courant (déplacement vers le haut),
// il est donc nécessaire de l'incrémenté de 1 (position de départ).
// Lors d'un déplacement vers le bas, l'index est déjà incrémenté de 1
// (l'écran en cours de déplacement est toujours présent dans la liste)
newIndex++;
}
this.$emit('update', { screen, newIndex });
}
}
<template>
<div v-drag-and-drop:options="options">
<ol class="p-2 text-sm select-none" @reordered="reordered">
<li
v-for="panelScreen in list"
:key="panelScreen.index"
:class="[
'flex items-center p-1 leading-tight transition rounded-md cursor-pointer group hover:absolute focus:ring ',
isCurrentPanel(panelScreen) ? 'font-medium text-white bg-blue-600 hover:bg-blue-700' : 'hover:bg-blue-100',
]"
tabindex="0"
:data-screenid="panelScreen.id"
role="button"
@click="selectScreen(panelScreen)"
>
<span
:class="[
'flex-shrink-0 inline-block w-8 h-6 mr-2 text-sm text-center border rounded-md',
isCurrentPanel(panelScreen)
? 'text-blue-900 bg-blue-100 border-white group-hover:border-blue-900-400'
: 'bg-white border-gray-300 group-hover:border-gray-400',
]"
>{{ panelScreen.index }}</span
>{{ panelScreen.titleBo }}
</li>
</ol>
</div>
</template>
<script lang="ts" src="./dnd-list.component.ts" />
import Vue from 'vue'; import Vue from 'vue';
import { Component, Inject } from 'vue-property-decorator'; import { Component, Inject, Watch } from 'vue-property-decorator';
import ScreenService from '@/entities/screen/screen.service';
import { IScreen } from '@/shared/model/screen.model'; import { IScreen } from '@/shared/model/screen.model';
import ScreenService from '@/entities/screen/screen.service';
import DndList from '@/components/dnd-list/dnd-list.vue';
import Confirm from '@/components/confirm/confirm.vue'; import Confirm from '@/components/confirm/confirm.vue';
import Icon from '@/components/icon/icon.vue'; import Icon from '@/components/icon/icon.vue';
@Component({ @Component({
components: { components: {
DndList,
Confirm, Confirm,
Icon, Icon,
}, },
...@@ -16,19 +18,37 @@ import Icon from '@/components/icon/icon.vue'; ...@@ -16,19 +18,37 @@ import Icon from '@/components/icon/icon.vue';
export default class Panel extends Vue { export default class Panel extends Vue {
@Inject('screenService') private screenService: () => ScreenService; @Inject('screenService') private screenService: () => ScreenService;
public screenList: any[];
/**
* Clé de MAJ du composant DND. On l'incrémente à chaque mise à jour de
* screenList pour indiquer au composant qu'il doit se recharger.
*/
public dndKey: number;
public showConfirm: boolean; public showConfirm: boolean;
constructor() { constructor() {
super(); super();
// Si getter screenList est déjà prêt, on le copie, sinon on charge une liste vide
this.screenList = this.$store.getters.screenList || [];
this.dndKey = 0;
this.showConfirm = false; this.showConfirm = false;
} }
public get screenList(): boolean { /**
return this.$store.getters.screenList; * Surveille la MAJ du getter screenList et met à jour la data screenList à chaque modification.
* Incrémente dndKey pour forcer le composant DndList à se réfraîchir.
*/
@Watch('$store.getters.screenList')
public reloadScreenList() {
this.screenList = this.$store.getters.screenList;
this.dndKey += 1;
} }
public isCurrentPanel(panel) { public updateScreenIndex({ screen, newIndex }) {
return this.idScreen == panel.id; this.$store.dispatch('updateScreenIndex', { screen, newIndex });
this.dndKey += 1;
} }
public get idWorkspace(): string { public get idWorkspace(): string {
...@@ -39,18 +59,18 @@ export default class Panel extends Vue { ...@@ -39,18 +59,18 @@ export default class Panel extends Vue {
return this.$store.getters.idForm; return this.$store.getters.idForm;
} }
public get idScreen(): string { public get idScreen(): number {
return this.$store.getters.idScreen; return this.$store.getters.idScreen;
} }
public selectScreen(screen) { public selectScreen(screen: IScreen) {
if (screen.id != this.idScreen) { if (screen.id != this.idScreen) {
this.$router.push({ this.$router.push({
name: 'ScreenEditComponent', name: 'ScreenEditComponent',
params: { params: {
idWorkspace: this.idWorkspace, idWorkspace: this.idWorkspace,
idForm: this.idForm, idForm: this.idForm,
idScreen: screen.id, idScreen: `${screen.id}`,
}, },
}); });
} }
......
...@@ -23,29 +23,7 @@ ...@@ -23,29 +23,7 @@
@confirm="deleteScreen" @confirm="deleteScreen"
/> />
</div> </div>
<ol class="p-2 text-sm select-none"> <dnd-list :screenList="screenList" :idScreen="idScreen" @update="updateScreenIndex" @selectScreen="selectScreen" :key="dndKey" />
<li
v-for="panelScreen in screenList"
:key="panelScreen.index"
:class="[
'flex items-center p-1 leading-tight transition rounded-md cursor-pointer group hover:absolute focus:ring ',
isCurrentPanel(panelScreen) ? 'font-medium text-white bg-blue-600 hover:bg-blue-700' : 'hover:bg-blue-100',
]"
tabindex="0"
role="button"
@click="selectScreen(panelScreen)"
>
<span
:class="[
'flex-shrink-0 inline-block w-8 h-6 mr-2 text-sm text-center border rounded-md',
isCurrentPanel(panelScreen)
? 'text-blue-900 bg-blue-100 border-white group-hover:border-blue-900-400'
: 'bg-white border-gray-300 group-hover:border-gray-400',
]"
>{{ panelScreen.index }}</span
>{{ panelScreen.titleBo }}
</li>
</ol>
</div> </div>
</aside> </aside>
</template> </template>
......
...@@ -95,4 +95,19 @@ export default class ScreenService { ...@@ -95,4 +95,19 @@ export default class ScreenService {
}); });
}); });
} }
public updateIndex(id: number, index: number): Promise<IScreen> {
return new Promise<IScreen>((resolve, reject) => {
axios
.patch(`${baseApiUrl}/${id}`, [{ op: 'replace', path: '/index', value: index }], {
headers: { 'Content-Type': 'application/json-patch+json' },
})
.then(res => {
resolve(res.data);
})
.catch(err => {
reject(err);
});
});
}
} }
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import Vue from 'vue'; import Vue from 'vue';
import App from './app.vue'; import App from './app.vue';
import Vue2Filters from 'vue2-filters'; import Vue2Filters from 'vue2-filters';
import VueDraggable from 'vue-draggable';
import router from './router'; import router from './router';
import { metaInfo } from './shared/meta'; import { metaInfo } from './shared/meta';
import * as config from './shared/config/config'; import * as config from './shared/config/config';
...@@ -18,6 +19,7 @@ import HeaderService from '@/shared/service/header-service'; ...@@ -18,6 +19,7 @@ import HeaderService from '@/shared/service/header-service';
Vue.config.productionTip = false; Vue.config.productionTip = false;
config.initVueApp(Vue); config.initVueApp(Vue);
Vue.use(Vue2Filters); Vue.use(Vue2Filters);
Vue.use(VueDraggable);
const i18n = config.initI18N(Vue); const i18n = config.initI18N(Vue);
const store = config.initVueXStore(Vue); const store = config.initVueXStore(Vue);
......