Allow multiple in v-upload

This commit is contained in:
rijkvanzanten
2020-08-07 15:47:35 -04:00
parent 9fb4d0e8eb
commit 6ea10375e9
22 changed files with 103 additions and 759 deletions

View File

@@ -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);
}

View File

@@ -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 };

View File

@@ -19,5 +19,5 @@ export default defineDisplay(({ i18n }) => ({
}
},
],
fields: ['data', 'type', 'title'],
fields: ['id', 'type', 'title'],
}));

View File

@@ -211,6 +211,7 @@ export default defineComponent({
}
function onUpload(fileInfo: FileInfo) {
console.log(fileInfo);
file.value = fileInfo;
activeDialog.value = null;
emit('input', fileInfo.id);

View File

@@ -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() {

View File

@@ -6,6 +6,7 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('image'),
icon: 'insert_photo',
component: InterfaceImage,
types: ['string'],
types: ['uuid'],
relationship: 'm2o',
options: [],
}));

View File

@@ -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>

View File

@@ -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"

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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;
}
},

View File

@@ -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 = [
{

View File

@@ -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',

View File

@@ -1,4 +0,0 @@
import InstallRoute from './install.vue';
export { InstallRoute };
export default InstallRoute;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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({

View File

@@ -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 }),