Skip to content

Commit 1fd2a72

Browse files
committed
feat: Export multiple portlets instances - MEED-8401 - Meeds-io/MIPs#180 (#356)
This PR will allow admin to multiple portlets instances export
1 parent d852247 commit 1fd2a72

6 files changed

Lines changed: 125 additions & 22 deletions

File tree

layout-service/src/main/java/io/meeds/layout/plugin/PortletInstanceDatabindPreferencePlugin.java renamed to layout-service/src/main/java/io/meeds/layout/plugin/PortletInstanceDatabindPlugin.java

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,53 +21,62 @@
2121
import io.meeds.layout.model.PortletInstancePreference;
2222
import io.meeds.layout.service.PortletInstanceService;
2323
import io.meeds.layout.util.JsonUtils;
24-
import io.meeds.social.databind.plugin.DatabindPreferencePlugin;
24+
import io.meeds.social.databind.plugin.DatabindPlugin;
2525
import io.meeds.social.databind.service.DatabindService;
2626
import jakarta.annotation.PostConstruct;
27-
import org.exoplatform.commons.exception.ObjectNotFoundException;
28-
import org.exoplatform.services.log.ExoLogger;
29-
import org.exoplatform.services.log.Log;
27+
import lombok.SneakyThrows;
28+
import org.apache.commons.lang3.StringUtils;
3029
import org.springframework.beans.factory.annotation.Autowired;
3130
import org.springframework.core.Ordered;
3231
import org.springframework.core.annotation.Order;
3332
import org.springframework.stereotype.Component;
3433

35-
import java.io.File;
36-
import java.io.FileWriter;
3734
import java.io.IOException;
35+
import java.nio.charset.StandardCharsets;
3836
import java.util.List;
37+
import java.util.zip.ZipEntry;
38+
import java.util.zip.ZipOutputStream;
3939

4040
@Component
4141
@Order(Ordered.HIGHEST_PRECEDENCE)
42-
public class PortletInstanceDatabindPreferencePlugin implements DatabindPreferencePlugin {
42+
public class PortletInstanceDatabindPlugin implements DatabindPlugin {
4343

44-
private static final Log LOG = ExoLogger.getExoLogger(PortletInstanceDatabindPreferencePlugin.class);
44+
public static final String OBJECT_TYPE = "PortletInstance";
4545

4646
@Autowired
4747
private PortletInstanceService portletInstanceService;
4848

4949
@Autowired
5050
private DatabindService databindService;
5151

52+
@PostConstruct
53+
public void init() {
54+
databindService.addPlugin(this);
55+
}
56+
5257
@Override
53-
public String getDataType() {
54-
return "PortletInstance";
58+
public String getObjectType() {
59+
return OBJECT_TYPE;
5560
}
5661

57-
@PostConstruct
58-
public void init() {
59-
databindService.addDataPreferencePlugin(this);
62+
@Override
63+
public boolean canHandleDatabind(String objectType, String objectId) {
64+
return StringUtils.equals(OBJECT_TYPE, objectType);
6065
}
6166

67+
@SneakyThrows
6268
@Override
63-
public void serialize(String objectId, File zipFile, String username) throws ObjectNotFoundException, IllegalAccessException {
69+
public void serialize(String objectId, ZipOutputStream zipOutputStream, String username) {
6470
List<PortletInstancePreference> preferences = portletInstanceService.getPortletInstancePreferences(Long.parseLong(objectId),
6571
username);
6672
String jsonData = JsonUtils.toJsonString(preferences);
67-
try (FileWriter writer = new FileWriter(zipFile)) {
68-
writer.write(jsonData);
69-
} catch (IOException e) {
70-
LOG.warn("Fail to serialize portlet instance with id", objectId, e);
71-
}
73+
writeContent(zipOutputStream, objectId, jsonData);
74+
}
75+
76+
private void writeContent(ZipOutputStream zipOutputStream, String objectId, String content) throws IOException {
77+
ZipEntry entry = new ZipEntry(String.format("%s_%s.json", OBJECT_TYPE, objectId));
78+
zipOutputStream.putNextEntry(entry);
79+
zipOutputStream.write(content.getBytes(StandardCharsets.UTF_8));
80+
zipOutputStream.closeEntry();
7281
}
7382
}

layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,8 @@ portlets.contentManagers=Content Managers
316316
portlets.spaceHost=Space Admin
317317
portletInstance.label.preview=Preview of {0}
318318

319+
portletInstance.label.allPortletInstancesSelected=All {0} portlet instances are selected
320+
portletInstance.label.selectedPortletInstancesCount={0} Portlet instances selected
319321
portletInstance.label.exportInstance=Export Instance
320322
portletInstance.label.export=Export
321323
portletInstance.label.exportInstance.part1=Export the Instance for use on another platform.

layout-webapp/src/main/webapp/vue-app/portlets/components/header/Toolbar.vue

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,18 @@
4242
@toggle-select="$emit('select-tab', $event)"
4343
@filter-text-input="$emit('portlet-instance-filter', $event)">
4444
<template v-if="!$root.isMobile && tabName === 'instances'" #left>
45+
<div v-if="$root.selectedPortletInstances?.length && !$root.isMobile">
46+
<v-btn
47+
color="primary"
48+
elevation="0"
49+
outlined
50+
@click="$root.$emit('serialize-drawer-open', 'PortletInstance', selectedPortletInstancesIds)">
51+
<v-icon size="16" class="me-2">fa-download</v-icon>
52+
{{ $t('portletInstance.label.export') }}
53+
</v-btn>
54+
</div>
4555
<v-btn
56+
v-else
4657
id="applicationToolbarLeftButton"
4758
:aria-label="$t('layout.portletInstance.add')"
4859
:class="$root.isMobile && 'px-0'"
@@ -69,5 +80,10 @@ export default {
6980
default: null,
7081
}
7182
},
83+
computed: {
84+
selectedPortletInstancesIds() {
85+
return this.$root.selectedPortletInstances.map(item => item.id);
86+
}
87+
}
7288
};
7389
</script>

layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Item.vue

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,17 @@
2020
-->
2121
<template>
2222
<tr>
23+
<td align="center">
24+
<v-checkbox
25+
:value="selected || $root.allPortletInstancesSelected"
26+
on-icon="fas fa-check-square fa-lg primary--text"
27+
off-icon="far fa-square fa-lg"
28+
class="my-auto pt-2"
29+
@change="changeCheckboxStatus" />
30+
</td>
2331
<!-- Illustration -->
2432
<td
2533
v-if="!$root.isMobile"
26-
class="px-0"
2734
align="center">
2835
<layout-image-illustration
2936
:value="portletInstance"
@@ -68,6 +75,14 @@ export default {
6875
type: Object,
6976
default: null,
7077
},
78+
selected: {
79+
type: Boolean,
80+
default: false,
81+
},
82+
select: {
83+
type: Object,
84+
default: null,
85+
},
7186
},
7287
data: () => ({
7388
menu: false,
@@ -122,6 +137,9 @@ export default {
122137
.catch(() => this.$root.$emit('alert-message', this.$t('portlets.status.update.error'), 'error'))
123138
.finally(() => this.loading = false);
124139
},
140+
changeCheckboxStatus(status) {
141+
this.select(status);
142+
}
125143
},
126144
};
127145
</script>

layout-webapp/src/main/webapp/vue-app/portlets/components/instances/List.vue

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,48 @@
2121
<template>
2222
<div class="application-layout-style">
2323
<v-data-table
24+
v-model="$root.selectedPortletInstances"
2425
:headers="headers"
2526
:items="filteredPortletInstances"
2627
:loading="loading"
2728
:disable-sort="$root.isMobile"
2829
:hide-default-header="$root.isMobile"
2930
:custom-sort="applySortOnItems"
31+
:show-select="!$root.isMobile"
3032
must-sort
3133
disable-pagination
3234
hide-default-footer
3335
class="application-body portletInstancesTable px-5">
36+
<template slot="header.data-table-select" slot-scope="{on, props}">
37+
<v-checkbox
38+
v-on="on"
39+
v-bind="props"
40+
on-icon="fas fa-check-square fa-lg primary--text"
41+
indeterminate-icon="fas fa-minus-square fa-lg"
42+
off-icon="far fa-square fa-lg"
43+
class="my-auto pt-2"
44+
@change="on.input" />
45+
</template>
46+
<template v-if="$root.selectedPortletInstances.length" slot="body.prepend">
47+
<tr>
48+
<td :colspan="headers.length + 1" class="px-0">
49+
<v-alert
50+
:icon="false"
51+
class="ma-0 ps-5 no-border-radius"
52+
border="left"
53+
type="info"
54+
colored-border>
55+
<div v-html="selectionLabel"></div>
56+
</v-alert>
57+
</td>
58+
</tr>
59+
</template>
3460
<template slot="item" slot-scope="props">
3561
<portlets-instance-item
3662
:key="props.item.id"
37-
:portlet-instance="props.item" />
63+
:portlet-instance="props.item"
64+
:selected="props.isSelected"
65+
:select="props.select" />
3866
</template>
3967
</v-data-table>
4068
<exo-confirm-dialog
@@ -168,6 +196,26 @@ export default {
168196
nameToDelete() {
169197
return this.portletInstanceToDelete && this.$te(this.portletInstanceToDelete?.name) ? this.$t(this.portletInstanceToDelete?.name) : this.portletInstanceToDelete?.name;
170198
},
199+
selectionLabel() {
200+
if (this.$root.allPortletInstancesSelected) {
201+
return this.$t('portletInstance.label.allPortletInstancesSelected', {
202+
0: `<strong>${this.$root.portletInstancesSize}</strong>`,
203+
});
204+
} else {
205+
return this.$t('portletInstance.label.selectedPortletInstancesCount', {
206+
0: `<strong>${this.$root.selectedPortletInstances.length}</strong>`,
207+
});
208+
}
209+
},
210+
selectedPortletInstances() {
211+
return this.$root.selectedPortletInstances;
212+
},
213+
},
214+
watch: {
215+
keyword() {
216+
this.$root.allPortletInstancesSelected = false;
217+
this.$root.selectedPortletInstances = [];
218+
},
171219
},
172220
created() {
173221
this.$root.$on('portlet-instance-delete', this.deletePortletInstanceConfirm);
@@ -204,6 +252,8 @@ export default {
204252
selectCategoryId(id) {
205253
if (this.categoryId !== id) {
206254
this.categoryId = id;
255+
this.$root.allPortletInstancesSelected = false;
256+
this.$root.selectedPortletInstances = [];
207257
}
208258
},
209259
deletePortletInstance(portletInstance) {

layout-webapp/src/main/webapp/vue-app/portlets/main.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export function init() {
4949
selectedCategoryId: null,
5050
loading: 0,
5151
collator: new Intl.Collator(eXo.env.portal.language, {numeric: true, sensitivity: 'base'}),
52+
allPortletInstancesSelected: false,
53+
selectedPortletInstances: [],
54+
portletInstancesSize: 0
5255
}),
5356
computed: {
5457
isMobile() {
@@ -99,9 +102,14 @@ export function init() {
99102
.finally(() => this.loading--);
100103
},
101104
refreshPortletInstances() {
105+
this.allPortletInstancesSelected = false;
106+
this.selectedPortletInstances = [];
102107
this.loading++;
103108
return this.$portletInstanceService.getPortletInstances()
104-
.then(data => this.portletInstances = data || [])
109+
.then(data => {
110+
this.portletInstances = data || [];
111+
this.portletInstancesSize = this.portletInstances?.length;
112+
})
105113
.finally(() => this.loading--);
106114
},
107115
refreshPortletInstanceCategories() {

0 commit comments

Comments
 (0)