Skip to content

Commit d57d162

Browse files
Merge pull request #1675 from nextcloud/enh/29/trashbin
Add trashbin GUI
2 parents 0a66108 + 3f3a7f1 commit d57d162

File tree

11 files changed

+924
-92
lines changed

11 files changed

+924
-92
lines changed

package-lock.json

Lines changed: 138 additions & 80 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@
3232
"@nextcloud/event-bus": "^2.0.0",
3333
"@nextcloud/initial-state": "1.2.0",
3434
"@nextcloud/l10n": "^1.4.1",
35+
"@nextcloud/logger": "^2.0.0",
3536
"@nextcloud/moment": "^1.1.1",
3637
"@nextcloud/router": "^2.0.0",
3738
"@nextcloud/vue": "^4.0.3",
3839
"@nextcloud/vue-dashboard": "^2.0.1",
3940
"@vue/test-utils": "^1.2.1",
41+
"calendar-js": "github:nextcloud/calendar-js",
4042
"cdav-library": "github:nextcloud/cdav-library",
4143
"color-convert": "^2.0.1",
4244
"debounce": "^1.2.1",
@@ -104,7 +106,7 @@
104106
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
105107
},
106108
"transformIgnorePatterns": [
107-
"/node_modules/(?!vue-material-design-icons)"
109+
"/node_modules/(?!(calendar-js)|(vue-material-design-icons))"
108110
],
109111
"snapshotSerializers": [
110112
"<rootDir>/node_modules/jest-serializer-vue"

src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export default {
5757
// get calendars then get tasks
5858
await client.connect({ enableCalDAV: true })
5959
await this.$store.dispatch('fetchCurrentUserPrincipal')
60-
const calendars = await this.$store.dispatch('getCalendars')
60+
const { calendars } = await this.$store.dispatch('getCalendarsAndTrashBin')
6161
const owners = []
6262
calendars.forEach((calendar) => {
6363
if (owners.indexOf(calendar.owner) === -1) {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<template>
2+
<span class="live-relative-timestamp" :data-timestamp="numericTimestamp * 1000" :title="title">{{ formatted }}</span>
3+
</template>
4+
5+
<script>
6+
import moment from '@nextcloud/moment'
7+
8+
export default {
9+
name: 'Moment',
10+
props: {
11+
timestamp: {
12+
type: [Date, Number],
13+
required: true,
14+
},
15+
format: {
16+
type: String,
17+
default: 'LLL',
18+
},
19+
},
20+
computed: {
21+
title() {
22+
return moment.unix(this.numericTimestamp).format(this.format)
23+
},
24+
formatted() {
25+
return moment.unix(this.numericTimestamp).fromNow()
26+
},
27+
numericTimestamp() {
28+
if (this.timestamp instanceof Date) {
29+
return this.timestamp.getTime() / 1000
30+
}
31+
return this.timestamp
32+
},
33+
},
34+
}
35+
</script>
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
<!--
2+
Nextcloud - Tasks
3+
4+
@author Christoph Wurst
5+
@copyright 2021 Christoph Wurst <[email protected]>
6+
@copyright 2021 Raimund Schlüßler <[email protected]>
7+
8+
This library is free software; you can redistribute it and/or
9+
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
10+
License as published by the Free Software Foundation; either
11+
version 3 of the License, or any later version.
12+
13+
This library is distributed in the hope that it will be useful,
14+
but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
GNU AFFERO GENERAL PUBLIC LICENSE for more details.
17+
18+
You should have received a copy of the GNU Affero General Public
19+
License along with this library. If not, see <http://www.gnu.org/licenses/>.
20+
21+
-->
22+
23+
<template>
24+
<AppNavigationItem :title="t('tasks', 'Trash bin')"
25+
:pinned="true"
26+
@click.prevent="onShow">
27+
<Delete
28+
slot="icon"
29+
:size="24"
30+
decorative />
31+
<template #extra>
32+
<Modal v-if="showModal"
33+
@close="showModal = false">
34+
<div class="modal__content">
35+
<h2>{{ t('tasks', 'Trash bin') }}</h2>
36+
<EmptyContent v-if="loading" icon="icon-loading">
37+
{{ t('tasks', 'Loading deleted calendars, tasks and events.') }}
38+
</EmptyContent>
39+
<EmptyContent v-else-if="!items.length">
40+
<Delete
41+
slot="icon"
42+
:size="24"
43+
decorative />
44+
{{ t('tasks', 'You do not have any deleted calendars, tasks or events.') }}
45+
</EmptyContent>
46+
<template v-else>
47+
<div class="table">
48+
<div class="table__header">
49+
{{ t('tasks', 'Name') }}
50+
</div>
51+
<div class="table__header deletedAt">
52+
{{ t('tasks', 'Deleted') }}
53+
</div>
54+
<div class="table__header">
55+
&nbsp;
56+
</div>
57+
<template v-for="item in items" class="row">
58+
<div :key="`${item.url}desc`">
59+
<div
60+
class="icon-bullet"
61+
:style="{ 'background-color': item.color }" />
62+
<div class="item-description">
63+
<div class="item-description__mainline">
64+
{{ item.name }}
65+
</div>
66+
<div v-if="item.subline" class="item-description__subline">
67+
{{ item.subline }}
68+
</div>
69+
</div>
70+
</div>
71+
<div :key="`${item.url}date`" class="deletedAt">
72+
<Moment class="timestamp" :timestamp="item.deletedAt" />
73+
</div>
74+
<div :key="`${item.url}action`">
75+
<button @click="restore(item)">
76+
{{ t('tasks','Restore') }}
77+
</button>
78+
<Actions :force-menu="true">
79+
<ActionButton
80+
@click="onDeletePermanently(item)">
81+
<Delete
82+
slot="icon"
83+
:size="24"
84+
decorative />
85+
{{ t('tasks','Delete permanently') }}
86+
</ActionButton>
87+
</Actions>
88+
</div>
89+
</template>
90+
</div>
91+
<p v-if="retentionDuration" class="footer">
92+
{{ n('calendar', 'Elements in the trash bin are deleted after {numDays} day', 'Elements in the trash bin are deleted after {numDays} days', retentionDuration, { numDays: retentionDuration }) }}
93+
</p>
94+
</template>
95+
</div>
96+
</Modal>
97+
</template>
98+
</AppNavigationItem>
99+
</template>
100+
101+
<script>
102+
import Moment from './Moment'
103+
import { uidToHexColor } from '../../utils/color'
104+
import logger from '../../utils/logger'
105+
106+
import { showError } from '@nextcloud/dialogs'
107+
import { translate as t } from '@nextcloud/l10n'
108+
import moment from '@nextcloud/moment'
109+
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
110+
import Actions from '@nextcloud/vue/dist/Components/Actions'
111+
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
112+
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
113+
import Modal from '@nextcloud/vue/dist/Components/Modal'
114+
115+
import Delete from 'vue-material-design-icons/Delete.vue'
116+
117+
import { mapGetters } from 'vuex'
118+
119+
export default {
120+
name: 'Trashbin',
121+
components: {
122+
AppNavigationItem,
123+
Delete,
124+
EmptyContent,
125+
Modal,
126+
Moment,
127+
Actions,
128+
ActionButton,
129+
},
130+
data() {
131+
return {
132+
showModal: false,
133+
loading: true,
134+
}
135+
},
136+
computed: {
137+
...mapGetters({
138+
trashBin: 'trashBin',
139+
calendars: 'sortedDeletedCalendars',
140+
objects: 'deletedCalendarObjects',
141+
}),
142+
items() {
143+
const formattedCalendars = this.calendars.map(calendar => ({
144+
calendar,
145+
type: 'calendar',
146+
key: calendar.url,
147+
name: calendar.displayname,
148+
url: calendar._url,
149+
deletedAt: calendar._props['{http://nextcloud.com/ns}deleted-at'],
150+
color: calendar.color ?? uidToHexColor(calendar.displayname),
151+
}))
152+
const formattedCalendarObjects = this.objects.map(vobject => {
153+
let eventSummary = t('tasks', 'Untitled item')
154+
try {
155+
eventSummary = vobject?.calendarComponent.getComponentIterator().next().value?.title
156+
} catch (e) {
157+
// ignore
158+
}
159+
let subline = vobject.calendar.displayName
160+
if (vobject.isEvent) {
161+
const event = vobject?.calendarComponent.getFirstComponent('VEVENT')
162+
if (event?.startDate.jsDate && event?.isAllDay()) {
163+
subline += ' · ' + moment(event.startDate.jsDate).format('LL')
164+
} else if (event?.startDate.jsDate) {
165+
subline += ' · ' + moment(event?.startDate.jsDate).format('LLL')
166+
}
167+
}
168+
const color = vobject.calendarComponent.getComponentIterator().next().value?.color
169+
?? vobject.calendar.color
170+
?? uidToHexColor(vobject.calendar.displayName)
171+
return {
172+
vobject,
173+
type: 'object',
174+
key: vobject.id,
175+
name: eventSummary,
176+
subline,
177+
url: vobject.uri,
178+
deletedAt: vobject.dav._props['{http://nextcloud.com/ns}deleted-at'],
179+
color,
180+
}
181+
})
182+
183+
return formattedCalendars.concat(formattedCalendarObjects).sort((item1, item2) => item2.deletedAt - item1.deletedAt)
184+
185+
},
186+
retentionDuration() {
187+
return Math.ceil(
188+
this.trashBin.retentionDuration / (60 * 60 * 24)
189+
)
190+
},
191+
},
192+
methods: {
193+
async onShow() {
194+
this.showModal = true
195+
196+
this.loading = true
197+
try {
198+
await Promise.all([
199+
this.$store.dispatch('loadDeletedCalendars'),
200+
this.$store.dispatch('loadDeletedCalendarObjects'),
201+
])
202+
203+
logger.debug('deleted calendars and objects loaded', {
204+
calendars: this.calendars,
205+
objects: this.objects,
206+
})
207+
} catch (error) {
208+
logger.error('could not load deleted calendars and objects', {
209+
error,
210+
})
211+
212+
showError(t('tasks', 'Could not load deleted calendars and objects'))
213+
}
214+
this.loading = false
215+
},
216+
async onDeletePermanently(item) {
217+
logger.debug('deleting ' + item.url + ' permanently', item)
218+
try {
219+
switch (item.type) {
220+
case 'calendar':
221+
await this.$store.dispatch('deleteCalendarPermanently', { calendar: item.calendar })
222+
break
223+
case 'object':
224+
await this.$store.dispatch('deleteCalendarObjectPermanently', { vobject: item.vobject })
225+
break
226+
}
227+
} catch (error) {
228+
logger.error('could not restore ' + item.url, { error })
229+
230+
showError(t('tasks', 'Could not restore calendar or event'))
231+
}
232+
},
233+
async restore(item) {
234+
logger.debug('restoring ' + item.url, item)
235+
try {
236+
switch (item.type) {
237+
case 'calendar': {
238+
await this.$store.dispatch('restoreCalendar', { calendar: item.calendar })
239+
const { calendars } = await this.$store.dispatch('getCalendarsAndTrashBin')
240+
// Load the tasks of the restored calendar
241+
const calendar = calendars.find(cal => cal.url === item.calendar.url)
242+
if (calendar) {
243+
await this.$store.dispatch('getTasksFromCalendar', { calendar, completed: false, related: null })
244+
}
245+
break
246+
}
247+
case 'object':
248+
await this.$store.dispatch('restoreCalendarObject', { vobject: item.vobject })
249+
break
250+
}
251+
} catch (error) {
252+
logger.error('could not restore ' + item.url, { error })
253+
254+
showError(t('tasks', 'Could not restore calendar or event'))
255+
}
256+
},
257+
},
258+
}
259+
</script>
260+
261+
<style lang="scss" scoped>
262+
::v-deep .modal-container {
263+
height: 80%;
264+
}
265+
266+
.modal__content {
267+
display: flex;
268+
flex-direction: column;
269+
margin: 2vw;
270+
height: calc(100% - 4vw);
271+
max-height: calc(100% - 4vw);
272+
}
273+
274+
.table {
275+
display: grid;
276+
grid-template-columns: minmax(200px, 1fr) max-content max-content;
277+
overflow: scroll;
278+
margin-bottom: auto;
279+
280+
&__header {
281+
position: sticky;
282+
top: 0;
283+
background-color: var(--color-main-background-translucent);
284+
z-index: 1;
285+
}
286+
287+
& > div {
288+
display: flex;
289+
align-items: center;
290+
padding: 4px;
291+
}
292+
}
293+
294+
.item-description {
295+
overflow: hidden;
296+
297+
&__mainline,
298+
&__subline {
299+
overflow: hidden;
300+
white-space: nowrap;
301+
text-overflow: ellipsis;
302+
}
303+
304+
&__subline {
305+
color: var(--color-text-maxcontrast);
306+
}
307+
}
308+
309+
.deletedAt {
310+
text-align: right;
311+
}
312+
313+
.footer {
314+
color: var(--color-text-lighter);
315+
text-align: center;
316+
font-size: small;
317+
margin-top: 16px;
318+
}
319+
320+
.icon-bullet {
321+
display: inline-block;
322+
vertical-align: middle;
323+
width: 14px;
324+
height: 14px;
325+
margin-right: 14px;
326+
border: none;
327+
border-radius: 50%;
328+
flex-shrink: 0;
329+
}
330+
</style>

0 commit comments

Comments
 (0)