diff --git a/src/components/v-list/v-list-item.vue b/src/components/v-list/v-list-item.vue
index 6c6801ffb2..43ec1b39f4 100644
--- a/src/components/v-list/v-list-item.vue
+++ b/src/components/v-list/v-list-item.vue
@@ -15,6 +15,8 @@
disabled,
dashed,
}"
+ :href="href"
+ :download="download"
v-on="disabled === false && $listeners"
>
@@ -39,6 +41,10 @@ export default defineComponent({
type: [String, Object] as PropType,
default: null,
},
+ href: {
+ type: String,
+ default: null,
+ },
disabled: {
type: Boolean,
default: false,
@@ -55,10 +61,22 @@ export default defineComponent({
type: Boolean,
default: false,
},
+ download: {
+ type: String,
+ default: null,
+ },
},
setup(props, { listeners }) {
- const component = computed(() => (props.to ? 'router-link' : 'li'));
- const isClickable = computed(() => Boolean(props.to || listeners.click !== undefined));
+ const component = computed(() => {
+ if (props.to) return 'router-link';
+ if (props.href) return 'a';
+ return 'li';
+ });
+
+ const isClickable = computed(() =>
+ Boolean(props.to || props.href || listeners.click !== undefined)
+ );
+
return { component, isClickable };
},
});
diff --git a/src/interfaces/file/file.vue b/src/interfaces/file/file.vue
new file mode 100644
index 0000000000..4713f81f57
--- /dev/null
+++ b/src/interfaces/file/file.vue
@@ -0,0 +1,310 @@
+
+
+
+
+
+
+
+
+
+
![]()
+
+ {{ fileExtension }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('download_file') }}
+
+
+
+
+
+
+
+ {{ $t(file ? 'replace_from_device' : 'upload_from_device') }}
+
+
+
+
+
+
+ {{ $t(file ? 'replace_from_library' : 'choose_from_library') }}
+
+
+
+
+
+
+ {{ $t(file ? 'replace_from_url' : 'import_from_url') }}
+
+
+
+
+
+
+
+ {{ $t('upload_from_device') }}
+
+
+
+
+ {{ $t('cancel') }}
+
+
+
+
+
+
+
+
+ {{ $t('import_from_url') }}
+
+
+
+
+
+ {{ $t('cancel') }}
+
+
+ {{ $t('import') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/interfaces/file/index.ts b/src/interfaces/file/index.ts
new file mode 100644
index 0000000000..5955957775
--- /dev/null
+++ b/src/interfaces/file/index.ts
@@ -0,0 +1,10 @@
+import { defineInterface } from '../define';
+import InterfaceFile from './file.vue';
+
+export default defineInterface(({ i18n }) => ({
+ id: 'file',
+ name: i18n.t('file'),
+ icon: 'note_add',
+ component: InterfaceFile,
+ options: [],
+}));
diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts
index f8eb56fcca..a9ee43c69c 100644
--- a/src/interfaces/index.ts
+++ b/src/interfaces/index.ts
@@ -22,6 +22,7 @@ import InterfaceSlug from './slug';
import InterfaceUser from './user';
import InterfaceTags from './tags';
import InterfaceRepeater from './repeater';
+import InterfaceFile from './file';
export const interfaces = [
InterfaceTextInput,
@@ -48,6 +49,7 @@ export const interfaces = [
InterfaceUser,
InterfaceTags,
InterfaceRepeater,
+ InterfaceFile,
];
export default interfaces;
diff --git a/src/lang/en-US/index.json b/src/lang/en-US/index.json
index a68455bcbb..7bcc1f6735 100644
--- a/src/lang/en-US/index.json
+++ b/src/lang/en-US/index.json
@@ -139,6 +139,8 @@
"one_to_many": "One to Many (O2M)",
"many_to_one": "Many to One (M2O)",
"original": "Original",
+ "url": "URL",
+ "import": "Import",
"file_details": "File Details",
"dimensions": "Dimensions",
@@ -155,6 +157,15 @@
"download": "Download",
"open": "Open",
+ "upload_from_device": "Upload File from Device",
+ "choose_from_library": "Choose File from Library",
+ "import_from_url": "Import File from URL",
+ "no_file_selected": "No File Selected",
+ "download_file": "Download File",
+ "replace_from_device": "Replace File from Device",
+ "replace_from_library": "Replace File from Library",
+ "replace_from_url": "Replace File from URL",
+
"name": "Name",
"primary_key_field": "Primary Key Field",
"type": "Type",
diff --git a/src/stores/projects/types.ts b/src/stores/projects/types.ts
index f061901b43..13ff6c56d2 100644
--- a/src/stores/projects/types.ts
+++ b/src/stores/projects/types.ts
@@ -13,14 +13,17 @@ export interface Project {
project_color: string;
project_logo: {
full_url: string;
+ asset_url: string;
url: string;
} | null;
project_foreground: {
full_url: string;
+ asset_url: string;
url: string;
} | null;
project_background: {
full_url: string;
+ asset_url: string;
url: string;
} | null;
project_public_note: string | null;
diff --git a/src/utils/readable-mime-type/extensions.json b/src/utils/readable-mime-type/extensions.json
new file mode 100644
index 0000000000..65bda82f3d
--- /dev/null
+++ b/src/utils/readable-mime-type/extensions.json
@@ -0,0 +1,73 @@
+{
+ "audio/aac": "aac",
+ "application/x-abiword": "abw",
+ "application/x-freearc": "arc",
+ "video/x-msvideo": "avi",
+ "application/vnd.amazon.ebook": "azw",
+ "application/octet-stream": "bin",
+ "image/bmp": "bmp",
+ "application/x-bzip": "bz",
+ "application/x-bzip2": "bz2",
+ "application/x-csh": "csh",
+ "text/css": "css",
+ "text/csv": "csv",
+ "application/msword": "doc",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
+ "application/vnd.ms-fontobject": "eot",
+ "application/epub+zip": "epub",
+ "application/gzip": "gz",
+ "image/gif": "gif",
+ "text/html": "html",
+ "image/vnd.microsoft.icon": "ico",
+ "text/calendar": "ics",
+ "application/java-archive": "jar",
+ "image/jpeg": "jpg",
+ "text/javascript": "js",
+ "application/json": "json",
+ "application/ld+json": "jsonld",
+ "audio/midi audio/x-midi": "midi",
+ "audio/mpeg": "mp3",
+ "video/mpeg": "mpeg",
+ "application/vnd.apple.installer+xml": "mpkg",
+ "application/vnd.oasis.opendocument.presentation": "odp",
+ "application/vnd.oasis.opendocument.spreadsheet": "ods",
+ "application/vnd.oasis.opendocument.text": "odt",
+ "audio/ogg": "oga",
+ "video/ogg": "ogv",
+ "application/ogg": "ogx",
+ "audio/opus": "opus",
+ "font/otf": "otf",
+ "image/png": "png",
+ "application/pdf": "pdf",
+ "application/x-httpd-php": "php",
+ "application/vnd.ms-powerpoint": "ppt",
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
+ "application/vnd.rar": "rar",
+ "application/rtf": "rtf",
+ "application/x-sh": "sh",
+ "image/svg+xml": "svg",
+ "application/x-shockwave-flash": "swf",
+ "application/x-tar": "tar",
+ "image/tiff": "tiff",
+ "video/mp2t": "ts",
+ "font/ttf": "ttf",
+ "text/plain": "txt",
+ "application/vnd.visio": "vsd",
+ "audio/wav": "wav",
+ "audio/webm": "weba",
+ "video/webm": "webm",
+ "image/webp": "webp",
+ "font/woff": "woff",
+ "font/woff2": "woff2",
+ "application/xhtml+xml": "xhtml",
+ "application/vnd.ms-excel": "xls",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
+ "application/xml": "xml",
+ "application/vnd.mozilla.xul+xml": "xul",
+ "application/zip": "zip",
+ "video/3gpp": "3gp",
+ "video/3gpp2": "3g2",
+ "audio/3gpp": "3gp",
+ "audio/3gpp2": "3g2",
+ "application/x-7z-compressed": "7z"
+}
diff --git a/src/utils/readable-mime-type/readable-mime-type.ts b/src/utils/readable-mime-type/readable-mime-type.ts
index 2e190ee715..1f4fb2718e 100644
--- a/src/utils/readable-mime-type/readable-mime-type.ts
+++ b/src/utils/readable-mime-type/readable-mime-type.ts
@@ -1,5 +1,10 @@
import types from './types.json';
+import extensions from './extensions.json';
+
+export default function readableMimeType(type: string, extension = false) {
+ if (extension) {
+ return (extensions as any)[type] || null;
+ }
-export default function readableMimeType(type: string) {
return (types as any)[type] || null;
}
diff --git a/src/views/private/components/module-bar-logo/module-bar-logo.story.ts b/src/views/private/components/module-bar-logo/module-bar-logo.story.ts
index 793cba94b8..7020bbbe5e 100644
--- a/src/views/private/components/module-bar-logo/module-bar-logo.story.ts
+++ b/src/views/private/components/module-bar-logo/module-bar-logo.story.ts
@@ -58,6 +58,8 @@ export const withCustomLogo = () =>
'https://demo.directus.io/uploads/thumper/originals/19acff06-4969-5c75-9cd5-dc3f27506de2.svg',
url:
'/uploads/thumper/originals/19acff06-4969-5c75-9cd5-dc3f27506de2.svg',
+ asset_url:
+ '/uploads/thumper/originals/19acff06-4969-5c75-9cd5-dc3f27506de2.svg',
},
project_color: '#4CAF50',
project_foreground: null,
diff --git a/src/views/private/components/module-bar-logo/module-bar-logo.test.ts b/src/views/private/components/module-bar-logo/module-bar-logo.test.ts
deleted file mode 100644
index 1eb5c1d119..0000000000
--- a/src/views/private/components/module-bar-logo/module-bar-logo.test.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import VueCompositionAPI from '@vue/composition-api';
-import ModuleBarLogo from './module-bar-logo.vue';
-import { useRequestsStore } from '@/stores/requests';
-import { useProjectsStore } from '@/stores/projects';
-
-const localVue = createLocalVue();
-localVue.use(VueCompositionAPI);
-
-describe('Views / Private / Module Bar / Logo', () => {
- it('Renders the default rabbit when we are not in a project', () => {
- const component = shallowMount(ModuleBarLogo, { localVue });
- expect((component.vm as any).customLogoPath).toBe(null);
- });
-
- it('Renders the default rabbit when the current project errored out', () => {
- const projectsStore = useProjectsStore({});
- projectsStore.currentProject = {
- value: {
- key: 'my-project',
- status: 500,
- error: {
- code: 400,
- message: 'Could not connect to the database',
- },
- },
- };
-
- const component = shallowMount(ModuleBarLogo, { localVue });
-
- expect((component.vm as any).customLogoPath).toBe(null);
- });
-
- it('Renders the default rabbit when the current project does not have a custom logo', () => {
- const projectsStore = useProjectsStore({});
- projectsStore.currentProject = {
- value: {
- key: 'my-project',
- api: {
- requires2FA: false,
- project_foreground: null,
- project_background: null,
- project_color: '#abcdef',
- project_public_note: '',
- default_locale: 'en-US',
- telemetry: false,
- project_name: 'test',
- project_logo: null,
- },
- },
- };
-
- const component = shallowMount(ModuleBarLogo, { localVue });
-
- expect((component.vm as any).customLogoPath).toBe(null);
- });
-
- it('Renders the custom logo if set', () => {
- const projectsStore = useProjectsStore({});
- projectsStore.currentProject = {
- value: {
- key: 'my-project',
- api: {
- requires2FA: false,
- project_foreground: null,
- project_background: null,
- project_color: '#abcdef',
- project_public_note: '',
- default_locale: 'en-US',
- telemetry: false,
- project_name: 'test',
- project_logo: {
- full_url: 'abc',
- url: 'abc',
- },
- },
- },
- };
-
- const component = shallowMount(ModuleBarLogo, { localVue });
-
- expect((component.vm as any).customLogoPath).toBe('abc');
- expect(component.find('img').attributes().src).toBe('abc');
- });
-
- it('Only stops running if the queue is empty', () => {
- const requestsStore = useRequestsStore({});
- requestsStore.queueHasItems = { value: false };
-
- let component = shallowMount(ModuleBarLogo, { localVue });
- (component.vm as any).isRunning = true;
- (component.vm as any).stopRunningIfQueueIsEmpty();
- expect((component.vm as any).isRunning).toBe(false);
-
- requestsStore.queueHasItems = { value: true };
- component = shallowMount(ModuleBarLogo, { localVue });
- expect((component.vm as any).isRunning).toBe(true);
- (component.vm as any).stopRunningIfQueueIsEmpty();
- expect((component.vm as any).isRunning).toBe(true);
-
- requestsStore.queueHasItems = { value: false };
- component = shallowMount(ModuleBarLogo, { localVue });
- (component.vm as any).stopRunningIfQueueIsEmpty();
- expect((component.vm as any).isRunning).toBe(false);
- });
-});
diff --git a/src/views/public/public-view.test.ts b/src/views/public/public-view.test.ts
index 7675aeeaf9..2ef407db90 100644
--- a/src/views/public/public-view.test.ts
+++ b/src/views/public/public-view.test.ts
@@ -33,17 +33,20 @@ const mockProject: ProjectWithKey = {
full_url:
'http://localhost:8080/uploads/my-project/originals/19acff06-4969-5c75-9cd5-dc3f27506de2.svg',
url: '/uploads/my-project/originals/19acff06-4969-5c75-9cd5-dc3f27506de2.svg',
+ asset_url: '/uploads/my-project/assets/abc',
},
project_color: '#4CAF50',
project_foreground: {
full_url:
'http://localhost:8080/uploads/my-project/originals/f28c49b0-2b4f-571e-bf62-593107cbf2ec.svg',
url: '/uploads/my-project/originals/f28c49b0-2b4f-571e-bf62-593107cbf2ec.svg',
+ asset_url: '/uploads/my-project/assets/abc',
},
project_background: {
full_url:
'http://localhost:8080/uploads/my-project/originals/03a06753-6794-4b9a-803b-3e1cd15e0742.jpg',
url: '/uploads/my-project/originals/03a06753-6794-4b9a-803b-3e1cd15e0742.jpg',
+ asset_url: '/uploads/my-project/assets/abc',
},
telemetry: true,
default_locale: 'en-US',
@@ -125,7 +128,7 @@ describe('Views / Public', () => {
store.state.projects = [mockProject];
store.state.currentProjectKey = 'my-project';
expect((component.vm as any).artStyles).toEqual({
- background: `url(${mockProject.api?.project_background?.full_url})`,
+ background: `url(${mockProject.api?.project_background?.asset_url})`,
backgroundPosition: 'center center',
backgroundSize: 'cover',
});