@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.
Files changed (84) hide show
  1. package/.babelrc +1 -0
  2. package/.eslintrc.js +22 -0
  3. package/.github/workflows/node.js.yml +29 -0
  4. package/.idea/modules.xml +8 -0
  5. package/.idea/ui.iml +10 -0
  6. package/.jshintrc +3 -0
  7. package/README.md +7 -0
  8. package/assets/oxygen-icon.png +0 -0
  9. package/jest.init.js +1 -0
  10. package/package.json +72 -0
  11. package/src/AuthApi.js +116 -0
  12. package/src/CrudApi.js +112 -0
  13. package/src/EventsApi.js +16 -0
  14. package/src/GroupsApi.js +9 -0
  15. package/src/Internationalize.js +31 -0
  16. package/src/MediaApi.js +52 -0
  17. package/src/MediaDirectoryApi.js +62 -0
  18. package/src/PreferencesApi.js +47 -0
  19. package/src/UserPermissions.js +66 -0
  20. package/src/UserPreferences.js +69 -0
  21. package/src/UserPreferences.test.js +23 -0
  22. package/src/UsersApi.js +41 -0
  23. package/src/api.js +209 -0
  24. package/src/components/App.vue +61 -0
  25. package/src/components/AuthenticatedLayout.vue +254 -0
  26. package/src/components/AuthenticationLog.vue +196 -0
  27. package/src/components/CodeEditor.vue +90 -0
  28. package/src/components/EditButtonOnRowHover.vue +21 -0
  29. package/src/components/Error404.vue +25 -0
  30. package/src/components/EventsChooser.vue +88 -0
  31. package/src/components/EventsTable.vue +82 -0
  32. package/src/components/GenericEditableField.vue +74 -0
  33. package/src/components/GroupsChooser.vue +58 -0
  34. package/src/components/GroupsList.vue +129 -0
  35. package/src/components/ImportExport.vue +45 -0
  36. package/src/components/LegacyPage.vue +256 -0
  37. package/src/components/UserJoined.vue +35 -0
  38. package/src/components/UserManagement.vue +168 -0
  39. package/src/components/UserProfileForm.vue +214 -0
  40. package/src/components/ViewProfile.vue +32 -0
  41. package/src/components/auth/Auth404.vue +16 -0
  42. package/src/components/auth/Login.vue +135 -0
  43. package/src/components/auth/LoginLogo.vue +30 -0
  44. package/src/components/auth/Logout.vue +26 -0
  45. package/src/components/auth/PasswordRemind.vue +71 -0
  46. package/src/components/auth/PasswordReset.vue +97 -0
  47. package/src/components/auth/TwoFactorSetup.vue +115 -0
  48. package/src/components/auth/VerifyEmail.vue +71 -0
  49. package/src/components/auth/WelcomeFloat.vue +87 -0
  50. package/src/components/auth/login.scss +17 -0
  51. package/src/components/media/MediaChooseDirectory.vue +129 -0
  52. package/src/components/media/MediaDirectory.vue +109 -0
  53. package/src/components/media/MediaInsertModal.vue +88 -0
  54. package/src/components/media/MediaItem.vue +282 -0
  55. package/src/components/media/MediaItemPreview.vue +45 -0
  56. package/src/components/media/MediaList.vue +305 -0
  57. package/src/components/media/MediaPage.vue +44 -0
  58. package/src/components/media/MediaResponsiveImages.vue +51 -0
  59. package/src/components/media/MediaUpload.vue +133 -0
  60. package/src/components/media/media.scss +51 -0
  61. package/src/components/preferences/PreferencesAdminAppearance.vue +22 -0
  62. package/src/components/preferences/PreferencesAuthentication.vue +27 -0
  63. package/src/components/preferences/PreferencesEventTemplates.vue +22 -0
  64. package/src/components/preferences/PreferencesField.vue +215 -0
  65. package/src/components/preferences/PreferencesList.vue +50 -0
  66. package/src/components/preferences/PreferencesPageTemplates.vue +23 -0
  67. package/src/components/preferences/PreferencesSiteAppearance.vue +22 -0
  68. package/src/components/preferences/PreferencesThemeChooser.vue +73 -0
  69. package/src/components/preferences/ShowIfPermitted.vue +37 -0
  70. package/src/components/preferences/UserPreferences.vue +30 -0
  71. package/src/components/users/CreateUserModal.vue +73 -0
  72. package/src/components/util.css +47 -0
  73. package/src/icons.js +90 -0
  74. package/src/main.js +112 -0
  75. package/src/modules/LegacyPages.js +18 -0
  76. package/src/modules/Media.js +45 -0
  77. package/src/modules/UserManagement.js +24 -0
  78. package/src/routes/index.js +92 -0
  79. package/src/store/index.js +70 -0
  80. package/src/styles/_variables.scss +23 -0
  81. package/src/styles/app.scss +76 -0
  82. package/src/unsavedChanges.js +16 -0
  83. package/src/util.js +65 -0
  84. 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>