@testdracul/media-frontend 2.0.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.development +4 -0
- package/.env.example +3 -0
- package/.eslintrc.json +25 -0
- package/babel.config.js +5 -0
- package/dist/dracul-media-frontend.es.js +16238 -0
- package/dist/dracul-media-frontend.umd.js +586 -0
- package/dist/media-frontend.css +1 -0
- package/docs-en.md +45 -0
- package/docs-es.md +45 -0
- package/package.json +56 -0
- package/readme.md +36 -0
- package/src/components/CsvWebViewer/CsvWebViewer.vue +81 -0
- package/src/components/CsvWebViewer/index.ts +4 -0
- package/src/components/FileUpload/FileUpload.vue +94 -0
- package/src/components/FileUpload/index.ts +4 -0
- package/src/components/FileUploadButton/FileUploadButton.vue +127 -0
- package/src/components/FileUploadButton/index.ts +4 -0
- package/src/components/FileUploadExpiration/FileUploadExpiration.vue +274 -0
- package/src/components/FileUploadExpiration/index.ts +4 -0
- package/src/components/FileUploadExpress/FileUploadExpress.vue +208 -0
- package/src/components/FileUploadExpress/index.ts +4 -0
- package/src/components/FileView/FileView.vue +336 -0
- package/src/components/FileView/index.ts +4 -0
- package/src/components/GroupsShow/GroupsShow.vue +40 -0
- package/src/components/GroupsShow/index.ts +4 -0
- package/src/components/MediaField/MediaField.vue +62 -0
- package/src/components/MediaField/index.ts +4 -0
- package/src/components/PdfWebViewer/PdfWebViewer.vue +81 -0
- package/src/components/PdfWebViewer/index.ts +4 -0
- package/src/components/UsersShow/UsersShow.vue +39 -0
- package/src/components/UsersShow/index.ts +4 -0
- package/src/components/XlsxWebViewer/XlsxWebViewer.vue +70 -0
- package/src/components/XlsxWebViewer/index.ts +4 -0
- package/src/helpers/redeableBytes.ts +9 -0
- package/src/i18n/index.ts +22 -0
- package/src/i18n/messages/DocMessages.ts +31 -0
- package/src/i18n/messages/ExtraMessages.ts +29 -0
- package/src/i18n/messages/FileMessages.ts +223 -0
- package/src/i18n/messages/UserStorageMessages.ts +145 -0
- package/src/i18n/permissions/FilePermissionMessages.ts +50 -0
- package/src/i18n/permissions/OldPermissionMessages.ts +59 -0
- package/src/i18n/permissions/UserStoragePermissionMessages.ts +40 -0
- package/src/index.ts +70 -0
- package/src/mixins/readableBytesMixin.ts +9 -0
- package/src/pages/FileManagementPage/FileCreate/FileCreate.vue +108 -0
- package/src/pages/FileManagementPage/FileCreate/index.ts +3 -0
- package/src/pages/FileManagementPage/FileCrud/FileCrud.vue +133 -0
- package/src/pages/FileManagementPage/FileCrud/index.ts +4 -0
- package/src/pages/FileManagementPage/FileDelete/FileDelete.vue +61 -0
- package/src/pages/FileManagementPage/FileDelete/index.ts +3 -0
- package/src/pages/FileManagementPage/FileFilters/FileFilters.vue +150 -0
- package/src/pages/FileManagementPage/FileFilters/index.ts +3 -0
- package/src/pages/FileManagementPage/FileForm/FileForm.vue +184 -0
- package/src/pages/FileManagementPage/FileForm/UserCombobox.vue +66 -0
- package/src/pages/FileManagementPage/FileForm/index.ts +3 -0
- package/src/pages/FileManagementPage/FileList/FileEditButton.vue +410 -0
- package/src/pages/FileManagementPage/FileList/FileList.vue +178 -0
- package/src/pages/FileManagementPage/FileList/index.ts +4 -0
- package/src/pages/FileManagementPage/FileShow/FileShow.vue +23 -0
- package/src/pages/FileManagementPage/FileShow/FileShowData.vue +35 -0
- package/src/pages/FileManagementPage/FileShow/index.ts +3 -0
- package/src/pages/FileManagementPage/FileUpdate/FileUpdate.vue +107 -0
- package/src/pages/FileManagementPage/FileUpdate/index.ts +4 -0
- package/src/pages/FileManagementPage/index.vue +20 -0
- package/src/pages/MediaDocPage/MediaDocCard.vue +35 -0
- package/src/pages/MediaDocPage/MediaDocPage.vue +78 -0
- package/src/pages/UserStoragePage/UserStorage.vue +311 -0
- package/src/pages/UserStoragePage/UserStorageForm/UserStorageForm.vue +172 -0
- package/src/pages/UserStoragePage/UserStorageUpdate/UserStorageUpdate.vue +91 -0
- package/src/pages/UserStoragePage/index.vue +14 -0
- package/src/providers/FileMetricsProvider.ts +47 -0
- package/src/providers/FileProvider.ts +60 -0
- package/src/providers/UploadProvider.ts +32 -0
- package/src/providers/UserStorageProvider.ts +47 -0
- package/src/providers/gql/almacenamientoPorUsuario.graphql +10 -0
- package/src/providers/gql/cantidadArchivosPorUsuario.graphql +10 -0
- package/src/providers/gql/fetchMediaVariables.graphql +6 -0
- package/src/providers/gql/fileCreate.graphql +27 -0
- package/src/providers/gql/fileDelete.graphql +7 -0
- package/src/providers/gql/fileFetch.graphql +29 -0
- package/src/providers/gql/fileFind.graphql +29 -0
- package/src/providers/gql/fileGlobalMetrics.graphql +6 -0
- package/src/providers/gql/filePaginate.graphql +38 -0
- package/src/providers/gql/fileUpdate.graphql +29 -0
- package/src/providers/gql/fileUpload.graphql +29 -0
- package/src/providers/gql/fileUploadAnonymous.graphql +25 -0
- package/src/providers/gql/fileUserMetrics.graphql +9 -0
- package/src/providers/gql/userStorageFetch.graphql +17 -0
- package/src/providers/gql/userStorageFindByUser.graphql +17 -0
- package/src/providers/gql/userStorageUpdate.graphql +31 -0
- package/src/routes/index.ts +32 -0
- package/vite.config.ts +65 -0
- package/vue.config.js +22 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-col cols="12" sm="6">
|
|
3
|
+
<v-select
|
|
4
|
+
prepend-inner-icon="mdi-account-circle"
|
|
5
|
+
:items="items"
|
|
6
|
+
:item-title="'name'"
|
|
7
|
+
:item-value="'id'"
|
|
8
|
+
v-model="item"
|
|
9
|
+
:label="$t('media.file.createdBy')"
|
|
10
|
+
:loading="loading"
|
|
11
|
+
:error="hasInputErrors('createdBy')"
|
|
12
|
+
:error-messages="getInputErrors('createdBy')"
|
|
13
|
+
color="secondary"
|
|
14
|
+
item-color="secondary"
|
|
15
|
+
|
|
16
|
+
></v-select>
|
|
17
|
+
</v-col>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup>
|
|
21
|
+
import { ref, computed, onMounted } from 'vue'
|
|
22
|
+
import {InputErrorsByProps, RequiredRule} from '@testdracul/common-frontend'
|
|
23
|
+
import UserProvider from "../../../providers/UserProvider"
|
|
24
|
+
|
|
25
|
+
const props = defineProps({
|
|
26
|
+
modelValue: { type: String }
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const emit = defineEmits(['update:modelValue'])
|
|
30
|
+
|
|
31
|
+
const items = ref([])
|
|
32
|
+
const loading = ref(false)
|
|
33
|
+
const formRef = ref(null)
|
|
34
|
+
|
|
35
|
+
const item = computed({
|
|
36
|
+
get() { return props.modelValue },
|
|
37
|
+
set(val) { emit('update:modelValue', val) }
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const validate = async () => {
|
|
41
|
+
if (formRef.value) {
|
|
42
|
+
const { valid } = await formRef.value.validate()
|
|
43
|
+
return valid
|
|
44
|
+
}
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
defineExpose({ validate })
|
|
49
|
+
|
|
50
|
+
const fetch = () => {
|
|
51
|
+
loading.value = true
|
|
52
|
+
UserProvider.fetchUsers().then(r => {
|
|
53
|
+
items.value = r.data.userFetch
|
|
54
|
+
}).catch(err => console.error(err))
|
|
55
|
+
.finally(() => loading.value = false)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
onMounted(() => {
|
|
59
|
+
fetch()
|
|
60
|
+
})
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<style scoped>
|
|
64
|
+
|
|
65
|
+
</style>
|
|
66
|
+
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-tooltip location="bottom">
|
|
3
|
+
<template v-slot:activator="{ props }">
|
|
4
|
+
<v-btn
|
|
5
|
+
icon="mdi-note-edit"
|
|
6
|
+
size="x-small"
|
|
7
|
+
color="primary"
|
|
8
|
+
variant="text"
|
|
9
|
+
class="mx-1"
|
|
10
|
+
v-bind="props"
|
|
11
|
+
@click="dialog = true"
|
|
12
|
+
/>
|
|
13
|
+
</template>
|
|
14
|
+
<span>Editar archivo</span>
|
|
15
|
+
</v-tooltip>
|
|
16
|
+
|
|
17
|
+
<v-dialog v-model="dialog" max-width="80vw" persistent>
|
|
18
|
+
<v-card style="max-height: 80vh; display: flex; flex-direction: column">
|
|
19
|
+
<v-toolbar color="primary" theme="dark">
|
|
20
|
+
<v-toolbar-title class="mt-3">
|
|
21
|
+
Editar archivo
|
|
22
|
+
<div class="text-subtitle-2 mt-n2 pa-0 text-white-50">{{ file.filename }}</div>
|
|
23
|
+
</v-toolbar-title>
|
|
24
|
+
</v-toolbar>
|
|
25
|
+
|
|
26
|
+
<v-card-text class="flex-grow-1 overflow-y-auto pa-4" style="min-height: 0">
|
|
27
|
+
<div class="d-flex mb-2 justify-end">
|
|
28
|
+
|
|
29
|
+
<v-tooltip location="bottom">
|
|
30
|
+
<template v-slot:activator="{ props }">
|
|
31
|
+
<v-btn
|
|
32
|
+
size="small"
|
|
33
|
+
icon="mdi-content-copy"
|
|
34
|
+
variant="text"
|
|
35
|
+
v-bind="props"
|
|
36
|
+
@click="copyToClipboard"
|
|
37
|
+
:disabled="!noteContent"
|
|
38
|
+
/>
|
|
39
|
+
</template>
|
|
40
|
+
<span>Copiar</span>
|
|
41
|
+
</v-tooltip>
|
|
42
|
+
|
|
43
|
+
<v-tooltip location="bottom">
|
|
44
|
+
<template v-slot:activator="{ props }">
|
|
45
|
+
<v-btn
|
|
46
|
+
size="small"
|
|
47
|
+
icon="mdi-backspace-outline"
|
|
48
|
+
variant="text"
|
|
49
|
+
class="ml-2"
|
|
50
|
+
v-bind="props"
|
|
51
|
+
@click="clearContent"
|
|
52
|
+
:disabled="!noteContent"
|
|
53
|
+
/>
|
|
54
|
+
</template>
|
|
55
|
+
<span>Limpiar</span>
|
|
56
|
+
</v-tooltip>
|
|
57
|
+
|
|
58
|
+
<v-tooltip location="bottom">
|
|
59
|
+
<template v-slot:activator="{ props }">
|
|
60
|
+
<v-btn
|
|
61
|
+
size="small"
|
|
62
|
+
icon="mdi-undo"
|
|
63
|
+
variant="text"
|
|
64
|
+
class="ml-2"
|
|
65
|
+
v-bind="props"
|
|
66
|
+
@click="undoAction"
|
|
67
|
+
:disabled="undoStack.length === 0"
|
|
68
|
+
/>
|
|
69
|
+
</template>
|
|
70
|
+
<span>Deshacer</span>
|
|
71
|
+
</v-tooltip>
|
|
72
|
+
|
|
73
|
+
<v-tooltip location="bottom">
|
|
74
|
+
<template v-slot:activator="{ props }">
|
|
75
|
+
<v-btn
|
|
76
|
+
size="small"
|
|
77
|
+
icon="mdi-redo"
|
|
78
|
+
variant="text"
|
|
79
|
+
class="ml-2"
|
|
80
|
+
v-bind="props"
|
|
81
|
+
@click="redoAction"
|
|
82
|
+
:disabled="redoStack.length === 0"
|
|
83
|
+
/>
|
|
84
|
+
</template>
|
|
85
|
+
<span>Rehacer</span>
|
|
86
|
+
</v-tooltip>
|
|
87
|
+
|
|
88
|
+
<v-spacer />
|
|
89
|
+
|
|
90
|
+
<v-tooltip location="bottom">
|
|
91
|
+
<template v-slot:activator="{ props }">
|
|
92
|
+
<v-btn
|
|
93
|
+
size="small"
|
|
94
|
+
:icon="isFormatted ? 'mdi-format-letter-case' : 'mdi-format-align-left'"
|
|
95
|
+
variant="text"
|
|
96
|
+
v-bind="props"
|
|
97
|
+
@click="toggleFormat"
|
|
98
|
+
:disabled="!isJson"
|
|
99
|
+
/>
|
|
100
|
+
</template>
|
|
101
|
+
<span>{{ isFormatted ? 'Minificar JSON' : 'Formatear JSON' }}</span>
|
|
102
|
+
</v-tooltip>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div
|
|
106
|
+
ref="editor"
|
|
107
|
+
class="dracul-json-editor"
|
|
108
|
+
:class="{ 'json-editor-error': hasErrors }"
|
|
109
|
+
contenteditable="true"
|
|
110
|
+
spellcheck="false"
|
|
111
|
+
autocorrect="off"
|
|
112
|
+
autocomplete="off"
|
|
113
|
+
autocapitalize="off"
|
|
114
|
+
@input="onEdit"
|
|
115
|
+
@blur="onBlur"
|
|
116
|
+
style="background: #2e2e2e !important; color: #ccc !important; white-space: pre-wrap !important; word-break: break-all !important; font-family: 'Fira Code', 'Consolas', monospace !important; outline: none !important; min-height: 300px !important; padding: 12px !important; border-radius: 4px !important; font-size: 14px !important; line-height: 1.5 !important; border: 1px solid #444 !important;"
|
|
117
|
+
></div>
|
|
118
|
+
|
|
119
|
+
<div v-if="hasErrors" class="error-message">
|
|
120
|
+
{{ errorMessages[0] }}
|
|
121
|
+
</div>
|
|
122
|
+
</v-card-text>
|
|
123
|
+
|
|
124
|
+
<v-divider />
|
|
125
|
+
|
|
126
|
+
<v-card-actions>
|
|
127
|
+
<v-spacer />
|
|
128
|
+
<v-btn variant="text" @click="dialog = false" :disabled="loading">
|
|
129
|
+
{{ t('common.cancel') }}
|
|
130
|
+
</v-btn>
|
|
131
|
+
<v-btn :disabled="hasErrors || loading" color="primary" variant="flat" @click="saveNote" :loading="loading">
|
|
132
|
+
{{ t('common.update') }}
|
|
133
|
+
</v-btn>
|
|
134
|
+
</v-card-actions>
|
|
135
|
+
</v-card>
|
|
136
|
+
</v-dialog>
|
|
137
|
+
</template>
|
|
138
|
+
|
|
139
|
+
<script setup>
|
|
140
|
+
import { ref, computed, watch, nextTick } from 'vue'
|
|
141
|
+
import FileProvider from "../../../providers/FileProvider"
|
|
142
|
+
import { useStore } from 'vuex'
|
|
143
|
+
import { useI18n } from 'vue-i18n'
|
|
144
|
+
|
|
145
|
+
// Props y Emits
|
|
146
|
+
const props = defineProps({
|
|
147
|
+
file: {
|
|
148
|
+
type: Object,
|
|
149
|
+
required: true
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const emit = defineEmits(['file-updated', 'itemUpdated'])
|
|
154
|
+
|
|
155
|
+
// Plugins
|
|
156
|
+
const store = useStore()
|
|
157
|
+
const { t } = useI18n()
|
|
158
|
+
|
|
159
|
+
// Estado Reactivo
|
|
160
|
+
const dialog = ref(false)
|
|
161
|
+
const noteContent = ref('')
|
|
162
|
+
const originalContent = ref('')
|
|
163
|
+
const isFormatted = ref(false)
|
|
164
|
+
const loading = ref(false)
|
|
165
|
+
const errorMessages = ref([])
|
|
166
|
+
const undoStack = ref([])
|
|
167
|
+
const redoStack = ref([])
|
|
168
|
+
|
|
169
|
+
// Referencia al div contenteditable
|
|
170
|
+
const editor = ref(null)
|
|
171
|
+
|
|
172
|
+
// Computados
|
|
173
|
+
const hasErrors = computed(() => errorMessages.value.length > 0)
|
|
174
|
+
const isJson = computed(() => {
|
|
175
|
+
if (!props.file?.extension) return false
|
|
176
|
+
const ext = props.file.extension.toLowerCase()
|
|
177
|
+
return (ext === '.json' || ext === 'json') && noteContent.value.trim() !== ''
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// === MÉTODO MODIFICADO (Respetando tu nueva lógica) ===
|
|
181
|
+
const loadFileText = async (retries = 3) => {
|
|
182
|
+
try {
|
|
183
|
+
const authToken = store.state.user.access_token
|
|
184
|
+
if (!props.file.url) throw new Error("File URL is missing")
|
|
185
|
+
|
|
186
|
+
const cacheBuster = `t=${Date.now()}`
|
|
187
|
+
const finalUrl = props.file.url.includes('?')
|
|
188
|
+
? `${props.file.url}&${cacheBuster}`
|
|
189
|
+
: `${props.file.url}?${cacheBuster}`
|
|
190
|
+
|
|
191
|
+
const requestOptions = {
|
|
192
|
+
method: 'GET',
|
|
193
|
+
headers: {}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (authToken) {
|
|
197
|
+
requestOptions.headers['Authorization'] = `Bearer ${authToken}`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let response = await fetch(finalUrl, requestOptions)
|
|
201
|
+
|
|
202
|
+
if (!response.ok && authToken) {
|
|
203
|
+
delete requestOptions.headers['Authorization']
|
|
204
|
+
response = await fetch(finalUrl, requestOptions)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`)
|
|
208
|
+
|
|
209
|
+
noteContent.value = await response.text()
|
|
210
|
+
originalContent.value = noteContent.value
|
|
211
|
+
isFormatted.value = false
|
|
212
|
+
undoStack.value = []
|
|
213
|
+
redoStack.value = []
|
|
214
|
+
|
|
215
|
+
validateJson()
|
|
216
|
+
await nextTick()
|
|
217
|
+
setTimeout(() => renderEditor(), 150)
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error(`Load attempt failed (${retries} retries left):`, error)
|
|
220
|
+
if (retries > 0) {
|
|
221
|
+
// Esperamos 1 segundo y reintentamos (el server puede estar reiniciandose)
|
|
222
|
+
setTimeout(() => loadFileText(retries - 1), 1000)
|
|
223
|
+
} else {
|
|
224
|
+
noteContent.value = ''
|
|
225
|
+
errorMessages.value = [`Error cargando el contenido: ${error.message}. Intenta de nuevo en unos segundos.`]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const validateJson = () => {
|
|
231
|
+
errorMessages.value = []
|
|
232
|
+
if (isJson.value) {
|
|
233
|
+
try {
|
|
234
|
+
JSON.parse(noteContent.value)
|
|
235
|
+
} catch {
|
|
236
|
+
errorMessages.value = ['Formato JSON inválido']
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const toggleFormat = () => {
|
|
242
|
+
if (!isJson.value) return
|
|
243
|
+
try {
|
|
244
|
+
const obj = JSON.parse(noteContent.value)
|
|
245
|
+
pushUndo()
|
|
246
|
+
if (!isFormatted.value) {
|
|
247
|
+
noteContent.value = JSON.stringify(obj, null, 2)
|
|
248
|
+
isFormatted.value = true
|
|
249
|
+
} else {
|
|
250
|
+
noteContent.value = JSON.stringify(obj)
|
|
251
|
+
isFormatted.value = false
|
|
252
|
+
}
|
|
253
|
+
validateJson()
|
|
254
|
+
renderEditor()
|
|
255
|
+
} catch (e) {
|
|
256
|
+
console.error("Format error:", e)
|
|
257
|
+
errorMessages.value = ['No se pudo formatear: JSON inválido']
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const onEdit = () => {
|
|
262
|
+
if (editor.value) {
|
|
263
|
+
const newContent = editor.value.innerText
|
|
264
|
+
if (newContent !== noteContent.value) {
|
|
265
|
+
// Guardamos el estado anterior para poder deshacer
|
|
266
|
+
undoStack.value.push(noteContent.value)
|
|
267
|
+
redoStack.value = []
|
|
268
|
+
|
|
269
|
+
noteContent.value = newContent
|
|
270
|
+
isFormatted.value = false
|
|
271
|
+
validateJson()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const onBlur = () => {
|
|
277
|
+
renderEditor()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const pushUndo = () => {
|
|
281
|
+
undoStack.value.push(noteContent.value)
|
|
282
|
+
redoStack.value = []
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const undoAction = () => {
|
|
286
|
+
if (!undoStack.value.length) return
|
|
287
|
+
redoStack.value.push(noteContent.value)
|
|
288
|
+
noteContent.value = undoStack.value.pop()
|
|
289
|
+
validateJson()
|
|
290
|
+
renderEditor()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const redoAction = () => {
|
|
294
|
+
if (!redoStack.value.length) return
|
|
295
|
+
undoStack.value.push(noteContent.value)
|
|
296
|
+
noteContent.value = redoStack.value.pop()
|
|
297
|
+
validateJson()
|
|
298
|
+
renderEditor()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const copyToClipboard = async () => {
|
|
302
|
+
try {
|
|
303
|
+
await navigator.clipboard.writeText(noteContent.value)
|
|
304
|
+
} catch (err) {
|
|
305
|
+
console.error('Fallo al copiar: ', err)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const clearContent = () => {
|
|
310
|
+
pushUndo()
|
|
311
|
+
noteContent.value = ''
|
|
312
|
+
validateJson()
|
|
313
|
+
renderEditor()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const renderEditor = () => {
|
|
317
|
+
if (!editor.value) return
|
|
318
|
+
const text = noteContent.value || ''
|
|
319
|
+
|
|
320
|
+
let html = text
|
|
321
|
+
.replace(/&/g, '&')
|
|
322
|
+
.replace(/</g, '<')
|
|
323
|
+
.replace(/>/g, '>')
|
|
324
|
+
|
|
325
|
+
if (isJson.value) {
|
|
326
|
+
html = html
|
|
327
|
+
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"\s*?)(?=:)/g, '<span class="json-key" style="color: #c5a5c5 !important; font-weight: bold !important;">$1</span>')
|
|
328
|
+
.replace(/(:\s*)("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*")/g, '$1<span class="json-string" style="color: #8dc891 !important;">$2</span>')
|
|
329
|
+
.replace(/\b(true|false|null)\b/g, '<span class="json-boolean" style="color: #f99157 !important; font-weight: bold !important;">$1</span>')
|
|
330
|
+
.replace(/\b(-?\d+\.?\d*)\b(?![^<]*>)/g, '<span class="json-number" style="color: #f99157 !important;">$1</span>')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
editor.value.innerHTML = html
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const saveNote = async () => {
|
|
337
|
+
if (hasErrors.value) return
|
|
338
|
+
loading.value = true
|
|
339
|
+
errorMessages.value = []
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const mimeType = props.file.extension?.toLowerCase() === '.json' ? 'application/json' : 'text/plain'
|
|
343
|
+
const blob = new Blob([noteContent.value], { type: mimeType })
|
|
344
|
+
const filename = props.file.filename || 'file.txt'
|
|
345
|
+
const newFile = new File([blob], filename, { type: mimeType, lastModified: Date.now() })
|
|
346
|
+
|
|
347
|
+
const input = {
|
|
348
|
+
id: props.file.id,
|
|
349
|
+
description: props.file.description || '',
|
|
350
|
+
tags: props.file.tags || [],
|
|
351
|
+
expirationDate: props.file.expirationDate || null,
|
|
352
|
+
isPublic: props.file.isPublic || false,
|
|
353
|
+
groups: props.file.groups || [],
|
|
354
|
+
users: props.file.users || []
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const result = await FileProvider.updateFile(input, newFile)
|
|
358
|
+
|
|
359
|
+
// Verificamos si hay errores de GraphQL en la respuesta
|
|
360
|
+
if (result?.errors && result.errors.length > 0) {
|
|
361
|
+
throw new Error(result.errors[0].message || 'Error en la respuesta del servidor')
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (result?.data?.fileUpdate) {
|
|
365
|
+
dialog.value = false
|
|
366
|
+
emit('file-updated', result.data.fileUpdate)
|
|
367
|
+
|
|
368
|
+
// Añadimos un pequeño delay antes de refrescar la lista para dar tiempo al backend
|
|
369
|
+
setTimeout(() => {
|
|
370
|
+
emit('itemUpdated')
|
|
371
|
+
}, 800)
|
|
372
|
+
} else {
|
|
373
|
+
throw new Error('La respuesta del servidor no fue exitosa')
|
|
374
|
+
}
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.error('Error guardando archivo:', error)
|
|
377
|
+
const msg = error.message.includes('fetch') ? 'Error de conexión con el servidor (posible reinicio)' : error.message
|
|
378
|
+
errorMessages.value = [msg]
|
|
379
|
+
} finally {
|
|
380
|
+
loading.value = false
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Watchers
|
|
385
|
+
watch(dialog, async (open) => {
|
|
386
|
+
if (open) {
|
|
387
|
+
errorMessages.value = []
|
|
388
|
+
await loadFileText()
|
|
389
|
+
}
|
|
390
|
+
})
|
|
391
|
+
</script>
|
|
392
|
+
|
|
393
|
+
<style>
|
|
394
|
+
/* Estilos globales para asegurar que funcionen con innerHTML */
|
|
395
|
+
.dracul-json-editor .json-key { color: #c5a5c5 !important; font-weight: bold !important; }
|
|
396
|
+
.dracul-json-editor .json-string { color: #8dc891 !important; }
|
|
397
|
+
.dracul-json-editor .json-number { color: #f99157 !important; }
|
|
398
|
+
.dracul-json-editor .json-boolean { color: #f99157 !important; font-weight: bold !important; }
|
|
399
|
+
|
|
400
|
+
.dracul-json-editor.json-editor-error {
|
|
401
|
+
border: 1px solid #ff5252 !important;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.error-message {
|
|
405
|
+
color: #ff5252 !important;
|
|
406
|
+
font-size: 0.875rem !important;
|
|
407
|
+
margin-top: 4px !important;
|
|
408
|
+
font-weight: bold !important;
|
|
409
|
+
}
|
|
410
|
+
</style>
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row row wrap>
|
|
3
|
+
|
|
4
|
+
<file-filters
|
|
5
|
+
@updateFilters="setFilters"
|
|
6
|
+
@clearFilter="clearFilters"
|
|
7
|
+
v-model="filters"
|
|
8
|
+
/>
|
|
9
|
+
|
|
10
|
+
<v-col cols="12">
|
|
11
|
+
|
|
12
|
+
<v-data-table-server
|
|
13
|
+
class="mt-3"
|
|
14
|
+
:headers="headers"
|
|
15
|
+
:items="items"
|
|
16
|
+
:search="search"
|
|
17
|
+
:items-length="totalItems"
|
|
18
|
+
:loading="loading"
|
|
19
|
+
v-model:page="pageNumber"
|
|
20
|
+
v-model:items-per-page="itemsPerPage"
|
|
21
|
+
v-model:sort-by="sortBy"
|
|
22
|
+
:items-per-page-options="[5, 10, 25, 50, 100]"
|
|
23
|
+
:items-per-page-text="t('common.itemsPerPageText')"
|
|
24
|
+
@update:options="fetch"
|
|
25
|
+
density="compact"
|
|
26
|
+
hover
|
|
27
|
+
>
|
|
28
|
+
|
|
29
|
+
<template v-slot:[`item.isPublic`]="{ item }">
|
|
30
|
+
<div v-if="item.isPublic">
|
|
31
|
+
<v-icon color="success">mdi-check-circle</v-icon>
|
|
32
|
+
</div>
|
|
33
|
+
<div v-else>
|
|
34
|
+
<v-icon color="error">mdi-close-circle</v-icon>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<template v-slot:no-data>
|
|
39
|
+
<div class="text-center">{{ t('common.noData') }}</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<template v-slot:[`item.size`]="{item}">
|
|
43
|
+
{{ item.size.toFixed(2) }} Mb
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<template v-slot:[`item.type`]="{item}">
|
|
47
|
+
{{ t(`media.file.${item.type}`) }}
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<template v-slot:[`item.createdAt`]="{item}">
|
|
51
|
+
{{ getDateTimeFormat(item.createdAt, true) }}
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<template v-slot:[`item.lastAccess`]="{item}">
|
|
55
|
+
{{ getDateTimeFormat(item.lastAccess, true) }}
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<template v-slot:[`item.action`]="{ item }">
|
|
59
|
+
<show-button @click="$emit('show', item)"/>
|
|
60
|
+
|
|
61
|
+
<edit-button
|
|
62
|
+
v-if="hasPermission('FILE_UPDATE_ALL') ||
|
|
63
|
+
(hasPermission('FILE_UPDATE_OWN') && item.createdBy.user?.id === me?.id)"
|
|
64
|
+
@click="$emit('update', item)"
|
|
65
|
+
/>
|
|
66
|
+
|
|
67
|
+
<file-edit-button v-if="editTextButtonMustBeRender(item.extension) && (hasPermission('FILE_UPDATE_ALL') || (hasPermission('FILE_UPDATE_OWN') && item.createdBy.user?.id === me?.id))"
|
|
68
|
+
:file="item"
|
|
69
|
+
@itemUpdated="$emit('itemUpdated')"
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
<delete-button
|
|
73
|
+
v-if="hasPermission('FILE_DELETE_ALL') ||
|
|
74
|
+
(hasPermission('FILE_DELETE_OWN') && item.createdBy.user?.id === me?.id)"
|
|
75
|
+
@click="$emit('delete', item)"
|
|
76
|
+
/>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
</v-data-table-server>
|
|
80
|
+
</v-col>
|
|
81
|
+
</v-row>
|
|
82
|
+
</template>
|
|
83
|
+
|
|
84
|
+
<script setup>
|
|
85
|
+
import { ref, computed, onBeforeMount } from 'vue'
|
|
86
|
+
import { useStore } from 'vuex'
|
|
87
|
+
import { useI18n } from 'vue-i18n'
|
|
88
|
+
import {DeleteButton, EditButton, ShowButton} from "@testdracul/common-frontend"
|
|
89
|
+
import {useDayjs} from "@testdracul/dayjs-frontend"
|
|
90
|
+
import FileEditButton from "./FileEditButton.vue"
|
|
91
|
+
|
|
92
|
+
// import redeableBytesMixin from "../../../mixins/readableBytesMixin";
|
|
93
|
+
import FileProvider from "../../../providers/FileProvider";
|
|
94
|
+
import FileFilters from "../FileFilters/FileFilters"
|
|
95
|
+
|
|
96
|
+
const emit = defineEmits(['update', 'delete', 'show', 'itemUpdated'])
|
|
97
|
+
|
|
98
|
+
const { t } = useI18n()
|
|
99
|
+
const store = useStore()
|
|
100
|
+
const { getDateTimeFormat } = useDayjs()
|
|
101
|
+
|
|
102
|
+
const items = ref([])
|
|
103
|
+
const totalItems = ref(0)
|
|
104
|
+
const loading = ref(false)
|
|
105
|
+
const sortBy = ref([])
|
|
106
|
+
const itemsPerPage = ref(5)
|
|
107
|
+
const pageNumber = ref(1)
|
|
108
|
+
const search = ref('')
|
|
109
|
+
const filters = ref([
|
|
110
|
+
{ field: 'dateFrom', operator: '$gte', value: null },
|
|
111
|
+
{ field: 'dateTo', operator: '$lte', value: null },
|
|
112
|
+
{ field: 'filename', operator: '$regex', value: null },
|
|
113
|
+
{ field: 'createdBy.user', operator: '$eq', value: null },
|
|
114
|
+
{ field: 'type', operator: '$regex', value: null },
|
|
115
|
+
{ field: 'minSize', operator: '$gte', value: null },
|
|
116
|
+
{ field: 'maxSize', operator: '$lte', value: null },
|
|
117
|
+
{ field: 'isPublic', operator: '$eq', value: null },
|
|
118
|
+
{ field: 'groups', operator: '$eq', value: null },
|
|
119
|
+
{ field: 'users', operator: '$eq', value: null }
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
const hasPermission = (permission) => store.getters.hasPermission(permission)
|
|
123
|
+
const me = computed(() => store.getters.me)
|
|
124
|
+
|
|
125
|
+
const headers = computed(() => [
|
|
126
|
+
{ title: t('media.file.filename'), key: 'filename' },
|
|
127
|
+
{ title: t('media.file.type'), key: 'type' },
|
|
128
|
+
{ title: t('media.file.size'), key: 'size' },
|
|
129
|
+
{ title: t('media.file.createdAt'), key: 'createdAt' },
|
|
130
|
+
{ title: t('media.file.lastAccess'), key: 'lastAccess' },
|
|
131
|
+
{ title: t('media.file.createdBy'), key: 'createdBy.username' },
|
|
132
|
+
{ title: t('media.file.isPublic'), key: 'isPublic' },
|
|
133
|
+
{ title: t('media.file.hits'), key: 'hits' },
|
|
134
|
+
{ title: t('common.actions'), key: 'action', sortable: false },
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
const getOrderBy = computed(() => sortBy.value.length > 0 ? sortBy.value[0].key : null)
|
|
138
|
+
const getOrderDesc = computed(() => sortBy.value.length > 0 ? sortBy.value[0].order === 'desc' : false)
|
|
139
|
+
|
|
140
|
+
const fetch = () => {
|
|
141
|
+
loading.value = true
|
|
142
|
+
FileProvider.paginateFiles(
|
|
143
|
+
pageNumber.value,
|
|
144
|
+
itemsPerPage.value,
|
|
145
|
+
search.value,
|
|
146
|
+
filters.value,
|
|
147
|
+
getOrderBy.value,
|
|
148
|
+
getOrderDesc.value
|
|
149
|
+
).then(r => {
|
|
150
|
+
items.value = r.data.filePaginate.items
|
|
151
|
+
totalItems.value = r.data.filePaginate.totalItems
|
|
152
|
+
}).catch(err => {
|
|
153
|
+
console.error(err)
|
|
154
|
+
}).finally(() => loading.value = false)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const setFilters = (fileFilters) => {
|
|
158
|
+
filters.value = fileFilters
|
|
159
|
+
fetch()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const clearFilters = () => {
|
|
163
|
+
filters.value.forEach(filter => {
|
|
164
|
+
filter.value = null
|
|
165
|
+
})
|
|
166
|
+
fetch()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const editTextButtonMustBeRender = (itemExtension) => {
|
|
170
|
+
return itemExtension === '.json' || itemExtension === '.md' || itemExtension === '.txt'
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
onBeforeMount(() => {
|
|
174
|
+
fetch()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
defineExpose({ fetch })
|
|
178
|
+
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<crud-show :title="title" :open="open" @close="$emit('close')">
|
|
3
|
+
<v-card-text>
|
|
4
|
+
<file-view :file="item" />
|
|
5
|
+
</v-card-text>
|
|
6
|
+
</crud-show>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup>
|
|
10
|
+
import { ref } from 'vue'
|
|
11
|
+
import {CrudShow} from '@testdracul/common-frontend'
|
|
12
|
+
import FileView from "../../../components/FileView/FileView";
|
|
13
|
+
|
|
14
|
+
defineProps({
|
|
15
|
+
open: {type: Boolean, default: true},
|
|
16
|
+
item: {type: Object, required: true}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
defineEmits(['close'])
|
|
20
|
+
|
|
21
|
+
const title = ref('media.file.showing')
|
|
22
|
+
</script>
|
|
23
|
+
|