mirror of
https://github.com/directus/directus.git
synced 2026-01-24 10:57:58 -05:00
Merge pull request #713 from directus/file-replace
add ability to replace file
This commit is contained in:
@@ -88,6 +88,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, watch } from '@vue/composition-api';
|
||||
import uploadFiles from '@/utils/upload-files';
|
||||
import uploadFile from '@/utils/upload-file';
|
||||
import DrawerCollection from '@/views/private/components/drawer-collection';
|
||||
import api from '@/api';
|
||||
import useItem from '@/composables/use-item';
|
||||
@@ -103,6 +104,10 @@ export default defineComponent({
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
fileId: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
fromUrl: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -155,16 +160,29 @@ export default defineComponent({
|
||||
try {
|
||||
numberOfFiles.value = files.length;
|
||||
|
||||
const uploadedFiles = await uploadFiles(Array.from(files), {
|
||||
onProgressChange: (percentage) => {
|
||||
progress.value = Math.round(percentage.reduce((acc, cur) => (acc += cur)) / files.length);
|
||||
done.value = percentage.filter((p) => p === 100).length;
|
||||
},
|
||||
preset: props.preset,
|
||||
});
|
||||
if (props.multiple === true) {
|
||||
const uploadedFiles = await uploadFiles(Array.from(files), {
|
||||
onProgressChange: (percentage) => {
|
||||
progress.value = Math.round(
|
||||
percentage.reduce((acc, cur) => (acc += cur)) / files.length
|
||||
);
|
||||
done.value = percentage.filter((p) => p === 100).length;
|
||||
},
|
||||
preset: props.preset,
|
||||
});
|
||||
|
||||
if (uploadedFiles) {
|
||||
emit('input', props.multiple ? uploadedFiles : uploadedFiles[0]);
|
||||
uploadedFiles && emit('input', uploadedFiles);
|
||||
} else {
|
||||
const uploadedFile = await uploadFile(Array.from(files)[0], {
|
||||
onProgressChange: (percentage) => {
|
||||
progress.value = percentage;
|
||||
done.value = percentage === 100 ? 1 : 0;
|
||||
},
|
||||
fileId: props.fileId,
|
||||
preset: props.preset,
|
||||
});
|
||||
|
||||
uploadedFile && emit('input', uploadedFile);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { APIError } from '@/types';
|
||||
export function useItem(collection: Ref<string>, primaryKey: Ref<string | number | null>) {
|
||||
const { info: collectionInfo, primaryKeyField } = useCollection(collection);
|
||||
|
||||
const item = ref<any>(null);
|
||||
const item = ref<Record<string, any> | null>(null);
|
||||
const error = ref(null);
|
||||
const validationErrors = ref([]);
|
||||
const loading = ref(false);
|
||||
@@ -203,7 +203,7 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
|
||||
if (unarchiveValue === 'false') unarchiveValue = false;
|
||||
|
||||
try {
|
||||
let value: any = item.value[field] === archiveValue ? unarchiveValue : archiveValue;
|
||||
let value: any = item.value && item.value[field] === archiveValue ? unarchiveValue : archiveValue;
|
||||
|
||||
if (value === 'true') value = true;
|
||||
if (value === 'false') value = false;
|
||||
|
||||
@@ -792,6 +792,7 @@
|
||||
"create_folder": "Create Folder",
|
||||
"folder_name": "Folder Name...",
|
||||
"add_file": "Add File",
|
||||
"replace_file": "Replace File",
|
||||
|
||||
"no_results": "No Results",
|
||||
"no_results_copy": "Adjust or clear search filters to see results.",
|
||||
|
||||
@@ -337,6 +337,7 @@ export default defineComponent({
|
||||
const fields = [primaryKeyField.value.field];
|
||||
|
||||
if (imageSource.value) {
|
||||
fields.push(`${imageSource.value}.modified_on`);
|
||||
fields.push(`${imageSource.value}.type`);
|
||||
fields.push(`${imageSource.value}.filename_disk`);
|
||||
fields.push(`${imageSource.value}.storage`);
|
||||
@@ -344,6 +345,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
if (props.collection === 'directus_files' && imageSource.value === '$file') {
|
||||
fields.push('modified_on');
|
||||
fields.push('type');
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ type File = {
|
||||
[key: string]: any;
|
||||
id: string;
|
||||
type: string;
|
||||
modified_on: Date;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
@@ -101,7 +102,7 @@ export default defineComponent({
|
||||
key = 'system-medium-contain';
|
||||
}
|
||||
|
||||
return getRootPath() + `assets/${props.file.id}?key=${key}`;
|
||||
return getRootPath() + `assets/${props.file.id}?key=${key}&modified=${props.file.modified_on}`;
|
||||
});
|
||||
|
||||
const svgSource = computed(() => {
|
||||
@@ -109,7 +110,7 @@ export default defineComponent({
|
||||
if (props.file.type.startsWith('image') === false) return null;
|
||||
if (props.file.type.includes('svg') === false) return null;
|
||||
|
||||
return getRootPath() + `assets/${props.file.id}`;
|
||||
return getRootPath() + `assets/${props.file.id}&modified=${props.file.modified_on}`;
|
||||
});
|
||||
|
||||
const selectionIcon = computed(() => {
|
||||
|
||||
@@ -64,10 +64,17 @@
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>{{ $t('file') }}</dt>
|
||||
<dd>
|
||||
<a :href="`${getRootPath()}assets/${file.id}`" target="_blank">{{ $t('open') }}</a>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>{{ $t('folder') }}</dt>
|
||||
<dd>
|
||||
<button @click="$emit('move-folder')">{{ folder ? folder.name : $t('file_library') }}</button>
|
||||
<router-link :to="folderLink">{{ folder ? folder.name : $t('file_library') }}</router-link>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
@@ -115,6 +122,7 @@ import i18n from '@/lang';
|
||||
import marked from 'marked';
|
||||
import localizedFormat from '@/utils/localized-format';
|
||||
import api from '@/api';
|
||||
import getRootPath from '@/utils/get-root-path';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -140,9 +148,20 @@ export default defineComponent({
|
||||
|
||||
const { creationDate, modificationDate } = useDates();
|
||||
const { userCreated, userModified } = useUser();
|
||||
const { folder } = useFolder();
|
||||
const { folder, folderLink } = useFolder();
|
||||
|
||||
return { readableMimeType, size, creationDate, modificationDate, userCreated, userModified, folder, marked };
|
||||
return {
|
||||
readableMimeType,
|
||||
size,
|
||||
creationDate,
|
||||
modificationDate,
|
||||
userCreated,
|
||||
userModified,
|
||||
folder,
|
||||
marked,
|
||||
folderLink,
|
||||
getRootPath,
|
||||
};
|
||||
|
||||
function useDates() {
|
||||
const creationDate = ref<string | null>(null);
|
||||
@@ -230,16 +249,23 @@ export default defineComponent({
|
||||
|
||||
function useFolder() {
|
||||
type Folder = {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const loading = ref(false);
|
||||
const folder = ref<Folder | null>(null);
|
||||
|
||||
const folderLink = computed(() => {
|
||||
if (folder.value === null) {
|
||||
return `/files`;
|
||||
}
|
||||
return `/files/?folder=${folder.value.id}`;
|
||||
});
|
||||
|
||||
watch(() => props.file, fetchFolder, { immediate: true });
|
||||
|
||||
return { folder };
|
||||
return { folder, folderLink };
|
||||
|
||||
async function fetchFolder() {
|
||||
if (!props.file) return null;
|
||||
|
||||
57
app/src/modules/files/components/replace-file.vue
Normal file
57
app/src/modules/files/components/replace-file.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<v-dialog :active="active" @toggle="$emit('toggle', false)" @esc="$emit('toggle', false)">
|
||||
<v-card v-if="file">
|
||||
<v-card-title>{{ $t('replace_file') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-upload :preset="preset" :file-id="file.id" @input="uploaded" from-url />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-button secondary @click="$emit('toggle', false)">{{ $t('done') }}</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import api from '@/api';
|
||||
import router from '@/router';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
model: {
|
||||
prop: 'active',
|
||||
event: 'toggle',
|
||||
},
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
file: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
preset: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
return { uploaded };
|
||||
function uploaded() {
|
||||
emit('toggle', false);
|
||||
emit('replaced');
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.add-new {
|
||||
--v-button-background-color: var(--primary-25);
|
||||
--v-button-color: var(--primary);
|
||||
--v-button-background-color-hover: var(--primary-50);
|
||||
--v-button-color-hover: var(--primary);
|
||||
}
|
||||
</style>
|
||||
@@ -119,15 +119,13 @@
|
||||
:width="item.width"
|
||||
:height="item.height"
|
||||
:title="item.title"
|
||||
@click="previewActive = true"
|
||||
@click="replaceFileDialogActive = true"
|
||||
/>
|
||||
|
||||
<file-lightbox v-if="item" :id="item.id" v-model="previewActive" />
|
||||
|
||||
<image-editor
|
||||
v-if="item && item.type.startsWith('image')"
|
||||
:id="item.id"
|
||||
@refresh="changeCacheBuster"
|
||||
@refresh="refresh"
|
||||
v-model="editActive"
|
||||
/>
|
||||
|
||||
@@ -156,7 +154,7 @@
|
||||
</v-dialog>
|
||||
|
||||
<template #sidebar>
|
||||
<file-info-sidebar-detail :file="item" @move-folder="moveToDialogActive = true" />
|
||||
<file-info-sidebar-detail :file="item" />
|
||||
<revisions-drawer-detail
|
||||
v-if="isBatch === false && isNew === false"
|
||||
collection="directus_files"
|
||||
@@ -169,6 +167,8 @@
|
||||
:primary-key="primaryKey"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<replace-file v-model="replaceFileDialogActive" @replaced="refresh" :file="item" />
|
||||
</private-view>
|
||||
</template>
|
||||
|
||||
@@ -184,7 +184,6 @@ import SaveOptions from '@/views/private/components/save-options';
|
||||
import FilePreview from '@/views/private/components/file-preview';
|
||||
import ImageEditor from '@/views/private/components/image-editor';
|
||||
import { nanoid } from 'nanoid';
|
||||
import FileLightbox from '@/views/private/components/file-lightbox';
|
||||
import { useFieldsStore } from '@/stores/';
|
||||
import { Field } from '@/types';
|
||||
import FileInfoSidebarDetail from '../components/file-info-sidebar-detail.vue';
|
||||
@@ -194,6 +193,7 @@ import api from '@/api';
|
||||
import getRootPath from '@/utils/get-root-path';
|
||||
import FilesNotFound from './not-found.vue';
|
||||
import useShortcut from '@/composables/use-shortcut';
|
||||
import ReplaceFile from '../components/replace-file.vue';
|
||||
|
||||
type Values = {
|
||||
[field: string]: any;
|
||||
@@ -220,10 +220,10 @@ export default defineComponent({
|
||||
SaveOptions,
|
||||
FilePreview,
|
||||
ImageEditor,
|
||||
FileLightbox,
|
||||
FileInfoSidebarDetail,
|
||||
FolderPicker,
|
||||
FilesNotFound,
|
||||
ReplaceFile,
|
||||
},
|
||||
props: {
|
||||
primaryKey: {
|
||||
@@ -236,6 +236,7 @@ export default defineComponent({
|
||||
const { primaryKey } = toRefs(props);
|
||||
const { breadcrumb } = useBreadcrumb();
|
||||
const fieldsStore = useFieldsStore();
|
||||
const replaceFileDialogActive = ref(false);
|
||||
|
||||
const revisionsDrawerDetail = ref<Vue | null>(null);
|
||||
|
||||
@@ -256,13 +257,15 @@ export default defineComponent({
|
||||
|
||||
const hasEdits = computed<boolean>(() => Object.keys(edits.value).length > 0);
|
||||
const confirmDelete = ref(false);
|
||||
const cacheBuster = ref(nanoid());
|
||||
const editActive = ref(false);
|
||||
const previewActive = ref(false);
|
||||
const fileSrc = computed(() => {
|
||||
return (
|
||||
getRootPath() + `assets/${props.primaryKey}?cache-buster=${cacheBuster.value}&key=system-large-contain`
|
||||
);
|
||||
if (item.value && item.value.modified_on) {
|
||||
return (
|
||||
getRootPath() +
|
||||
`assets/${props.primaryKey}?cache-buster=${item.value.modified_on}&key=system-large-contain`
|
||||
);
|
||||
}
|
||||
return getRootPath() + `assets/${props.primaryKey}?key=system-large-contain`;
|
||||
});
|
||||
|
||||
// These are the fields that will be prevented from showing up in the form because they'll be shown in the sidebar
|
||||
@@ -318,10 +321,7 @@ export default defineComponent({
|
||||
saveAndStay,
|
||||
saveAsCopyAndNavigate,
|
||||
isBatch,
|
||||
changeCacheBuster,
|
||||
cacheBuster,
|
||||
editActive,
|
||||
previewActive,
|
||||
revisionsDrawerDetail,
|
||||
formFields,
|
||||
confirmLeave,
|
||||
@@ -335,12 +335,10 @@ export default defineComponent({
|
||||
fileSrc,
|
||||
form,
|
||||
to,
|
||||
replaceFileDialogActive,
|
||||
refresh,
|
||||
};
|
||||
|
||||
function changeCacheBuster() {
|
||||
cacheBuster.value = nanoid();
|
||||
}
|
||||
|
||||
function useBreadcrumb() {
|
||||
const breadcrumb = computed(() => {
|
||||
if (!item?.value?.folder) {
|
||||
@@ -405,7 +403,7 @@ export default defineComponent({
|
||||
const selectedFolder = ref<number | null>();
|
||||
|
||||
watch(item, () => {
|
||||
selectedFolder.value = item.value.folder;
|
||||
selectedFolder.value = item.value?.folder || null;
|
||||
});
|
||||
|
||||
return { moveToDialogActive, moving, moveToFolder, selectedFolder };
|
||||
|
||||
@@ -133,7 +133,7 @@ export default defineComponent({
|
||||
const values = {
|
||||
...item.value,
|
||||
...edits.value,
|
||||
};
|
||||
} as Record<string, any>;
|
||||
|
||||
return !!values.admin_access;
|
||||
});
|
||||
|
||||
@@ -115,7 +115,7 @@ export default defineComponent({
|
||||
const title = computed(() => {
|
||||
if (loading.value) return i18n.t('loading');
|
||||
if (isNew.value) return i18n.t('creating_webhook');
|
||||
return item.value.name;
|
||||
return item.value?.name;
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -384,7 +384,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
async function refreshCurrentUser() {
|
||||
if (userStore.state.currentUser!.id === item.value.id) {
|
||||
if (userStore.state.currentUser!.id === item.value?.id) {
|
||||
await userStore.hydrate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { usePermissionsStore, useUserStore } from '@/stores';
|
||||
import { Permission } from '@/types';
|
||||
import generateJoi from '@/utils/generate-joi';
|
||||
|
||||
export function isAllowed(collection: string, action: Permission['action'], value: Record<string, any>) {
|
||||
export function isAllowed(collection: string, action: Permission['action'], value: Record<string, any> | null) {
|
||||
const permissionsStore = usePermissionsStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ export default async function uploadFile(
|
||||
onProgressChange?: (percentage: number) => void;
|
||||
notifications?: boolean;
|
||||
preset?: Record<string, any>;
|
||||
fileId?: string;
|
||||
}
|
||||
) {
|
||||
const progressHandler = options?.onProgressChange || (() => undefined);
|
||||
@@ -25,9 +26,17 @@ export default async function uploadFile(
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const response = await api.post(`/files`, formData, {
|
||||
onUploadProgress,
|
||||
});
|
||||
let response = null;
|
||||
|
||||
if (options?.fileId !== undefined) {
|
||||
response = await api.patch(`/files/${options.fileId}`, formData, {
|
||||
onUploadProgress,
|
||||
});
|
||||
} else {
|
||||
response = await api.post(`/files`, formData, {
|
||||
onUploadProgress,
|
||||
});
|
||||
}
|
||||
|
||||
if (options?.notifications) {
|
||||
notify({
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
<template>
|
||||
<div class="file-preview" v-if="type">
|
||||
<div v-if="type === 'image'" class="image" :class="{ svg: isSVG, 'max-size': inModal === false }" @click="$emit('click')">
|
||||
<div
|
||||
v-if="type === 'image'"
|
||||
class="image"
|
||||
:class="{ svg: isSVG, 'max-size': inModal === false }"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<img
|
||||
:src="src"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:style="{
|
||||
'maxWidth': width ? width + 'px' : '100%'
|
||||
maxWidth: width ? width + 'px' : '100%',
|
||||
}"
|
||||
:alt="title"
|
||||
/>
|
||||
<v-icon v-if="inModal === false" name="fullscreen" />
|
||||
<v-icon v-if="inModal === false" name="upload" />
|
||||
</div>
|
||||
|
||||
<video v-else-if="type === 'video'" controls :src="src" />
|
||||
@@ -106,9 +111,9 @@ audio {
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: inherit;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
max-height: inherit;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user