mirror of
https://github.com/directus/directus.git
synced 2026-01-25 10:48:45 -05:00
Allow multiple in v-upload
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
|
||||
<template v-else-if="uploading">
|
||||
<p class="type-label">{{ progress }}%</p>
|
||||
<p class="type-text">{{ $t('upload_file_indeterminate') }}</p>
|
||||
<p class="type-text">{{ multiple && numberOfFiles > 1 ? $t('upload_files_indeterminate', { done: done, total: numberOfFiles }) : $t('upload_file_indeterminate') }}</p>
|
||||
<v-progress-linear :value="progress" rounded />
|
||||
</template>
|
||||
|
||||
@@ -29,12 +29,17 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import uploadFile from '@/utils/upload-file';
|
||||
import uploadFiles from '@/utils/upload-files';
|
||||
|
||||
export default defineComponent({
|
||||
props: {},
|
||||
props: {
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const { uploading, progress, error, upload, onBrowseSelect } = useUpload();
|
||||
const { uploading, progress, error, upload, onBrowseSelect, done, numberOfFiles } = useUpload();
|
||||
const { onDragEnter, onDragLeave, onDrop, dragging } = useDragging();
|
||||
|
||||
return {
|
||||
@@ -46,40 +51,50 @@ export default defineComponent({
|
||||
onDrop,
|
||||
dragging,
|
||||
onBrowseSelect,
|
||||
done,
|
||||
numberOfFiles
|
||||
};
|
||||
|
||||
function useUpload() {
|
||||
const uploading = ref(false);
|
||||
const progress = ref(0);
|
||||
const numberOfFiles = ref(0);
|
||||
const done = ref(0);
|
||||
const error = ref(null);
|
||||
|
||||
return { uploading, progress, error, upload, onBrowseSelect };
|
||||
return { uploading, progress, error, upload, onBrowseSelect, numberOfFiles, done };
|
||||
|
||||
async function upload(file: File) {
|
||||
async function upload(files: FileList) {
|
||||
uploading.value = true;
|
||||
progress.value = 0;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await uploadFile(file, (percentage) => {
|
||||
progress.value = percentage;
|
||||
numberOfFiles.value = files.length;
|
||||
|
||||
const uploadedFiles = await uploadFiles(Array.from(files), (percentage) => {
|
||||
progress.value = Math.round(percentage.reduce((acc, cur) => acc += cur) / files.length);
|
||||
done.value = percentage.filter((p) => p === 100).length;
|
||||
});
|
||||
|
||||
if (response) {
|
||||
emit('upload', response.data.data);
|
||||
if (uploadedFiles) {
|
||||
emit('upload', props.multiple ? uploadedFiles : uploadedFiles[0]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
error.value = err;
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
done.value = 0;
|
||||
numberOfFiles.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function onBrowseSelect(event: InputEvent) {
|
||||
const file = (event.target as HTMLInputElement)?.files?.[0];
|
||||
const files = (event.target as HTMLInputElement)?.files;
|
||||
|
||||
if (file) {
|
||||
upload(file);
|
||||
if (files) {
|
||||
upload(files);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,10 +126,10 @@ export default defineComponent({
|
||||
dragCounter = 0;
|
||||
dragging.value = false;
|
||||
|
||||
const file = event.dataTransfer?.files[0];
|
||||
const files = event.dataTransfer?.files;
|
||||
|
||||
if (file) {
|
||||
upload(file);
|
||||
if (files) {
|
||||
upload(files);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +156,7 @@ export default defineComponent({
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:not(.uploading):hover {
|
||||
color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
@@ -6,16 +6,12 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import ValueNull from '@/views/private/components/value-null';
|
||||
import getRootPath from '@/utils/get-root-path';
|
||||
|
||||
type Image = {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
data: {
|
||||
thumbnails: {
|
||||
key: string;
|
||||
url: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
@@ -33,7 +29,7 @@ export default defineComponent({
|
||||
setup(props) {
|
||||
const src = computed(() => {
|
||||
if (props.value === null) return null;
|
||||
return props.value?.data?.thumbnails?.find((thumb) => thumb.key === 'system-small-crop')?.url || null;
|
||||
return getRootPath() + `assets/${props.value.id}?key=system-small-cover`;
|
||||
});
|
||||
|
||||
return { src };
|
||||
|
||||
@@ -19,5 +19,5 @@ export default defineDisplay(({ i18n }) => ({
|
||||
}
|
||||
},
|
||||
],
|
||||
fields: ['data', 'type', 'title'],
|
||||
fields: ['id', 'type', 'title'],
|
||||
}));
|
||||
|
||||
@@ -211,6 +211,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function onUpload(fileInfo: FileInfo) {
|
||||
console.log(fileInfo);
|
||||
file.value = fileInfo;
|
||||
activeDialog.value = null;
|
||||
emit('input', fileInfo.id);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<v-button
|
||||
icon
|
||||
rounded
|
||||
:href="image.data.full_url"
|
||||
:href="downloadSrc"
|
||||
:download="image.filename_download"
|
||||
v-tooltip="$t('download')"
|
||||
>
|
||||
@@ -57,16 +57,11 @@ import i18n from '@/lang';
|
||||
import FileLightbox from '@/views/private/components/file-lightbox';
|
||||
import ImageEditor from '@/views/private/components/image-editor';
|
||||
import { nanoid } from 'nanoid';
|
||||
import getRootPath from '@/utils/get-root-path';
|
||||
|
||||
type Image = {
|
||||
id: string; // uuid
|
||||
type: string;
|
||||
data: {
|
||||
full_url: string;
|
||||
thumbnails: {
|
||||
key: string;
|
||||
url: string;
|
||||
}[];
|
||||
};
|
||||
filesize: number;
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -77,7 +72,7 @@ export default defineComponent({
|
||||
components: { FileLightbox, ImageEditor },
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
@@ -98,18 +93,21 @@ export default defineComponent({
|
||||
if (!image.value) return null;
|
||||
|
||||
if (image.value.type.includes('svg')) {
|
||||
return image.value.data.full_url;
|
||||
return getRootPath() + `assets/${image.value.id}`;
|
||||
}
|
||||
|
||||
const url = image.value.data.thumbnails.find((thumb) => thumb.key === 'system-large-crop')?.url;
|
||||
|
||||
if (url) {
|
||||
return `${url}&cache-buster=${cacheBuster.value}`;
|
||||
if (image.value.type.includes('image')) {
|
||||
return getRootPath() + `assets/${image.value.id}?key=system-large-cover&cache-buster=${cacheBuster.value}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const downloadSrc = computed(() => {
|
||||
if (!image.value) return null;
|
||||
return getRootPath() + `assets/${image.value.id}`;
|
||||
});
|
||||
|
||||
const meta = computed(() => {
|
||||
if (!image.value) return null;
|
||||
const { filesize, width, height, type } = image.value;
|
||||
@@ -143,6 +141,7 @@ export default defineComponent({
|
||||
changeCacheBuster,
|
||||
setImage,
|
||||
deselect,
|
||||
downloadSrc,
|
||||
};
|
||||
|
||||
async function fetchImage() {
|
||||
@@ -151,7 +150,7 @@ export default defineComponent({
|
||||
try {
|
||||
const response = await api.get(`/files/${props.value}`, {
|
||||
params: {
|
||||
fields: ['id', 'data', 'title', 'width', 'height', 'filesize', 'type', 'filename_download'],
|
||||
fields: ['id', 'title', 'width', 'height', 'filesize', 'type', 'filename_download'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -169,6 +168,7 @@ export default defineComponent({
|
||||
|
||||
function setImage(data: Image) {
|
||||
image.value = data;
|
||||
emit('input', data.id);
|
||||
}
|
||||
|
||||
function deselect() {
|
||||
|
||||
@@ -6,6 +6,7 @@ export default defineInterface(({ i18n }) => ({
|
||||
name: i18n.t('image'),
|
||||
icon: 'insert_photo',
|
||||
component: InterfaceImage,
|
||||
types: ['string'],
|
||||
types: ['uuid'],
|
||||
relationship: 'm2o',
|
||||
options: [],
|
||||
}));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('add_new_file') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-upload @upload="onUpload" />
|
||||
<v-upload multiple @upload="close" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-button secondary @click="close">{{ $t('done') }}</v-button>
|
||||
@@ -24,15 +24,11 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
return { onUpload, close };
|
||||
return { close };
|
||||
|
||||
function close() {
|
||||
router.push('/files');
|
||||
}
|
||||
|
||||
function onUpload() {
|
||||
emit('upload');
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-button
|
||||
v-if="collection.system === null && collection.collection.startsWith('directus_') === false"
|
||||
v-if="collection.meta === null && collection.collection.startsWith('directus_') === false"
|
||||
x-small
|
||||
outlined
|
||||
class="manage"
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<div>
|
||||
<h2 class="type-title">{{ $t('display_setup_title') }}</h2>
|
||||
|
||||
<v-fancy-select class="select" :items="selectItems" v-model="fieldData.system.display" />
|
||||
<v-fancy-select class="select" :items="selectItems" v-model="fieldData.meta.display" />
|
||||
|
||||
<v-notice class="not-found" type="danger" v-if="fieldData.system.display && !selectedDisplay">
|
||||
{{ $t('display_not_found', { display: fieldData.system.display }) }}
|
||||
<v-notice class="not-found" type="danger" v-if="fieldData.meta.display && !selectedDisplay">
|
||||
{{ $t('display_not_found', { display: fieldData.meta.display }) }}
|
||||
<div class="spacer" />
|
||||
<button @click="fieldData.system.display = null">{{ $t('reset_display') }}</button>
|
||||
<button @click="fieldData.meta.display = null">{{ $t('reset_display') }}</button>
|
||||
</v-notice>
|
||||
|
||||
<template v-if="fieldData.system.display && !selectedDisplay">
|
||||
<template v-if="fieldData.meta.display && !selectedDisplay">
|
||||
<v-form
|
||||
v-if="
|
||||
selectedDisplay.options &&
|
||||
@@ -19,7 +19,7 @@
|
||||
"
|
||||
:fields="selectedDisplay.options"
|
||||
primary-key="+"
|
||||
v-model="fieldData.system.options"
|
||||
v-model="fieldData.meta.options"
|
||||
/>
|
||||
|
||||
<v-notice v-else>
|
||||
@@ -71,7 +71,7 @@ export default defineComponent({
|
||||
);
|
||||
|
||||
const selectedDisplay = computed(() => {
|
||||
return displays.find((display) => display.id === state.fieldData.system.display);
|
||||
return displays.find((display) => display.id === state.fieldData.meta.display);
|
||||
});
|
||||
|
||||
return { fieldData: state.fieldData, selectItems, selectedDisplay };
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<div>
|
||||
<h2 class="type-title">{{ $t('interface_setup_title') }}</h2>
|
||||
|
||||
<v-fancy-select class="select" :items="selectItems" v-model="fieldData.system.interface" />
|
||||
<v-fancy-select class="select" :items="selectItems" v-model="fieldData.meta.interface" />
|
||||
|
||||
<v-notice class="not-found" type="danger" v-if="fieldData.system.interface && !selectedInterface">
|
||||
{{ $t('interface_not_found', { interface: fieldData.system.interface }) }}
|
||||
<v-notice class="not-found" type="danger" v-if="fieldData.meta.interface && !selectedInterface">
|
||||
{{ $t('interface_not_found', { interface: fieldData.meta.interface }) }}
|
||||
<div class="spacer" />
|
||||
<button @click="fieldData.system.interface = null">{{ $t('reset_interface') }}</button>
|
||||
<button @click="fieldData.meta.interface = null">{{ $t('reset_interface') }}</button>
|
||||
</v-notice>
|
||||
|
||||
<template v-if="fieldData.system.interface && selectedInterface">
|
||||
<template v-if="fieldData.meta.interface && selectedInterface">
|
||||
<v-form
|
||||
v-if="
|
||||
selectedInterface.options &&
|
||||
@@ -19,7 +19,7 @@
|
||||
"
|
||||
:fields="selectedInterface.options"
|
||||
primary-key="+"
|
||||
v-model="fieldData.system.options"
|
||||
v-model="fieldData.meta.options"
|
||||
/>
|
||||
|
||||
<v-notice v-else>
|
||||
@@ -71,7 +71,7 @@ export default defineComponent({
|
||||
);
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
return interfaces.find((inter) => inter.id === state.fieldData.system.interface);
|
||||
return interfaces.find((inter) => inter.id === state.fieldData.meta.interface);
|
||||
});
|
||||
|
||||
return { fieldData: state.fieldData, selectItems, selectedInterface };
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<div class="field">
|
||||
<div class="label type-label">{{ $t('type') }}</div>
|
||||
<v-input v-if="!fieldData.database" :value="$t('alias')" disabled />
|
||||
<v-input v-if="!fieldData.schema" :value="$t('alias')" disabled />
|
||||
<v-select
|
||||
v-else
|
||||
:disabled="typeDisabled || isExisting"
|
||||
@@ -23,32 +23,32 @@
|
||||
|
||||
<div class="field full">
|
||||
<div class="label type-label">{{ $t('note') }}</div>
|
||||
<v-input v-model="fieldData.system.comment" :placeholder="$t('add_note')" />
|
||||
<v-input v-model="fieldData.meta.comment" :placeholder="$t('add_note')" />
|
||||
</div>
|
||||
|
||||
<!-- @todo base default value field type on selected type -->
|
||||
<div class="field" v-if="fieldData.database">
|
||||
<div class="field" v-if="fieldData.schema">
|
||||
<div class="label type-label">{{ $t('default_value') }}</div>
|
||||
<v-input
|
||||
class="monospace"
|
||||
v-model="fieldData.database.default_value"
|
||||
v-model="fieldData.schema.default_value"
|
||||
:placeholder="$t('add_a_default_value')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="fieldData.database">
|
||||
<div class="field" v-if="fieldData.schema">
|
||||
<div class="label type-label">{{ $t('length') }}</div>
|
||||
<v-input
|
||||
type="number"
|
||||
:placeholder="fieldData.type !== 'string' ? $t('not_available_for_type') : '255'"
|
||||
:disabled="isExisting || fieldData.type !== 'string'"
|
||||
v-model="fieldData.database.max_length"
|
||||
v-model="fieldData.schema.max_length"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="fieldData.database">
|
||||
<div class="field" v-if="fieldData.schema">
|
||||
<div class="label type-label">{{ $t('allow_null') }}</div>
|
||||
<v-checkbox v-model="fieldData.database.is_nullable" :label="$t('allow_null_label')" block />
|
||||
<v-checkbox v-model="fieldData.schema.is_nullable" :label="$t('allow_null_label')" block />
|
||||
</div>
|
||||
|
||||
<!--
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
<div class="field">
|
||||
<div class="label type-label">{{ $t('unique') }}</div>
|
||||
<v-input v-model="fieldData.database.unique" />
|
||||
<v-input v-model="fieldData.schema.unique" />
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,17 +116,17 @@ export default defineComponent({
|
||||
|
||||
function setType(value: typeof types[number]) {
|
||||
if (value === 'uuid') {
|
||||
state.fieldData.system.special = 'uuid';
|
||||
state.fieldData.meta.special = 'uuid';
|
||||
} else {
|
||||
state.fieldData.system.special = null;
|
||||
state.fieldData.meta.special = null;
|
||||
}
|
||||
|
||||
// We'll reset the interface/display as they most likely won't work for the newly selected
|
||||
// type
|
||||
state.fieldData.system.interface = null;
|
||||
state.fieldData.system.options = null;
|
||||
state.fieldData.system.display = null;
|
||||
state.fieldData.system.display_options = null;
|
||||
state.fieldData.meta.interface = null;
|
||||
state.fieldData.meta.options = null;
|
||||
state.fieldData.meta.display = null;
|
||||
state.fieldData.meta.display_options = null;
|
||||
state.fieldData.type = value;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,12 +25,12 @@ function initLocalStore(
|
||||
fieldData: {
|
||||
field: '',
|
||||
type: '',
|
||||
database: {
|
||||
schema: {
|
||||
default_value: undefined,
|
||||
max_length: undefined,
|
||||
is_nullable: true,
|
||||
},
|
||||
system: {
|
||||
meta: {
|
||||
hidden: false,
|
||||
interface: undefined,
|
||||
options: undefined,
|
||||
@@ -52,8 +52,8 @@ function initLocalStore(
|
||||
|
||||
state.fieldData.field = existingField.field;
|
||||
state.fieldData.type = existingField.type;
|
||||
state.fieldData.database = existingField.schema;
|
||||
state.fieldData.system = existingField.meta;
|
||||
state.fieldData.schema = existingField.schema;
|
||||
state.fieldData.meta = existingField.meta;
|
||||
|
||||
state.relations = relationsStore.getRelationsForField(collection, field);
|
||||
}
|
||||
@@ -122,11 +122,11 @@ function initLocalStore(
|
||||
}
|
||||
|
||||
if (type === 'o2m') {
|
||||
delete state.fieldData.database;
|
||||
delete state.fieldData.schema;
|
||||
delete state.fieldData.type;
|
||||
|
||||
if (!isExisting) {
|
||||
state.fieldData.system.special = 'o2m';
|
||||
state.fieldData.meta.special = 'o2m';
|
||||
|
||||
state.relations = [
|
||||
{
|
||||
@@ -159,11 +159,11 @@ function initLocalStore(
|
||||
}
|
||||
|
||||
if (type === 'm2m' || type === 'files') {
|
||||
delete state.fieldData.database;
|
||||
delete state.fieldData.schema;
|
||||
delete state.fieldData.type;
|
||||
|
||||
if (!isExisting) {
|
||||
state.fieldData.system.special = 'm2m';
|
||||
state.fieldData.meta.special = 'm2m';
|
||||
|
||||
state.relations = [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import VueRouter, { NavigationGuard, RouteConfig, Route } from 'vue-router';
|
||||
import LoginRoute from '@/routes/login';
|
||||
import LogoutRoute from '@/routes/logout';
|
||||
import InstallRoute from '@/routes/install';
|
||||
import ResetPasswordRoute from '@/routes/reset-password';
|
||||
import { refresh } from '@/auth';
|
||||
import { hydrate } from '@/hydrate';
|
||||
@@ -17,17 +16,6 @@ export const defaultRoutes: RouteConfig[] = [
|
||||
path: '/',
|
||||
redirect: '/login',
|
||||
},
|
||||
{
|
||||
name: 'install',
|
||||
path: '/install',
|
||||
component: InstallRoute,
|
||||
/**
|
||||
* @todo redirect to /login if project is already installed
|
||||
*/
|
||||
meta: {
|
||||
public: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'login',
|
||||
path: '/login',
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import InstallRoute from './install.vue';
|
||||
|
||||
export { InstallRoute };
|
||||
export default InstallRoute;
|
||||
@@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="type-title pane-title">{{ $t('database_connection') }}</div>
|
||||
<div class="pane-content">
|
||||
<div class="pane-form">
|
||||
<div class="field">
|
||||
<div class="type-label label">{{ $t('host') }}</div>
|
||||
<v-input v-model="_value.db_host" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label label">{{ $t('port') }}</div>
|
||||
<v-input type="number" v-model="_value.db_port" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label label">{{ $t('db_user') }}</div>
|
||||
<v-input v-model="_value.db_user" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label label">{{ $t('db_password') }}</div>
|
||||
<v-input type="password" v-model="_value.db_password" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label label">{{ $t('db_name') }}</div>
|
||||
<v-input v-model="_value.db_name" class="db" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label label">{{ $t('db_type') }}</div>
|
||||
<v-input value="MySQL" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pane-buttons">
|
||||
<v-button secondary @click="$emit('prev')">{{ $t('back') }}</v-button>
|
||||
<v-button :disabled="nextEnabled === false" @click="$emit('next')">
|
||||
{{ $t('create_project') }}
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
|
||||
type DatabaseInfo = {
|
||||
db_host: string | null;
|
||||
db_name: string | null;
|
||||
db_port: number | null;
|
||||
db_user: string | null;
|
||||
db_password: string | null;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Object as PropType<DatabaseInfo>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const _value = computed<DatabaseInfo>({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(newValue) {
|
||||
emit('input', newValue);
|
||||
},
|
||||
});
|
||||
|
||||
const nextEnabled = computed<boolean>(() => {
|
||||
const requiredKeys: (keyof DatabaseInfo)[] = ['db_host', 'db_name', 'db_port', 'db_user', 'db_password'];
|
||||
|
||||
return !!requiredKeys.every(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
(key) => _value.value[key] && _value.value[key] !== ''
|
||||
);
|
||||
});
|
||||
|
||||
return { _value, nextEnabled };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-input.db {
|
||||
--v-input-font-family: var(--family-monospace);
|
||||
}
|
||||
</style>
|
||||
@@ -1,105 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="type-title pane-title">
|
||||
<template v-if="loading">{{ $t('creating_project') }}</template>
|
||||
<template v-else-if="error">{{ $t('creating_project_failed') }}</template>
|
||||
<template v-else>{{ $t('creating_project_success') }}</template>
|
||||
</div>
|
||||
<div class="pane-content">
|
||||
<v-progress-linear v-if="loading" indeterminate />
|
||||
|
||||
<template v-else>
|
||||
<v-notice type="danger" v-if="error">
|
||||
{{ errorFormatted }}
|
||||
</v-notice>
|
||||
|
||||
<template v-else>{{ $t('creating_project_success_copy') }}</template>
|
||||
|
||||
<template v-if="first">
|
||||
<v-notice type="warning">
|
||||
{{ $t('creating_project_success_super_admin_password') }}
|
||||
</v-notice>
|
||||
|
||||
<v-input readonly :value="_token" />
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="pane-buttons" v-if="!loading">
|
||||
<v-button v-if="error" secondary @click="$emit('prev')">{{ $t('back') }}</v-button>
|
||||
<v-button v-else @click="$emit('next')">{{ $t('sign_in') }}</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from '@vue/composition-api';
|
||||
import api, { RequestError } from '@/api';
|
||||
import { translateAPIError } from '@/lang';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
type ProjectInfo = {
|
||||
db_host: string | null;
|
||||
db_name: string | null;
|
||||
db_port: number | null;
|
||||
db_user: string | null;
|
||||
db_password: string | null;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
first: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
database: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const loading = ref(true);
|
||||
const error = ref<RequestError | null>(null);
|
||||
|
||||
const _token = computed(() => {
|
||||
return props.token || nanoid();
|
||||
});
|
||||
|
||||
const errorFormatted = computed(() => {
|
||||
return error.value && translateAPIError(error.value);
|
||||
});
|
||||
|
||||
createProject();
|
||||
|
||||
return { loading, error, errorFormatted, _token };
|
||||
|
||||
async function createProject() {
|
||||
try {
|
||||
await api.post('/server/projects', {
|
||||
...props.database,
|
||||
...props.project,
|
||||
super_admin_token: _token.value,
|
||||
});
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-notice {
|
||||
margin: 24px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,91 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="type-title pane-title">{{ $t('project_info') }}</div>
|
||||
<div class="pane-content">
|
||||
<div class="pane-form">
|
||||
<div class="field">
|
||||
<div class="type-label label">{{ $t('project_name') }}</div>
|
||||
<v-input v-model="_value.project_name" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label label">{{ $t('project_key') }}</div>
|
||||
<v-input slug v-model="_value.project" class="key" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label label">{{ $t('admin_email') }}</div>
|
||||
<v-input type="email" v-model="_value.user_email" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label label">{{ $t('admin_password') }}</div>
|
||||
<v-input type="password" v-model="_value.user_password" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pane-buttons">
|
||||
<v-button secondary @click="$emit('prev')">{{ $t('back') }}</v-button>
|
||||
<v-button :disabled="nextEnabled === false" @click="$emit('next')">
|
||||
{{ $t('next') }}
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed, watch } from '@vue/composition-api';
|
||||
import slugify from '@sindresorhus/slugify';
|
||||
|
||||
type ProjectInfo = {
|
||||
project_name: string | null;
|
||||
project: string | null;
|
||||
user_email: string | null;
|
||||
user_password: string | null;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Object as PropType<ProjectInfo>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const _value = computed<ProjectInfo>({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(newValue) {
|
||||
emit('input', newValue);
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => _value.value.project_name,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
_value.value = {
|
||||
..._value.value,
|
||||
project: slugify(newValue),
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const nextEnabled = computed<boolean>(() => {
|
||||
const requiredKeys: (keyof ProjectInfo)[] = ['project_name', 'project', 'user_email', 'user_password'];
|
||||
|
||||
return !!requiredKeys.every(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
(key) => _value.value[key] && _value.value[key]!.length > 0
|
||||
);
|
||||
});
|
||||
|
||||
return { _value, nextEnabled };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-input.key {
|
||||
--v-input-font-family: var(--family-monospace);
|
||||
}
|
||||
</style>
|
||||
@@ -1,147 +0,0 @@
|
||||
<template>
|
||||
<div class="requirements">
|
||||
<div class="pane-title type-title">{{ $t('requirements') }}</div>
|
||||
<div class="pane-content">
|
||||
<div class="loader" v-if="loading">
|
||||
<v-skeleton-loader v-for="n in 5" :key="n" />
|
||||
</div>
|
||||
<v-notice
|
||||
v-else
|
||||
v-for="requirement in requirements"
|
||||
:key="requirement.key"
|
||||
:type="requirement.success ? 'success' : 'warning'"
|
||||
>
|
||||
{{ requirement.value }}
|
||||
</v-notice>
|
||||
</div>
|
||||
<div class="pane-buttons">
|
||||
<v-button secondary @click="$emit('prev')">{{ $t('back') }}</v-button>
|
||||
<v-button @click="$emit('next')">{{ $t('next') }}</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed } from '@vue/composition-api';
|
||||
import api from '@/api';
|
||||
import { satisfies } from 'semver';
|
||||
import i18n from '@/lang';
|
||||
|
||||
type ServerInfo = {
|
||||
server: {
|
||||
type: string;
|
||||
};
|
||||
php: {
|
||||
version: string;
|
||||
extensions: { [extension: string]: boolean };
|
||||
};
|
||||
permissions: { [folder: string]: string };
|
||||
directus: string;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
token: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const serverInfo = ref<ServerInfo | null>(null);
|
||||
const lastTag = ref<string | null>(null);
|
||||
|
||||
const requirements = computed(() => {
|
||||
if (serverInfo.value === null) return null;
|
||||
|
||||
const phpVersion = serverInfo.value.php.version.split('-')[0];
|
||||
|
||||
const extensions = Object.keys(serverInfo.value.php.extensions).map((key) => ({
|
||||
key,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
enabled: serverInfo.value!.php.extensions[key],
|
||||
}));
|
||||
|
||||
const permissions = Object.keys(serverInfo.value?.permissions).map((key) => ({
|
||||
key,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
permission: serverInfo.value!.permissions[key],
|
||||
}));
|
||||
|
||||
const failedPermissions = permissions.filter((p) => +p.permission[1] !== 7);
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'server',
|
||||
success: serverInfo.value?.server.type.toLowerCase().includes('apache'),
|
||||
value: serverInfo.value?.server.type,
|
||||
},
|
||||
{
|
||||
key: 'php',
|
||||
success: satisfies(phpVersion, '>=7.2.0'),
|
||||
value: `PHP ${phpVersion}`,
|
||||
},
|
||||
{
|
||||
key: 'extensions',
|
||||
success: extensions.every((e) => e.enabled),
|
||||
value: extensions.every((e) => e.enabled)
|
||||
? i18n.t('php_extensions')
|
||||
: i18n.t('missing_value', {
|
||||
value: extensions.filter((e) => e.enabled === false).map((e) => e.key),
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'permissions',
|
||||
success: failedPermissions.length === 0,
|
||||
value:
|
||||
failedPermissions.length === 0
|
||||
? i18n.t('write_access')
|
||||
: i18n.t('value_not_writeable', {
|
||||
value: failedPermissions.map((f) => `/${f.key}`).join(', '),
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'version',
|
||||
success: 'v' + serverInfo.value.directus === lastTag.value,
|
||||
value: i18n.t('directus_version') + ': v' + serverInfo.value.directus,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
getServerInfo();
|
||||
|
||||
return { loading, error, requirements };
|
||||
|
||||
async function getServerInfo() {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const infoResponse = await api.get('/server/info', {
|
||||
params: props.token
|
||||
? {
|
||||
super_admin_token: props.token,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
|
||||
const ghResponse = await api.get('https://api.github.com/repos/directus/directus/tags');
|
||||
|
||||
serverInfo.value = infoResponse.data.data;
|
||||
lastTag.value = ghResponse.data[0].name;
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-notice,
|
||||
.v-skeleton-loader {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,95 +0,0 @@
|
||||
<template>
|
||||
<div class="install-welcome" v-if="first">
|
||||
<h1 class="pane-title type-title">{{ $t('welcome_to_directus') }}</h1>
|
||||
<div class="pane-content">{{ $t('welcome_to_directus_copy') }}</div>
|
||||
<div class="pane-buttons">
|
||||
<v-button @click="$emit('next')">{{ $t('next') }}</v-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="install-welcome" v-else>
|
||||
<h1 class="pane-title type-title">{{ $t('create_new_project') }}</h1>
|
||||
<div class="pane-content">
|
||||
{{ $t('create_new_project_copy') }}
|
||||
<v-input @input="setToken" :value="token" :placeholder="$t('super_admin_token')" class="token">
|
||||
<template #append>
|
||||
<v-progress-circular indeterminate v-if="verifying" />
|
||||
<v-icon
|
||||
v-else-if="tokenCorrect !== null"
|
||||
:name="tokenCorrect ? 'check' : 'error'"
|
||||
:class="{ correct: tokenCorrect }"
|
||||
/>
|
||||
</template>
|
||||
</v-input>
|
||||
</div>
|
||||
<div class="pane-buttons">
|
||||
<v-button :disabled="nextDisabled" @click="$emit('next')">{{ $t('next') }}</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from '@vue/composition-api';
|
||||
import { debounce } from 'lodash';
|
||||
import api from '@/api';
|
||||
|
||||
export default defineComponent({
|
||||
model: {
|
||||
prop: 'token',
|
||||
},
|
||||
props: {
|
||||
first: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const verifying = ref(false);
|
||||
const tokenCorrect = ref<boolean | null>(null);
|
||||
|
||||
const nextDisabled = computed(() => {
|
||||
return (
|
||||
props.token === null ||
|
||||
props.token.length === 0 ||
|
||||
tokenCorrect.value === false ||
|
||||
tokenCorrect.value === null
|
||||
);
|
||||
});
|
||||
|
||||
const verifyToken = debounce(async (token: string) => {
|
||||
verifying.value = true;
|
||||
|
||||
try {
|
||||
await api.get(`/server/info?super_admin_token=${token}`);
|
||||
|
||||
tokenCorrect.value = true;
|
||||
} catch {
|
||||
tokenCorrect.value = false;
|
||||
} finally {
|
||||
verifying.value = false;
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return { nextDisabled, verifyToken, tokenCorrect, verifying, setToken };
|
||||
|
||||
function setToken(token: string) {
|
||||
emit('input', token);
|
||||
|
||||
verifyToken(token);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-input {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.v-input.token {
|
||||
--v-input-font-family: var(--family-monospace);
|
||||
}
|
||||
</style>
|
||||
@@ -1,131 +0,0 @@
|
||||
<template>
|
||||
<public-view class="install" :wide="['project', 'database'].includes(currentPane[0])">
|
||||
<v-tabs-items v-model="currentPane">
|
||||
<v-tab-item value="welcome">
|
||||
<install-welcome :first="first" @next="nextPane" v-model="token" />
|
||||
</v-tab-item>
|
||||
<v-tab-item value="requirements">
|
||||
<install-requirements :token="token" :first="first" @prev="prevPane" @next="nextPane" />
|
||||
</v-tab-item>
|
||||
<v-tab-item value="project">
|
||||
<install-project :first="first" v-model="projectInfo" @prev="prevPane" @next="nextPane" />
|
||||
</v-tab-item>
|
||||
<v-tab-item value="database">
|
||||
<install-database v-model="databaseInfo" :first="first" @prev="prevPane" @next="nextPane" />
|
||||
</v-tab-item>
|
||||
<v-tab-item value="final">
|
||||
<install-final
|
||||
:project="projectInfo"
|
||||
:database="databaseInfo"
|
||||
:first="first"
|
||||
:token="token"
|
||||
@prev="prevPane"
|
||||
@next="finish"
|
||||
/>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</public-view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref, reactive } from '@vue/composition-api';
|
||||
|
||||
import InstallWelcome from './install-welcome.vue';
|
||||
import InstallRequirements from './install-requirements.vue';
|
||||
import InstallProject from './install-project.vue';
|
||||
import InstallDatabase from './install-database.vue';
|
||||
import InstallFinal from './install-final.vue';
|
||||
import router from '@/router';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
InstallWelcome,
|
||||
InstallRequirements,
|
||||
InstallProject,
|
||||
InstallDatabase,
|
||||
InstallFinal,
|
||||
},
|
||||
setup() {
|
||||
const first = computed(() => {
|
||||
/**
|
||||
* @todo remove difference between first or not (it's always first now)
|
||||
*/
|
||||
return true;
|
||||
});
|
||||
|
||||
const panes = ['welcome', 'requirements', 'project', 'database', 'final'];
|
||||
|
||||
const currentPane = ref(['welcome']);
|
||||
|
||||
const token = ref(null);
|
||||
|
||||
const projectInfo = reactive({
|
||||
project_name: null,
|
||||
project: null,
|
||||
user_email: null,
|
||||
user_password: null,
|
||||
});
|
||||
|
||||
const databaseInfo = reactive({
|
||||
db_host: 'localhost',
|
||||
db_name: null,
|
||||
db_password: null,
|
||||
db_port: 3306,
|
||||
db_user: null,
|
||||
});
|
||||
|
||||
return {
|
||||
currentPane,
|
||||
first,
|
||||
prevPane,
|
||||
nextPane,
|
||||
projectInfo,
|
||||
databaseInfo,
|
||||
token,
|
||||
finish,
|
||||
};
|
||||
|
||||
function prevPane() {
|
||||
const currentIndex = panes.findIndex((pane) => currentPane.value[0] === pane);
|
||||
currentPane.value = [panes[currentIndex - 1]];
|
||||
}
|
||||
|
||||
function nextPane() {
|
||||
const currentIndex = panes.findIndex((pane) => currentPane.value[0] === pane);
|
||||
currentPane.value = [panes[currentIndex + 1]];
|
||||
}
|
||||
|
||||
async function finish() {
|
||||
router.push('/');
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep {
|
||||
.pane-title,
|
||||
.pane-content {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.pane-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.v-button {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.pane-form {
|
||||
display: grid;
|
||||
grid-gap: 32px 48px;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
.label {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,8 @@ import api from '@/api';
|
||||
import notify from '@/utils/notify';
|
||||
import i18n from '@/lang';
|
||||
|
||||
import emitter, { Events } from '@/events';
|
||||
|
||||
export default async function uploadFile(
|
||||
file: File,
|
||||
onProgressChange?: (percentage: number) => void,
|
||||
@@ -24,7 +26,9 @@ export default async function uploadFile(
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
emitter.emit(Events.upload);
|
||||
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
if (showNotifications) {
|
||||
notify({
|
||||
|
||||
@@ -8,7 +8,7 @@ export default async function uploadFiles(files: File[], onProgressChange?: (per
|
||||
const progressForFiles = files.map(() => 0);
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
const uploadedFiles = await Promise.all(
|
||||
files.map((file, index) =>
|
||||
uploadFile(
|
||||
file,
|
||||
@@ -20,10 +20,13 @@ export default async function uploadFiles(files: File[], onProgressChange?: (per
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
notify({
|
||||
title: i18n.t('upload_files_success', { count: files.length }),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
return uploadedFiles;
|
||||
} catch (error) {
|
||||
notify({
|
||||
title: i18n.t('upload_files_failed', { count: files.length }),
|
||||
|
||||
Reference in New Issue
Block a user