@oxygen-cms/ui 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +1 -0
- package/.eslintrc.js +22 -0
- package/.github/workflows/node.js.yml +29 -0
- package/.idea/modules.xml +8 -0
- package/.idea/ui.iml +10 -0
- package/.jshintrc +3 -0
- package/README.md +7 -0
- package/assets/oxygen-icon.png +0 -0
- package/jest.init.js +1 -0
- package/package.json +72 -0
- package/src/AuthApi.js +116 -0
- package/src/CrudApi.js +112 -0
- package/src/EventsApi.js +16 -0
- package/src/GroupsApi.js +9 -0
- package/src/Internationalize.js +31 -0
- package/src/MediaApi.js +52 -0
- package/src/MediaDirectoryApi.js +62 -0
- package/src/PreferencesApi.js +47 -0
- package/src/UserPermissions.js +66 -0
- package/src/UserPreferences.js +69 -0
- package/src/UserPreferences.test.js +23 -0
- package/src/UsersApi.js +41 -0
- package/src/api.js +209 -0
- package/src/components/App.vue +61 -0
- package/src/components/AuthenticatedLayout.vue +254 -0
- package/src/components/AuthenticationLog.vue +196 -0
- package/src/components/CodeEditor.vue +90 -0
- package/src/components/EditButtonOnRowHover.vue +21 -0
- package/src/components/Error404.vue +25 -0
- package/src/components/EventsChooser.vue +88 -0
- package/src/components/EventsTable.vue +82 -0
- package/src/components/GenericEditableField.vue +74 -0
- package/src/components/GroupsChooser.vue +58 -0
- package/src/components/GroupsList.vue +129 -0
- package/src/components/ImportExport.vue +45 -0
- package/src/components/LegacyPage.vue +256 -0
- package/src/components/UserJoined.vue +35 -0
- package/src/components/UserManagement.vue +168 -0
- package/src/components/UserProfileForm.vue +214 -0
- package/src/components/ViewProfile.vue +32 -0
- package/src/components/auth/Auth404.vue +16 -0
- package/src/components/auth/Login.vue +135 -0
- package/src/components/auth/LoginLogo.vue +30 -0
- package/src/components/auth/Logout.vue +26 -0
- package/src/components/auth/PasswordRemind.vue +71 -0
- package/src/components/auth/PasswordReset.vue +97 -0
- package/src/components/auth/TwoFactorSetup.vue +115 -0
- package/src/components/auth/VerifyEmail.vue +71 -0
- package/src/components/auth/WelcomeFloat.vue +87 -0
- package/src/components/auth/login.scss +17 -0
- package/src/components/media/MediaChooseDirectory.vue +129 -0
- package/src/components/media/MediaDirectory.vue +109 -0
- package/src/components/media/MediaInsertModal.vue +88 -0
- package/src/components/media/MediaItem.vue +282 -0
- package/src/components/media/MediaItemPreview.vue +45 -0
- package/src/components/media/MediaList.vue +305 -0
- package/src/components/media/MediaPage.vue +44 -0
- package/src/components/media/MediaResponsiveImages.vue +51 -0
- package/src/components/media/MediaUpload.vue +133 -0
- package/src/components/media/media.scss +51 -0
- package/src/components/preferences/PreferencesAdminAppearance.vue +22 -0
- package/src/components/preferences/PreferencesAuthentication.vue +27 -0
- package/src/components/preferences/PreferencesEventTemplates.vue +22 -0
- package/src/components/preferences/PreferencesField.vue +215 -0
- package/src/components/preferences/PreferencesList.vue +50 -0
- package/src/components/preferences/PreferencesPageTemplates.vue +23 -0
- package/src/components/preferences/PreferencesSiteAppearance.vue +22 -0
- package/src/components/preferences/PreferencesThemeChooser.vue +73 -0
- package/src/components/preferences/ShowIfPermitted.vue +37 -0
- package/src/components/preferences/UserPreferences.vue +30 -0
- package/src/components/users/CreateUserModal.vue +73 -0
- package/src/components/util.css +47 -0
- package/src/icons.js +90 -0
- package/src/main.js +112 -0
- package/src/modules/LegacyPages.js +18 -0
- package/src/modules/Media.js +45 -0
- package/src/modules/UserManagement.js +24 -0
- package/src/routes/index.js +92 -0
- package/src/store/index.js +70 -0
- package/src/styles/_variables.scss +23 -0
- package/src/styles/app.scss +76 -0
- package/src/unsavedChanges.js +16 -0
- package/src/util.js +65 -0
- package/src/util.test.js +39 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="media-item card" :class="item.selected ? 'media-item-selected' : ''" >
|
|
3
|
+
<div class="card-image cursor-pointer" @click.exact="select(true)" @click.shift.exact="select(false)">
|
|
4
|
+
<MediaItemPreview :item="item" />
|
|
5
|
+
</div>
|
|
6
|
+
<div class="card-content">
|
|
7
|
+
<p class="title is-4 cursor-pointer has-text-centered" @click.exact="select(true)" @click.shift.exact="select(false)">{{ item.name }}</p>
|
|
8
|
+
<p v-if="displayFullPath" class="subtitle is-6 cursor-pointer has-text-centered" @click.exact="select(true)" @click.shift.exact="select(false)">
|
|
9
|
+
inside '{{ directoryPath }}'
|
|
10
|
+
<!-- <b-icon icon="file-image" v-if="item.type === TYPE_IMAGE"></b-icon>-->
|
|
11
|
+
<!-- <b-icon icon="file" v-else-if="item.type === TYPE_DOCUMENT"></b-icon>-->
|
|
12
|
+
<!-- <b-icon icon="file-audio" v-else-if="item.type === TYPE_AUDIO"></b-icon>-->
|
|
13
|
+
<!-- {{ item.slug }}.{{ item.extension }}-->
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<div v-if="item.selected" class="content media-item-toolbar">
|
|
17
|
+
<b-button icon-left="photo-video" size="is-small" rounded tag="a" :href="externalLink">View</b-button>
|
|
18
|
+
<b-button icon-left="pencil-alt" size="is-small" rounded @click="openEditModal">Edit info</b-button>
|
|
19
|
+
<MediaChooseDirectory v-if="!item.deletedAt" @submit="moveToDirectory"></MediaChooseDirectory>
|
|
20
|
+
<b-button v-if="!item.deletedAt" icon-left="share" size="is-small" rounded outlined @click="isShareModalActive = true">Share</b-button>
|
|
21
|
+
<b-button v-if="!item.deletedAt" icon-left="trash" size="is-small" rounded outlined type="is-danger" @click="deleteItem">Delete</b-button>
|
|
22
|
+
<b-button v-if="item.deletedAt" icon-left="recycle" size="is-small" rounded outlined @click="restoreItem">Restore</b-button>
|
|
23
|
+
<b-button v-if="item.deletedAt" icon-left="trash" size="is-small" rounded outlined type="is-danger" @click="confirmForceDeleteItem">Delete forever</b-button>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<b-modal :active.sync="isShareModalActive" trap-focus has-modal-card width="80%">
|
|
28
|
+
<div class="modal-card" style="width: auto;">
|
|
29
|
+
<header class="modal-card-head">
|
|
30
|
+
<p class="modal-card-title">Share a public link</p>
|
|
31
|
+
</header>
|
|
32
|
+
<section class="modal-card-body">
|
|
33
|
+
<p>When referencing "{{ item.name }}" externally, use the following URL:<br/>
|
|
34
|
+
<a :href="externalLink">{{ externalLink }}</a>
|
|
35
|
+
</p>
|
|
36
|
+
<br />
|
|
37
|
+
|
|
38
|
+
<p>To use this item within Oxygen, use the <em>"Insert Photo or File"</em> button inside the content editor.</p>
|
|
39
|
+
</section>
|
|
40
|
+
<footer class="modal-card-foot is-flex">
|
|
41
|
+
<div class="is-flex-grow-1"></div>
|
|
42
|
+
<b-button
|
|
43
|
+
label="Close"
|
|
44
|
+
@click="isShareModalActive = false" />
|
|
45
|
+
</footer>
|
|
46
|
+
</div>
|
|
47
|
+
</b-modal>
|
|
48
|
+
|
|
49
|
+
<b-modal v-hotkey="keymap" :active.sync="isEditModalActive" trap-focus has-modal-card width="80%">
|
|
50
|
+
<div class="modal-card" style="width: auto">
|
|
51
|
+
<header class="modal-card-head">
|
|
52
|
+
<p class="modal-card-title">Edit Media Item - {{ name }}</p>
|
|
53
|
+
</header>
|
|
54
|
+
<section class="modal-card-body">
|
|
55
|
+
<b-field label="Name">
|
|
56
|
+
<b-input v-model="name"></b-input>
|
|
57
|
+
</b-field>
|
|
58
|
+
<b-field>
|
|
59
|
+
<template #label>
|
|
60
|
+
Slug
|
|
61
|
+
<b-tooltip multilined position="is-right" type="is-dark" label="How this item is referenced in code. If left blank, it will be autogenerated.">
|
|
62
|
+
<b-icon size="is-small" icon="question-circle"></b-icon>
|
|
63
|
+
</b-tooltip>
|
|
64
|
+
</template>
|
|
65
|
+
<p class="control">
|
|
66
|
+
<b-tooltip label="To change, move this item to another directory."
|
|
67
|
+
multilined
|
|
68
|
+
type="is-dark"
|
|
69
|
+
position="is-right">
|
|
70
|
+
<b-button disabled>{{ directorySlug }}/</b-button>
|
|
71
|
+
</b-tooltip>
|
|
72
|
+
</p>
|
|
73
|
+
<b-input v-model="slug" placeholder="will be set from 'Name'" expanded></b-input>
|
|
74
|
+
<p class="control">
|
|
75
|
+
<b-tooltip label="The extension is set from the file type."
|
|
76
|
+
multilined
|
|
77
|
+
type="is-dark"
|
|
78
|
+
position="is-left">
|
|
79
|
+
<b-button disabled>.{{ item.extension }}</b-button>
|
|
80
|
+
</b-tooltip>
|
|
81
|
+
</p>
|
|
82
|
+
</b-field>
|
|
83
|
+
<b-field label="Author">
|
|
84
|
+
<b-input v-model="author"></b-input>
|
|
85
|
+
</b-field>
|
|
86
|
+
<b-field>
|
|
87
|
+
<template #label>
|
|
88
|
+
Caption
|
|
89
|
+
<b-tooltip multilined position="is-right" type="is-dark" label="May be displayed next to the item, or used as alt-text for vision-impaired.">
|
|
90
|
+
<b-icon size="is-small" icon="question-circle"></b-icon>
|
|
91
|
+
</b-tooltip>
|
|
92
|
+
</template>
|
|
93
|
+
<b-input v-model="caption"></b-input>
|
|
94
|
+
</b-field>
|
|
95
|
+
<b-field>
|
|
96
|
+
<template #label>
|
|
97
|
+
Description
|
|
98
|
+
<b-tooltip multilined position="is-right" type="is-dark" label="Arbitrary extra details/notes - will not be shown to the public.">
|
|
99
|
+
<b-icon size="is-small" icon="question-circle"></b-icon>
|
|
100
|
+
</b-tooltip>
|
|
101
|
+
</template>
|
|
102
|
+
<b-input v-model="description" type="textarea"></b-input>
|
|
103
|
+
</b-field>
|
|
104
|
+
|
|
105
|
+
<label class="label">
|
|
106
|
+
Variants
|
|
107
|
+
<b-tooltip multilined position="is-right" type="is-dark" label="Each uploaded media item will be automatically converted into different sizes/formats to serve optimized images to users.">
|
|
108
|
+
<b-icon size="is-small" icon="question-circle"></b-icon>
|
|
109
|
+
</b-tooltip>
|
|
110
|
+
</label>
|
|
111
|
+
<b-table striped :data="variants" :default-sort="['width']">
|
|
112
|
+
<b-table-column v-slot="props" label="Filename" field="filename"><a :href="'/content/media/' + props.row.filename">{{ props.row.filename }}</a></b-table-column>
|
|
113
|
+
<b-table-column v-slot="props" label="Width (px)" field="width" sortable>{{ props.row.width ? props.row.width : 'Full size' }}</b-table-column>
|
|
114
|
+
<b-table-column v-slot="props" label="Format" field="mime" sortable>{{ props.row.mime }}</b-table-column>
|
|
115
|
+
</b-table>
|
|
116
|
+
|
|
117
|
+
<label class="label">Versions</label>
|
|
118
|
+
|
|
119
|
+
<b-table striped :data="versions"
|
|
120
|
+
:loading="versions.length === 0"
|
|
121
|
+
custom-row-key="id"
|
|
122
|
+
detailed
|
|
123
|
+
detail-key="id">
|
|
124
|
+
<b-table-column v-slot="props" label="Name" field="name">{{ props.row.name }}</b-table-column>
|
|
125
|
+
<b-table-column v-slot="props" label="Path" field="fullPath">{{ props.row.fullPath }}</b-table-column>
|
|
126
|
+
<b-table-column v-slot="props" label="Last Updated" field="updatedAt">
|
|
127
|
+
<div class="is-size-7">{{ Internationalize.formatLastUpdated(props.row.updatedAt) }}</div>
|
|
128
|
+
</b-table-column>
|
|
129
|
+
|
|
130
|
+
<b-table-column v-slot="slotProps" label="">
|
|
131
|
+
<b-button rounded :disabled="slotProps.row.headVersion === null" @click="restoreVersion(slotProps.row.id)">
|
|
132
|
+
<span v-if="slotProps.row.headVersion === null">Already current</span>
|
|
133
|
+
<span v-else>Restore version</span>
|
|
134
|
+
</b-button>
|
|
135
|
+
</b-table-column>
|
|
136
|
+
|
|
137
|
+
<template slot="detail" slot-scope="props">
|
|
138
|
+
<article class="media">
|
|
139
|
+
<div class="media-content">
|
|
140
|
+
<div class="content">
|
|
141
|
+
<p>
|
|
142
|
+
<strong>Author: </strong><span v-if="props.row.author">{{ props.row.author }}</span><em v-else>none</em><br>
|
|
143
|
+
<strong>Caption: </strong><span v-if="props.row.caption">{{ props.row.caption }}</span><em v-else>none</em><br>
|
|
144
|
+
<strong>Description: </strong><span v-if="props.row.description">{{ props.row.description }}</span><em v-else>none</em><br>
|
|
145
|
+
</p>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</article>
|
|
149
|
+
</template>
|
|
150
|
+
</b-table>
|
|
151
|
+
</section>
|
|
152
|
+
<footer class="modal-card-foot is-flex">
|
|
153
|
+
<div class="is-flex-grow-1"></div>
|
|
154
|
+
<b-button @click="isEditModalActive = false">Close</b-button>
|
|
155
|
+
<b-button type="is-primary" @click="saveEdits">Save</b-button>
|
|
156
|
+
</footer>
|
|
157
|
+
</div>
|
|
158
|
+
</b-modal>
|
|
159
|
+
</div>
|
|
160
|
+
</template>
|
|
161
|
+
|
|
162
|
+
<script>
|
|
163
|
+
|
|
164
|
+
import {morphToNotification} from "../../api";
|
|
165
|
+
import MediaApi from "../../MediaApi";
|
|
166
|
+
import MediaChooseDirectory from "./MediaChooseDirectory.vue";
|
|
167
|
+
import Internationalize from "../../Internationalize";
|
|
168
|
+
import {getDirectoryFullSlug, getDirectoryPathString} from "../../MediaDirectoryApi";
|
|
169
|
+
import MediaItemPreview from "./MediaItemPreview.vue";
|
|
170
|
+
|
|
171
|
+
export default {
|
|
172
|
+
name: "MediaItem",
|
|
173
|
+
components: {MediaChooseDirectory, MediaItemPreview},
|
|
174
|
+
props: {
|
|
175
|
+
item: { type: Object, required: true },
|
|
176
|
+
displayFullPath: Boolean
|
|
177
|
+
},
|
|
178
|
+
data() {
|
|
179
|
+
return {
|
|
180
|
+
isEditModalActive: false,
|
|
181
|
+
isShareModalActive: false,
|
|
182
|
+
name: null,
|
|
183
|
+
author: null,
|
|
184
|
+
slug: null,
|
|
185
|
+
versions: [],
|
|
186
|
+
caption: null,
|
|
187
|
+
description: null,
|
|
188
|
+
mediaApi: new MediaApi(this.$buefy),
|
|
189
|
+
Internationalize
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
computed: {
|
|
193
|
+
keymap() {
|
|
194
|
+
return {
|
|
195
|
+
'ctrl+s': (event) => {
|
|
196
|
+
this.saveEdits();
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
variants() {
|
|
202
|
+
return this.item.variants;
|
|
203
|
+
},
|
|
204
|
+
externalLink() {
|
|
205
|
+
return window.location.origin + '/media/' + this.item.fullPath;
|
|
206
|
+
},
|
|
207
|
+
directoryPath() {
|
|
208
|
+
return getDirectoryPathString(this.item.parentDirectory);
|
|
209
|
+
},
|
|
210
|
+
directorySlug() {
|
|
211
|
+
return getDirectoryFullSlug(this.item.parentDirectory);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
methods: {
|
|
215
|
+
select(toggle) {
|
|
216
|
+
if(this.item.selected) {
|
|
217
|
+
this.$emit('double-click-action', this.item);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.$emit('select', this.item, toggle);
|
|
221
|
+
},
|
|
222
|
+
openEditModal() {
|
|
223
|
+
this.isEditModalActive = true;
|
|
224
|
+
this.name = this.item.name;
|
|
225
|
+
this.author = this.item.author;
|
|
226
|
+
this.slug = this.item.slug;
|
|
227
|
+
this.caption = this.item.caption;
|
|
228
|
+
this.description = this.item.description;
|
|
229
|
+
this.fetchVersions();
|
|
230
|
+
},
|
|
231
|
+
async fetchVersions() {
|
|
232
|
+
this.versions = [];
|
|
233
|
+
this.versions = (await this.mediaApi.listVersions(this.item.id)).items;
|
|
234
|
+
},
|
|
235
|
+
async saveEdits() {
|
|
236
|
+
let data = await this.mediaApi.update({
|
|
237
|
+
id: this.item.id,
|
|
238
|
+
parentDirectory: this.item.parentDirectory,
|
|
239
|
+
name: this.name,
|
|
240
|
+
author: this.author,
|
|
241
|
+
slug: this.slug,
|
|
242
|
+
caption: this.caption,
|
|
243
|
+
description: this.description
|
|
244
|
+
});
|
|
245
|
+
this.$buefy.toast.open(morphToNotification(data));
|
|
246
|
+
this.isEditModalActive = false;
|
|
247
|
+
this.$emit('update:item', data.item);
|
|
248
|
+
},
|
|
249
|
+
async restoreItem() {
|
|
250
|
+
await this.mediaApi.restoreAndNotify(this.item.id);
|
|
251
|
+
this.$emit('update:item', this.item);
|
|
252
|
+
},
|
|
253
|
+
async confirmForceDeleteItem() {
|
|
254
|
+
await this.mediaApi.confirmForceDelete(this.item.id);
|
|
255
|
+
this.$emit('update:item', null);
|
|
256
|
+
},
|
|
257
|
+
async deleteItem() {
|
|
258
|
+
await this.mediaApi.deleteAndNotify(this.item.id);
|
|
259
|
+
this.$emit('update:item', this.item);
|
|
260
|
+
},
|
|
261
|
+
async restoreVersion(id) {
|
|
262
|
+
let data = await this.mediaApi.makeHeadVersion(id);
|
|
263
|
+
this.$buefy.toast.open(morphToNotification(data));
|
|
264
|
+
this.versions = [];
|
|
265
|
+
this.isEditModalActive = false;
|
|
266
|
+
},
|
|
267
|
+
async moveToDirectory(directory) {
|
|
268
|
+
console.log('move to directory', directory);
|
|
269
|
+
let data = await this.mediaApi.update({
|
|
270
|
+
id: this.item.id,
|
|
271
|
+
parentDirectory: directory
|
|
272
|
+
});
|
|
273
|
+
this.$buefy.toast.open(morphToNotification(data));
|
|
274
|
+
this.$emit('update:item', this.item);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
</script>
|
|
279
|
+
|
|
280
|
+
<style scoped lang="scss">
|
|
281
|
+
@import './media.scss';
|
|
282
|
+
</style>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="media-icon-container">
|
|
3
|
+
<img v-if="item.type === TYPE_IMAGE && !loadingError" :src="'/content/media/' + item.filename" :alt="item.caption ? item.caption : item.name" @error="loadingFailed">
|
|
4
|
+
<b-tooltip v-else-if="item.type === TYPE_IMAGE" :label="missingMessage" type="is-dark" position="is-bottom" multilined>
|
|
5
|
+
<b-icon type="is-danger" icon="file-image" size="is-large" class="media-icon"></b-icon>
|
|
6
|
+
</b-tooltip>
|
|
7
|
+
<img v-if="item.type === TYPE_DOCUMENT && !loadingError" :src="'/oxygen/api/media/' + item.id + '/preview'" alt="PDF preview" @error="loadingFailed">
|
|
8
|
+
<b-tooltip v-else-if="item.type === TYPE_DOCUMENT" :label="missingMessage" type="is-dark" position="is-bottom" multilined>
|
|
9
|
+
<b-icon type="is-danger" icon="file-pdf" size="is-large" class="media-icon"></b-icon>
|
|
10
|
+
</b-tooltip>
|
|
11
|
+
<b-icon v-else-if="item.type === TYPE_AUDIO" icon="music" size="is-large" class="media-icon"></b-icon>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script>
|
|
16
|
+
import MediaApi from "../../MediaApi";
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
name: "MediaItemPreview",
|
|
20
|
+
props: {
|
|
21
|
+
item: { type: Object, required: true },
|
|
22
|
+
missingMessage: {
|
|
23
|
+
type: String,
|
|
24
|
+
default: 'There was an error trying to load this media item. The file may be corrupt or missing'
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
data() {
|
|
28
|
+
return {
|
|
29
|
+
TYPE_IMAGE: MediaApi.TYPE_IMAGE,
|
|
30
|
+
TYPE_AUDIO: MediaApi.TYPE_AUDIO,
|
|
31
|
+
TYPE_DOCUMENT: MediaApi.TYPE_DOCUMENT,
|
|
32
|
+
loadingError: false
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
methods: {
|
|
36
|
+
loadingFailed() {
|
|
37
|
+
this.loadingError = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<style scoped lang="scss">
|
|
44
|
+
@import './media.scss';
|
|
45
|
+
</style>
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
|
|
3
|
+
<div class="full-height full-height-container media-container">
|
|
4
|
+
<div class="top-bar">
|
|
5
|
+
<div v-if="!paginatedItems.loading && !inTrash && !searchQuery" class="breadcrumb">
|
|
6
|
+
<ul>
|
|
7
|
+
<li v-for="item in getDirectoryBreadcrumbItems(paginatedItems.currentDirectory)" :key="JSON.stringify(item)" :class="item.separator ? 'separator' : ''">
|
|
8
|
+
<b-icon v-if="item.home && paginatedItems.currentDirectory === null" class="subtitle" icon="home"></b-icon>
|
|
9
|
+
<b-button v-else-if="item.home" type="is-text" class="subtitle" icon-left="home" @click="navigateTo({ currentPath: '' })"></b-button>
|
|
10
|
+
<b-button v-else-if="item.link !== null" type="is-text" class="subtitle" @click="navigateTo({ currentPath: item.link })">{{ item.text }}</b-button>
|
|
11
|
+
<span v-else class="subtitle is-active">{{ item.text }}</span>
|
|
12
|
+
</li>
|
|
13
|
+
</ul>
|
|
14
|
+
</div>
|
|
15
|
+
<div v-else-if="inTrash" class="is-flex">
|
|
16
|
+
<b-button outlined rounded icon-left="arrow-left" class="action-bar-pad" @click="navigateTo({ currentPath: '' })">All Items</b-button>
|
|
17
|
+
<div class="title action-bar-pad">Deleted Photos & Files</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div v-else-if="searchQuery" class="is-flex">
|
|
20
|
+
<b-button outlined rounded icon-left="arrow-left" class="action-bar-pad" @click="navigateTo({ currentPath: '' })">All Items</b-button>
|
|
21
|
+
<div class="title action-bar-pad">Search results for "{{ searchQuery }}"</div>
|
|
22
|
+
</div>
|
|
23
|
+
<ul v-else>
|
|
24
|
+
<li><b-skeleton :animated="true" size="is-large" :width="100"></b-skeleton></li>
|
|
25
|
+
</ul>
|
|
26
|
+
|
|
27
|
+
<div class="is-flex-grow-1"></div>
|
|
28
|
+
|
|
29
|
+
<b-field class="action-bar-pad">
|
|
30
|
+
<p class="control">
|
|
31
|
+
<b-button v-if="numberOfItemsSelected > 0" disabled type="is-primary">{{ numberOfItemsSelected }} item(s) selected</b-button>
|
|
32
|
+
</p>
|
|
33
|
+
<p class="control">
|
|
34
|
+
<b-button v-if="numberOfItemsSelected > 0" icon-left="times" type="is-primary" @click="resetSelection"></b-button>
|
|
35
|
+
</p>
|
|
36
|
+
</b-field>
|
|
37
|
+
|
|
38
|
+
<b-button v-if="!inTrash && !searchQuery" icon-left="folder-plus" class="action-bar-pad" @click="isCreateDirectoryModalActive = true">New Directory</b-button>
|
|
39
|
+
<b-button v-if="!inTrash && !searchQuery" icon-left="file-upload" type="is-success" class="action-bar-pad" @click="$router.push({ query: { upload: true }})">Upload Files</b-button>
|
|
40
|
+
<b-input v-if="!inTrash" rounded placeholder="Search photos and files..." icon="search"
|
|
41
|
+
icon-pack="fas" :value="searchQuery" class="action-bar-pad" @input="value => navigateTo({searchQuery: value})"></b-input>
|
|
42
|
+
<b-button v-if="!inTrash" icon-left="trash" type="is-danger" outlined class="action-bar-pad" @click="navigateTo({inTrash: true})">Deleted Items</b-button>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<div class="media-items full-height-flex scroll-container">
|
|
47
|
+
|
|
48
|
+
<b-loading v-model="paginatedItems.loading" :is-full-page="false" :can-cancel="false"></b-loading>
|
|
49
|
+
|
|
50
|
+
<h2 v-if="!paginatedItems.loading && paginatedItems.directories.length === 0 && paginatedItems.files.length === 0" class="subtitle media-items-empty">
|
|
51
|
+
No items found.
|
|
52
|
+
</h2>
|
|
53
|
+
|
|
54
|
+
<MediaDirectory
|
|
55
|
+
v-for="dir in paginatedItems.directories"
|
|
56
|
+
:key="dir.id"
|
|
57
|
+
:directory="dir"
|
|
58
|
+
:display-full-path="!!searchQuery"
|
|
59
|
+
@rename="renameDirectory"
|
|
60
|
+
@move="fetchData"
|
|
61
|
+
@delete="fetchData"
|
|
62
|
+
@select="(item, toggle) => handleSelect(paginatedItems.directories, item, toggle)"
|
|
63
|
+
@navigate="navigateTo"></MediaDirectory>
|
|
64
|
+
<MediaItem
|
|
65
|
+
v-for="item in paginatedItems.files"
|
|
66
|
+
:key="item.id"
|
|
67
|
+
:item="item"
|
|
68
|
+
:display-full-path="!!searchQuery || inTrash"
|
|
69
|
+
@double-click-action="args => $emit('double-click-action', args)"
|
|
70
|
+
@update:item="fetchData"
|
|
71
|
+
@select="(i, toggle) => handleSelect(paginatedItems.files, i, toggle)"></MediaItem>
|
|
72
|
+
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div v-if="paginatedItems.totalFiles > paginatedItems.filesPerPage" class="pagination-container">
|
|
76
|
+
<b-pagination
|
|
77
|
+
v-model="paginatedItems.currentPage"
|
|
78
|
+
:total="paginatedItems.totalFiles"
|
|
79
|
+
:per-page="paginatedItems.filesPerPage"
|
|
80
|
+
aria-next-label="Next page"
|
|
81
|
+
aria-previous-label="Previous page"
|
|
82
|
+
aria-page-label="Page"
|
|
83
|
+
aria-current-label="Current page">
|
|
84
|
+
</b-pagination>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<b-modal :active.sync="isCreateDirectoryModalActive" trap-focus has-modal-card aria-role="dialog" aria-modal auto-focus>
|
|
88
|
+
<div class="modal-card">
|
|
89
|
+
<header class="modal-card-head">
|
|
90
|
+
<p class="modal-card-title">Create directory inside of '{{ displayPath }}'</p>
|
|
91
|
+
</header>
|
|
92
|
+
<section class="modal-card-body">
|
|
93
|
+
<b-field label-position="inside" label="Name">
|
|
94
|
+
<b-input v-model="newDirectoryName" @keyup.native.enter="doCreateDirectory"></b-input>
|
|
95
|
+
</b-field>
|
|
96
|
+
</section>
|
|
97
|
+
<footer class="modal-card-foot is-flex">
|
|
98
|
+
<div class="is-flex-grow-1"></div>
|
|
99
|
+
<b-button @click="isCreateDirectoryModalActive = false">Close</b-button>
|
|
100
|
+
<b-button type="is-primary" @click="doCreateDirectory">Create</b-button>
|
|
101
|
+
</footer>
|
|
102
|
+
</div>
|
|
103
|
+
</b-modal>
|
|
104
|
+
|
|
105
|
+
<b-modal :active.sync="isUploadModalActive" trap-focus has-modal-card aria-role="dialog" aria-modal auto-focus>
|
|
106
|
+
<MediaUpload :current-directory="paginatedItems.currentDirectory" @close="$router.push({ query: { }})" @uploaded="fetchData"></MediaUpload>
|
|
107
|
+
</b-modal>
|
|
108
|
+
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
</template>
|
|
112
|
+
|
|
113
|
+
<script>
|
|
114
|
+
import MediaApi from "../../MediaApi";
|
|
115
|
+
import MediaDirectory from "./MediaDirectory.vue";
|
|
116
|
+
import MediaItem from "./MediaItem.vue";
|
|
117
|
+
import MediaDirectoryApi, {getDirectoryBreadcrumbItems, getDirectoryPathString} from "../../MediaDirectoryApi";
|
|
118
|
+
import {morphToNotification} from "../../api";
|
|
119
|
+
import MediaUpload from "./MediaUpload.vue";
|
|
120
|
+
|
|
121
|
+
export default {
|
|
122
|
+
name: "MediaList",
|
|
123
|
+
components: { MediaDirectory, MediaItem, MediaUpload },
|
|
124
|
+
props: {
|
|
125
|
+
currentPath: {
|
|
126
|
+
type: String,
|
|
127
|
+
required: true
|
|
128
|
+
},
|
|
129
|
+
inTrash: {
|
|
130
|
+
type: Boolean
|
|
131
|
+
},
|
|
132
|
+
searchQuery: {
|
|
133
|
+
type: String,
|
|
134
|
+
default: null
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
data() {
|
|
138
|
+
return {
|
|
139
|
+
paginatedItems: {files: [], directories: [], currentDirectory: null, totalFiles: null, filesPerPage: null, loading: false, currentPage: 1},
|
|
140
|
+
isCreateDirectoryModalActive: false,
|
|
141
|
+
newDirectoryName: '',
|
|
142
|
+
searchDebounce: null,
|
|
143
|
+
mediaApi: new MediaApi(this.$buefy),
|
|
144
|
+
mediaDirectoryApi: new MediaDirectoryApi(this.$buefy),
|
|
145
|
+
getDirectoryBreadcrumbItems: getDirectoryBreadcrumbItems
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
computed: {
|
|
149
|
+
isUploadModalActive() {
|
|
150
|
+
return this.$route.query.upload === 'true';
|
|
151
|
+
},
|
|
152
|
+
displayPath() {
|
|
153
|
+
return getDirectoryPathString(this.paginatedItems.currentDirectory);
|
|
154
|
+
},
|
|
155
|
+
numberOfItemsSelected() {
|
|
156
|
+
return this.paginatedItems.files.concat(this.paginatedItems.directories).filter(item => item.selected).length;
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
watch: {
|
|
160
|
+
'searchQuery': 'debounceFetchData',
|
|
161
|
+
'inTrash': 'fetchData',
|
|
162
|
+
'paginatedItems.currentPage': 'fetchData',
|
|
163
|
+
'currentPath': 'fetchData'
|
|
164
|
+
},
|
|
165
|
+
created() {
|
|
166
|
+
this.fetchData()
|
|
167
|
+
},
|
|
168
|
+
methods: {
|
|
169
|
+
async fetchData() {
|
|
170
|
+
if(this.paginatedItems.loading) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.paginatedItems.loading = true;
|
|
174
|
+
this.paginatedItems.files = []
|
|
175
|
+
this.paginatedItems.directories = [];
|
|
176
|
+
|
|
177
|
+
let data = await this.mediaApi.list(this.inTrash, this.searchQuery, this.paginatedItems.currentPage, this.searchQuery ? '' : this.currentPath);
|
|
178
|
+
|
|
179
|
+
this.paginatedItems.currentDirectory = data.currentDirectory;
|
|
180
|
+
this.paginatedItems.directories = data.directories.map(dir => { dir.selected = false; return dir; });
|
|
181
|
+
this.paginatedItems.files = data.files.map(file => {file.selected = false; return file;});
|
|
182
|
+
this.paginatedItems.totalFiles = data.totalFiles;
|
|
183
|
+
this.paginatedItems.filesPerPage = data.filesPerPage;
|
|
184
|
+
this.paginatedItems.loading = false;
|
|
185
|
+
},
|
|
186
|
+
debounceFetchData() {
|
|
187
|
+
clearTimeout(this.searchDebounce)
|
|
188
|
+
this.searchDebounce = setTimeout(() => {
|
|
189
|
+
this.fetchData();
|
|
190
|
+
}, 400);
|
|
191
|
+
},
|
|
192
|
+
handleSelect(items, item, toggle) {
|
|
193
|
+
this.paginatedItems.files.concat(this.paginatedItems.directories)
|
|
194
|
+
.forEach(i => {
|
|
195
|
+
if(i === item && toggle) {
|
|
196
|
+
i.selected = !i.selected;
|
|
197
|
+
} else if(i === item) {
|
|
198
|
+
i.selected = true;
|
|
199
|
+
} else if(toggle) {
|
|
200
|
+
i.selected = false;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
this.$emit('select-files', this.paginatedItems.files.filter(i => i.selected));
|
|
204
|
+
},
|
|
205
|
+
resetSelection() {
|
|
206
|
+
console.log('resetting selection');
|
|
207
|
+
this.paginatedItems.files.concat(this.paginatedItems.directories)
|
|
208
|
+
.forEach(i => i.selected = false);
|
|
209
|
+
},
|
|
210
|
+
navigateTo(options) {
|
|
211
|
+
this.$emit('navigate', options);
|
|
212
|
+
},
|
|
213
|
+
async doCreateDirectory() {
|
|
214
|
+
let data = await this.mediaDirectoryApi.create({
|
|
215
|
+
name: this.newDirectoryName,
|
|
216
|
+
parentDirectory: this.paginatedItems.currentDirectory
|
|
217
|
+
});
|
|
218
|
+
this.$buefy.toast.open(morphToNotification(data));
|
|
219
|
+
this.newDirectoryName = '';
|
|
220
|
+
this.isCreateDirectoryModalActive = false;
|
|
221
|
+
await this.fetchData();
|
|
222
|
+
},
|
|
223
|
+
async renameDirectory(dir, name) {
|
|
224
|
+
let data = await this.mediaDirectoryApi.update({ ... dir, name: name });
|
|
225
|
+
this.$buefy.toast.open(morphToNotification(data));
|
|
226
|
+
await this.fetchData();
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
</script>
|
|
231
|
+
|
|
232
|
+
<style scoped lang="scss">
|
|
233
|
+
@import '../../styles/variables';
|
|
234
|
+
@import '../util.css';
|
|
235
|
+
|
|
236
|
+
.box {
|
|
237
|
+
display: flex;
|
|
238
|
+
flex-direction: column;
|
|
239
|
+
padding: 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.media-items {
|
|
243
|
+
position: relative;
|
|
244
|
+
flex: 1;
|
|
245
|
+
display: flex;
|
|
246
|
+
flex-wrap: wrap;
|
|
247
|
+
align-items: flex-start;
|
|
248
|
+
align-content: flex-start;
|
|
249
|
+
padding: 1rem;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.pagination-container {
|
|
253
|
+
padding: 1rem;
|
|
254
|
+
background-color: white;
|
|
255
|
+
border-top: 1px solid $grey-lighter;
|
|
256
|
+
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.media-container {
|
|
260
|
+
background-color: $white-bis;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.top-bar {
|
|
264
|
+
display: flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
background-color: white;
|
|
267
|
+
padding: 1rem;
|
|
268
|
+
margin-bottom: 0;
|
|
269
|
+
border-bottom: 1px solid $grey-lighter;
|
|
270
|
+
width: 100%;
|
|
271
|
+
position: sticky;
|
|
272
|
+
top: 0;
|
|
273
|
+
z-index: 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.top-bar .breadcrumb {
|
|
277
|
+
margin-bottom: 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.action-bar-pad {
|
|
281
|
+
margin: 0 0.25rem;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.breadcrumb li {
|
|
285
|
+
padding: 0 0.5em;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.breadcrumb li:first-child a {
|
|
289
|
+
padding-left: inherit !important;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.breadcrumb li + li:before {
|
|
293
|
+
padding-right: 1.5em;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.breadcrumb ul {
|
|
297
|
+
align-items: center;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.media-items-empty {
|
|
301
|
+
text-align: center;
|
|
302
|
+
padding: 3rem 1rem;
|
|
303
|
+
width: 100%;
|
|
304
|
+
}
|
|
305
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<MediaList :current-path="currentPath" :in-trash="inTrash" :search-query="searchQuery" @navigate="onNavigate" @double-click-action="viewItem" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script>
|
|
6
|
+
import MediaList from './MediaList.vue';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
name: "MediaPage",
|
|
10
|
+
components: { MediaList },
|
|
11
|
+
props: {
|
|
12
|
+
currentPath: {
|
|
13
|
+
type: String,
|
|
14
|
+
default: ''
|
|
15
|
+
},
|
|
16
|
+
inTrash: {
|
|
17
|
+
type: Boolean,
|
|
18
|
+
default: false
|
|
19
|
+
},
|
|
20
|
+
searchQuery: {
|
|
21
|
+
type: String,
|
|
22
|
+
default: null
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
methods: {
|
|
26
|
+
onNavigate(options) {
|
|
27
|
+
if(typeof options.searchQuery !== 'undefined' && options.searchQuery !== null && options.searchQuery !== '') {
|
|
28
|
+
this.$router.push({ name: 'media.search', params: { searchQuery: options.searchQuery }});
|
|
29
|
+
} else if(options.inTrash) {
|
|
30
|
+
this.$router.push({ name: 'media.trash' });
|
|
31
|
+
} else {
|
|
32
|
+
this.$router.push({ name: 'media.list', params: { currentPath: options.currentPath } });
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
viewItem(item) {
|
|
36
|
+
window.location.href = '/media/' + item.fullPath;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<style scoped>
|
|
43
|
+
|
|
44
|
+
</style>
|