mbkbucket 1.5.1 → 2.1.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/.env.example +2 -2
- package/README.md +72 -0
- package/index.d.ts +87 -49
- package/index.js +20 -20
- package/lib/config/index.js +34 -98
- package/lib/config/validation.js +89 -0
- package/lib/debug.js +12 -0
- package/lib/routes/api.js +147 -88
- package/lib/routes/index.js +70 -1
- package/lib/routes/view.js +108 -35
- package/lib/s3.js +102 -57
- package/package.json +6 -5
- package/public/bucketadmin.css +571 -18
- package/public/bucketadmin.js +431 -150
- package/test/config-validation.test.js +116 -0
- package/views/bucket.handlebars +66 -31
- package/views/bucketadmincontent.handlebars +40 -19
- package/views/bucketportal.handlebars +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { parseAndValidateMbkbucketVar, parseAndValidateBucketConnection, normalizeBooleanLike } from '../lib/config/validation.js';
|
|
4
|
+
|
|
5
|
+
test('parseAndValidateMbkbucketVar applies defaults when empty', () => {
|
|
6
|
+
const cfg = parseAndValidateMbkbucketVar(undefined);
|
|
7
|
+
assert.equal(cfg.p_view_inline, true);
|
|
8
|
+
assert.equal(cfg.publiView_enabled, false);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('parseAndValidateMbkbucketVar normalizes boolean-like strings', () => {
|
|
12
|
+
const cfg = parseAndValidateMbkbucketVar(JSON.stringify({
|
|
13
|
+
p_view_inline: 'false',
|
|
14
|
+
publiView_enabled: 'true'
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
assert.equal(cfg.p_view_inline, false);
|
|
18
|
+
assert.equal(cfg.publiView_enabled, true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('parseAndValidateMbkbucketVar rejects invalid JSON', () => {
|
|
22
|
+
assert.throws(() => parseAndValidateMbkbucketVar('{oops'), /Invalid JSON/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('parseAndValidateMbkbucketVar rejects invalid boolean values', () => {
|
|
26
|
+
assert.throws(() => parseAndValidateMbkbucketVar(JSON.stringify({
|
|
27
|
+
p_view_inline: 'not-bool'
|
|
28
|
+
})), /must be a boolean/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('parseAndValidateMbkbucketVar accepts common boolean aliases', () => {
|
|
32
|
+
const cfg = parseAndValidateMbkbucketVar(JSON.stringify({
|
|
33
|
+
p_view_inline: ' ON ',
|
|
34
|
+
publiView_enabled: '0'
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
assert.equal(cfg.p_view_inline, true);
|
|
38
|
+
assert.equal(cfg.publiView_enabled, false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('parseAndValidateMbkbucketVar rejects non-object values', () => {
|
|
42
|
+
assert.throws(() => parseAndValidateMbkbucketVar('null'), /must be a valid object/);
|
|
43
|
+
assert.throws(() => parseAndValidateMbkbucketVar('[]'), /must be a valid object/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('normalizeBooleanLike returns untouched for unsupported values', () => {
|
|
47
|
+
assert.equal(normalizeBooleanLike('maybe'), 'maybe');
|
|
48
|
+
assert.equal(normalizeBooleanLike(123), 123);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('parseAndValidateBucketConnection accepts multi-bucket config', () => {
|
|
52
|
+
const raw = JSON.stringify({
|
|
53
|
+
R2_Bucket: {
|
|
54
|
+
BUCKET_NAME: 'my-r2-bucket',
|
|
55
|
+
ACCESS_KEY_ID: 'abc',
|
|
56
|
+
SECRET_ACCESS_KEY: 'def',
|
|
57
|
+
ENDPOINT: 'https://example.r2.cloudflarestorage.com'
|
|
58
|
+
},
|
|
59
|
+
S3_Bucket: {
|
|
60
|
+
BUCKET_NAME: 'my-s3-bucket',
|
|
61
|
+
ACCESS_KEY_ID: 'ghi',
|
|
62
|
+
SECRET_ACCESS_KEY: 'jkl',
|
|
63
|
+
ENDPOINT: 'https://s3.ap-southeast-1.amazonaws.com'
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const cfg = parseAndValidateBucketConnection(raw);
|
|
68
|
+
assert.deepEqual(Object.keys(cfg), ['R2_Bucket', 'S3_Bucket']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('parseAndValidateBucketConnection returns null when unset', () => {
|
|
72
|
+
assert.equal(parseAndValidateBucketConnection(undefined), null);
|
|
73
|
+
assert.equal(parseAndValidateBucketConnection(''), null);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('parseAndValidateBucketConnection rejects invalid JSON', () => {
|
|
77
|
+
assert.throws(() => parseAndValidateBucketConnection('{oops'), /not valid JSON/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('parseAndValidateBucketConnection rejects empty object and array', () => {
|
|
81
|
+
assert.throws(() => parseAndValidateBucketConnection('{}'), /non-empty object/);
|
|
82
|
+
assert.throws(() => parseAndValidateBucketConnection('[]'), /non-empty object/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('parseAndValidateBucketConnection rejects missing required fields', () => {
|
|
86
|
+
const raw = JSON.stringify({
|
|
87
|
+
bad: {
|
|
88
|
+
BUCKET_NAME: 'x',
|
|
89
|
+
ACCESS_KEY_ID: 'y',
|
|
90
|
+
ENDPOINT: 'https://example.com'
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
assert.throws(() => parseAndValidateBucketConnection(raw), /missing required fields/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('parseAndValidateBucketConnection rejects quoted inner object shape', () => {
|
|
98
|
+
const raw = JSON.stringify({
|
|
99
|
+
bad: '{"BUCKET_NAME":"x"}'
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assert.throws(() => parseAndValidateBucketConnection(raw), /must be an object/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('parseAndValidateBucketConnection rejects empty required fields', () => {
|
|
106
|
+
const raw = JSON.stringify({
|
|
107
|
+
bad: {
|
|
108
|
+
BUCKET_NAME: '',
|
|
109
|
+
ACCESS_KEY_ID: 'abc',
|
|
110
|
+
SECRET_ACCESS_KEY: 'def',
|
|
111
|
+
ENDPOINT: 'https://example.com'
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
assert.throws(() => parseAndValidateBucketConnection(raw), /missing required fields/);
|
|
116
|
+
});
|
package/views/bucket.handlebars
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<title>{{#if page}}{{page}} - {{/if}}Web Portal</title>
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
-
<link href="https://fonts.googleapis.com/css2?family=
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
11
11
|
<script src="/mbkauthe/main.js" defer></script>
|
|
12
12
|
<script src="https://portal.mbktech.org/Assets/Scripts/timeoutlogout.js" defer></script>
|
|
13
13
|
<script src="https://portal.mbktech.org/Assets/Scripts/auto-ids.js" defer></script>
|
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
<link rel="icon" type="image/x-icon" href="/icon.svg">
|
|
16
16
|
<style>
|
|
17
17
|
:root {
|
|
18
|
-
--primary-color-ml: #
|
|
19
|
-
--primary-light-color-ml: #
|
|
20
|
-
--primary-dark-color-ml: #
|
|
21
|
-
--secondary-color-ml: #
|
|
22
|
-
--dark-color-ml: #
|
|
23
|
-
--light-color-ml: #
|
|
24
|
-
--gray-color-ml: #
|
|
25
|
-
--gray-light-color-ml: #
|
|
18
|
+
--primary-color-ml: #0f766e;
|
|
19
|
+
--primary-light-color-ml: #d7f4ee;
|
|
20
|
+
--primary-dark-color-ml: #0b4f4a;
|
|
21
|
+
--secondary-color-ml: #1f8f84;
|
|
22
|
+
--dark-color-ml: #1f2a2a;
|
|
23
|
+
--light-color-ml: #f4f7f3;
|
|
24
|
+
--gray-color-ml: #667c79;
|
|
25
|
+
--gray-light-color-ml: #dce8e3;
|
|
26
26
|
--danger-color-ml: #dc2626;
|
|
27
27
|
--warning-color-ml: #d97706;
|
|
28
28
|
--success-color-ml: #16a34a;
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
--color-accent: var(--primary-color-ml);
|
|
40
40
|
--color-text: var(--dark-color-ml);
|
|
41
41
|
--color-bg: var(--light-color-ml);
|
|
42
|
-
--font-family: "
|
|
43
|
-
--font-family-headings: "
|
|
42
|
+
--font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
|
43
|
+
--font-family-headings: "Sora", "Segoe UI", sans-serif
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
* {
|
|
@@ -50,25 +50,56 @@
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
body {
|
|
53
|
-
font-family:
|
|
54
|
-
background:
|
|
53
|
+
font-family: var(--font-family);
|
|
54
|
+
background:
|
|
55
|
+
radial-gradient(circle at 12% 14%, rgba(16, 185, 129, 0.12), transparent 44%),
|
|
56
|
+
radial-gradient(circle at 86% 8%, rgba(244, 114, 38, 0.12), transparent 40%),
|
|
57
|
+
linear-gradient(160deg, #eef7f2 0, #f8fbf7 52%, #eaf2ef 100%);
|
|
55
58
|
color: var(--dark-color-ml);
|
|
56
59
|
display: flex;
|
|
57
60
|
min-height: 100vh;
|
|
58
61
|
overflow-x: hidden;
|
|
59
62
|
line-height: 1.6;
|
|
60
|
-
font-size: 14px
|
|
63
|
+
font-size: 14px;
|
|
64
|
+
position: relative;
|
|
65
|
+
isolation: isolate
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
body::before,
|
|
69
|
+
body::after {
|
|
70
|
+
content: "";
|
|
71
|
+
position: fixed;
|
|
72
|
+
border-radius: 999px;
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
z-index: -1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
body::before {
|
|
78
|
+
width: 260px;
|
|
79
|
+
height: 260px;
|
|
80
|
+
left: -80px;
|
|
81
|
+
top: 18vh;
|
|
82
|
+
background: radial-gradient(circle, rgba(15, 118, 110, 0.16) 0, rgba(15, 118, 110, 0) 70%)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
body::after {
|
|
86
|
+
width: 300px;
|
|
87
|
+
height: 300px;
|
|
88
|
+
right: -120px;
|
|
89
|
+
bottom: 10vh;
|
|
90
|
+
background: radial-gradient(circle, rgba(251, 146, 60, 0.18) 0, rgba(251, 146, 60, 0) 72%)
|
|
61
91
|
}
|
|
62
92
|
|
|
63
93
|
/* Removed unused sidebar/menu styles to reduce CSS size. */
|
|
64
94
|
|
|
65
95
|
.logo {
|
|
66
|
-
font-
|
|
96
|
+
font-family: var(--font-family-headings);
|
|
97
|
+
font-size: 1.65rem;
|
|
67
98
|
font-weight: 700;
|
|
68
|
-
color:
|
|
99
|
+
color: #0b4f4a;
|
|
69
100
|
display: flex;
|
|
70
101
|
align-items: center;
|
|
71
|
-
gap: .
|
|
102
|
+
gap: .5rem
|
|
72
103
|
}
|
|
73
104
|
|
|
74
105
|
.logo img {
|
|
@@ -84,15 +115,15 @@
|
|
|
84
115
|
}
|
|
85
116
|
|
|
86
117
|
.top-nav {
|
|
87
|
-
background:
|
|
118
|
+
background: rgba(255, 255, 255, .85);
|
|
88
119
|
backdrop-filter: blur(10px);
|
|
89
|
-
padding: .5rem
|
|
120
|
+
padding: .75rem 1.5rem;
|
|
90
121
|
display: flex;
|
|
91
122
|
align-items: center;
|
|
92
123
|
justify-content: flex-end;
|
|
93
124
|
box-shadow: var(--shadow-md-ml);
|
|
94
125
|
z-index: 90;
|
|
95
|
-
border-bottom: 1px solid
|
|
126
|
+
border-bottom: 1px solid rgba(255, 255, 255, .75);
|
|
96
127
|
gap: 1rem
|
|
97
128
|
}
|
|
98
129
|
|
|
@@ -100,7 +131,7 @@
|
|
|
100
131
|
|
|
101
132
|
.content-area {
|
|
102
133
|
flex: 1;
|
|
103
|
-
padding:
|
|
134
|
+
padding: 1.6rem;
|
|
104
135
|
overflow-y: auto
|
|
105
136
|
}
|
|
106
137
|
|
|
@@ -125,13 +156,17 @@
|
|
|
125
156
|
display: flex;
|
|
126
157
|
align-items: center;
|
|
127
158
|
justify-content: space-between;
|
|
128
|
-
margin-bottom:
|
|
129
|
-
padding
|
|
130
|
-
border
|
|
159
|
+
margin-bottom: 1.2rem;
|
|
160
|
+
padding: 1rem 1.2rem;
|
|
161
|
+
border: 1px solid rgba(255, 255, 255, .9);
|
|
162
|
+
border-radius: 1rem;
|
|
163
|
+
background: rgba(255, 255, 255, .74);
|
|
164
|
+
box-shadow: 0 12px 34px rgba(15, 23, 42, .07)
|
|
131
165
|
}
|
|
132
166
|
|
|
133
167
|
.page-title {
|
|
134
|
-
font-
|
|
168
|
+
font-family: var(--font-family-headings);
|
|
169
|
+
font-size: 1.5rem;
|
|
135
170
|
font-weight: 700;
|
|
136
171
|
color: var(--dark-color-ml);
|
|
137
172
|
margin: 0
|
|
@@ -142,10 +177,10 @@
|
|
|
142
177
|
align-items: center;
|
|
143
178
|
color: var(--gray-color-ml);
|
|
144
179
|
font-size: .875rem;
|
|
145
|
-
background:
|
|
146
|
-
padding: .
|
|
147
|
-
border-radius:
|
|
148
|
-
border: 1px solid
|
|
180
|
+
background: linear-gradient(135deg, rgba(15, 118, 110, .08), rgba(251, 146, 60, .08));
|
|
181
|
+
padding: .4rem .85rem;
|
|
182
|
+
border-radius: 999px;
|
|
183
|
+
border: 1px solid rgba(15, 118, 110, .15)
|
|
149
184
|
}
|
|
150
185
|
|
|
151
186
|
.breadcrumb a {
|
|
@@ -204,8 +239,8 @@
|
|
|
204
239
|
<div class="main-content">
|
|
205
240
|
<nav class="top-nav">
|
|
206
241
|
<div class="logo">
|
|
207
|
-
<img onclick="window.location='/'" src="/icon.svg" alt="Logo">
|
|
208
|
-
<span onclick="window.location='/'" style="cursor:pointer;">BKBucket</span>
|
|
242
|
+
<img onclick="window.location='/mbkbucket'" src="/icon.svg" alt="Logo">
|
|
243
|
+
<span onclick="window.location='/mbkbucket'" style="cursor:pointer;">BKBucket</span>
|
|
209
244
|
</div>
|
|
210
245
|
|
|
211
246
|
{{> profilemenu }}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{{!-- File Management System - Bucket Dashboard --}}
|
|
2
|
-
<link rel="stylesheet" href="/mbkbucket/bucketadmin.css
|
|
2
|
+
<link rel="stylesheet" href="/mbkbucket/bucketadmin.css{{mbkbucket_cachebuster}}">
|
|
3
3
|
<div id="bucketAppData" data-app-name="{{APP_NAME}}" hidden></div>
|
|
4
4
|
<!-- File Statistics -->
|
|
5
|
-
<div class="dashboard-card file-stats">
|
|
5
|
+
<div class="dashboard-card file-stats reveal-item reveal-delay-1">
|
|
6
6
|
<div class="card-body">
|
|
7
7
|
<div class="stats-content">
|
|
8
8
|
<div class="stats-icon">
|
|
@@ -12,10 +12,24 @@
|
|
|
12
12
|
<h2 id="totalFilesCount">Loading...</h2>
|
|
13
13
|
<p id="filterInfo">Loading file information...</p>
|
|
14
14
|
</div>
|
|
15
|
-
<div
|
|
16
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
15
|
+
<div class="bucket-switcher-wrap">
|
|
16
|
+
<div class="bucket-switcher" role="group" aria-label="Bucket selection">
|
|
17
|
+
<label for="bucketSelector" class="bucket-switcher-label">Active Bucket</label>
|
|
18
|
+
<div class="bucket-switcher-control">
|
|
19
|
+
<i class="fas fa-database bucket-switcher-icon" aria-hidden="true"></i>
|
|
20
|
+
<select id="bucketSelector" class="form-control-sm bucket-switcher-select" aria-label="Select connected bucket">
|
|
21
|
+
{{#if bucketOptions.length}}
|
|
22
|
+
{{#each bucketOptions}}
|
|
23
|
+
<option value="{{name}}" {{#if isSelected}}selected{{/if}}>{{name}}{{#if isDefault}} (defualt){{/if}}</option>
|
|
24
|
+
{{/each}}
|
|
25
|
+
{{else}}
|
|
26
|
+
<option value="">No buckets configured</option>
|
|
27
|
+
{{/if}}
|
|
28
|
+
</select>
|
|
29
|
+
<i class="fas fa-chevron-down bucket-switcher-caret" aria-hidden="true"></i>
|
|
30
|
+
</div>
|
|
31
|
+
<small id="bucketSwitchStatus" class="bucket-switcher-status is-hidden">Switching bucket...</small>
|
|
32
|
+
</div>
|
|
19
33
|
</div>
|
|
20
34
|
</div>
|
|
21
35
|
</div>
|
|
@@ -23,7 +37,7 @@
|
|
|
23
37
|
|
|
24
38
|
{{> bucketadmin_alerts }}
|
|
25
39
|
|
|
26
|
-
<div class="mb-4">
|
|
40
|
+
<div class="mb-4 reveal-item reveal-delay-2">
|
|
27
41
|
<!-- Upload Form -->
|
|
28
42
|
<div class="col-lg-12">
|
|
29
43
|
<div class="dashboard-card h-100">
|
|
@@ -51,7 +65,7 @@
|
|
|
51
65
|
<div class="drop-hint">Supports multiple files</div>
|
|
52
66
|
</div>
|
|
53
67
|
</div>
|
|
54
|
-
<input type="file" class="form-control d-none"
|
|
68
|
+
<input type="file" class="form-control d-none" id="fileInput" name="files" multiple>
|
|
55
69
|
<div class="form-text">
|
|
56
70
|
<i class="fas fa-info-circle"></i>
|
|
57
71
|
You can drop multiple files. Large files will use chunked upload when available.
|
|
@@ -62,13 +76,13 @@
|
|
|
62
76
|
<div id="uploadQueue" class="mb-3"></div>
|
|
63
77
|
|
|
64
78
|
<!-- Upload Progress (global fallback) -->
|
|
65
|
-
<div id="uploadProgress" class="mb-3
|
|
79
|
+
<div id="uploadProgress" class="mb-3 is-hidden">
|
|
66
80
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
67
81
|
<span class="upload-label">Uploading...</span>
|
|
68
82
|
<span id="uploadPercent" class="upload-percent">0%</span>
|
|
69
83
|
</div>
|
|
70
84
|
<div class="progress-container">
|
|
71
|
-
<div id="progressBar" class="progress-bar
|
|
85
|
+
<div id="progressBar" class="progress-bar progress-bar-empty"></div>
|
|
72
86
|
</div>
|
|
73
87
|
<div class="upload-details mt-2">
|
|
74
88
|
<small id="uploadDetails" class="text-muted"></small>
|
|
@@ -92,7 +106,7 @@
|
|
|
92
106
|
</div>
|
|
93
107
|
|
|
94
108
|
<!-- File List -->
|
|
95
|
-
<div class="dashboard-card">
|
|
109
|
+
<div class="dashboard-card reveal-item reveal-delay-3">
|
|
96
110
|
<div class="card-header file-list-header">
|
|
97
111
|
<h5 class="card-title">
|
|
98
112
|
<div>
|
|
@@ -104,29 +118,36 @@
|
|
|
104
118
|
<!-- Inline Search (moved from separate card) -->
|
|
105
119
|
<div class="search-inline">
|
|
106
120
|
<input type="text" class="form-control form-control-sm" id="searchInput" placeholder="Search files and folders..." aria-label="Search files">
|
|
107
|
-
<button type="button" class="btn btn-sm btn-primary"
|
|
108
|
-
<button type="button" class="btn btn-sm btn-outline-secondary"
|
|
121
|
+
<button type="button" class="btn btn-sm btn-primary" data-action="search" aria-label="Search"><i class="fas fa-search"></i></button>
|
|
122
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" data-action="clear-search" aria-label="Clear search"><i class="fas fa-times"></i></button>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div class="quick-filter-group" role="group" aria-label="Quick filters">
|
|
126
|
+
<button type="button" class="btn btn-sm quick-filter-btn active" data-filter="all">All</button>
|
|
127
|
+
<button type="button" class="btn btn-sm quick-filter-btn" data-filter="media">Media</button>
|
|
128
|
+
<button type="button" class="btn btn-sm quick-filter-btn" data-filter="docs">Docs</button>
|
|
129
|
+
<button type="button" class="btn btn-sm quick-filter-btn" data-filter="code">Code</button>
|
|
109
130
|
</div>
|
|
110
131
|
|
|
111
132
|
<div class="btn-group" role="group" aria-label="Actions">
|
|
112
|
-
<button type="button" class="btn btn-sm btn-success"
|
|
133
|
+
<button type="button" class="btn btn-sm btn-success" data-action="create-file" title="New Text File">
|
|
113
134
|
<i class="fas fa-file-alt"></i> New File
|
|
114
135
|
</button>
|
|
115
|
-
<button type="button" class="btn btn-sm btn-outline-success"
|
|
136
|
+
<button type="button" class="btn btn-sm btn-outline-success" data-action="create-folder" title="New Folder">
|
|
116
137
|
<i class="fas fa-folder-plus"></i> New Folder
|
|
117
138
|
</button>
|
|
118
139
|
</div>
|
|
119
140
|
|
|
120
141
|
<div class="btn-group" role="group" aria-label="View Mode">
|
|
121
|
-
<button type="button" class="btn btn-sm btn-light view-toggle-btn view-btn-left active" id="viewBtnFolder"
|
|
142
|
+
<button type="button" class="btn btn-sm btn-light view-toggle-btn view-btn-left active" id="viewBtnFolder" data-view-mode="folder" title="Folder View">
|
|
122
143
|
<i class="fas fa-folder"></i> Folders
|
|
123
144
|
</button>
|
|
124
|
-
<button type="button" class="btn btn-sm btn-outline-light view-toggle-btn view-btn-right" id="viewBtnFlat"
|
|
145
|
+
<button type="button" class="btn btn-sm btn-outline-light view-toggle-btn view-btn-right" id="viewBtnFlat" data-view-mode="flat" title="Flat List (All Files)">
|
|
125
146
|
<i class="fas fa-list-ul"></i> Flat View
|
|
126
147
|
</button>
|
|
127
148
|
</div>
|
|
128
149
|
</div>
|
|
129
|
-
<div class="card-body
|
|
150
|
+
<div class="card-body file-list-card-body">
|
|
130
151
|
<div id="fileListContainer">
|
|
131
152
|
{{> bucketadmin_filelist_skeleton }}
|
|
132
153
|
</div>
|
|
@@ -148,4 +169,4 @@
|
|
|
148
169
|
<!-- Toast Container -->
|
|
149
170
|
<div id="toast-container" class="toast-container"></div>
|
|
150
171
|
|
|
151
|
-
<script src="/mbkbucket/bucketadmin.js
|
|
172
|
+
<script src="/mbkbucket/bucketadmin.js{{mbkbucket_cachebuster}}"></script>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{> bucketadmincontent }}
|