Merge branch 'main' into room-cleaning

This commit is contained in:
Nitwel
2020-09-08 16:22:07 +02:00
38 changed files with 631 additions and 297 deletions

7
api/package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "directus",
"version": "9.0.0-alpha.32",
"version": "9.0.0-alpha.33",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3882,11 +3882,6 @@
}
}
},
"knex-schema-inspector": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/knex-schema-inspector/-/knex-schema-inspector-0.0.9.tgz",
"integrity": "sha512-WN3m3dSxadXKtXYIyP3bRLHr786EjUXzbj6PweTc6CJKOOEWHA/FK8aOqNcYAFzjIT8opNR/iAgV8ZssKerDpA=="
},
"levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "directus",
"version": "9.0.0-alpha.32",
"version": "9.0.0-alpha.33",
"license": "GPL-3.0-only",
"homepage": "https://github.com/directus/next#readme",
"description": "Directus is a real-time API and App dashboard for managing SQL database content.",
@@ -64,7 +64,7 @@
"example.env"
],
"dependencies": {
"@directus/app": "^9.0.0-alpha.32",
"@directus/app": "^9.0.0-alpha.33",
"@directus/format-title": "^3.2.0",
"@slynova/flydrive": "^1.0.2",
"@slynova/flydrive-gcs": "^1.0.2",

View File

@@ -634,22 +634,24 @@ rows:
special: json
interface: repeater
options:
template: '{{ locale }}'
template: '{{ translation }} ({{ locale }})'
fields:
- field: locale
name: Locale
name: Language
type: string
schema:
default_value: en-US
meta:
interface: language
options:
limit: true
interface: system-language
width: half
- field: translation
name: Translation
type: string
meta:
interface: text-input
interface: system-language
width: half
options:
placeholder: Enter a translation...
locked: true
sort: 8
width: full

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/app",
"version": "9.0.0-alpha.32",
"version": "9.0.0-alpha.33",
"private": false,
"description": "Directus is an Open-Source Headless CMS & API for Managing Custom Databases",
"author": "Rijk van Zanten <rijk@rngr.org>",

View File

@@ -64,7 +64,7 @@ export const onError = async (error: RequestError) => {
try {
newToken = await refresh();
} catch {
logout({ reason: LogoutReason.ERROR_SESSION_EXPIRED });
logout({ reason: LogoutReason.SESSION_EXPIRED });
return Promise.reject();
}

View File

@@ -49,13 +49,13 @@ export async function refresh({ navigate }: LogoutOptions = { navigate: true })
return accessToken;
} catch (error) {
await logout({ navigate, reason: LogoutReason.ERROR_SESSION_EXPIRED });
await logout({ navigate, reason: LogoutReason.SESSION_EXPIRED });
}
}
export enum LogoutReason {
SIGN_OUT = 'SIGN_OUT',
ERROR_SESSION_EXPIRED = 'ERROR_SESSION_EXPIRED',
SESSION_EXPIRED = 'SESSION_EXPIRED',
}
export type LogoutOptions = {

View File

@@ -0,0 +1,21 @@
<template functional>
<svg
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
fill-rule="evenodd"
clip-rule="evenodd"
stroke-linejoin="round"
stroke-miterlimit="2"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.38 3.6c-.38-.4-.83-.6-1.36-.6H6.98c-.53 0-1 .2-1.4.6-.38.42-.56.88-.56 1.42V21L12 18l6.98 3V5.02c0-.54-.2-1-.6-1.41zm-8.05 11.5l6.7-6.7-1.4-1.4-5.3 5.3-1.92-1.93L7 11.79l3.33 3.33z"
/>
</svg>
</template>
<script lang="ts">
export default {};
</script>

View File

@@ -16,6 +16,7 @@ import { defineComponent, computed } from '@vue/composition-api';
import useSizeClass, { sizeProps } from '@/composables/size-class';
import CustomIconDirectus from './custom-icons/directus.vue';
import CustomIconBookmarkSave from './custom-icons/bookmark_save.vue';
import CustomIconBox from './custom-icons/box.vue';
import CustomIconCommitNode from './custom-icons/commit_node.vue';
import CustomIconGrid1 from './custom-icons/grid_1.vue';
@@ -34,6 +35,7 @@ import CustomIconLogout from './custom-icons/logout.vue';
const customIcons: string[] = [
'directus',
'bookmark_save',
'box',
'commit_node',
'grid_1',
@@ -54,6 +56,7 @@ const customIcons: string[] = [
export default defineComponent({
components: {
CustomIconDirectus,
CustomIconBookmarkSave,
CustomIconBox,
CustomIconCommitNode,
CustomIconGrid1,

View File

@@ -13,7 +13,7 @@ device.
<v-tab><v-icon name="help" left /> Help</v-tab>
</v-tabs>
<v-tabs-items>
<v-tabs-items v-model="selection">
<v-tab-item>I'm the content for Home!</v-tab-item>
<v-tab-item>I'm the content for News!</v-tab-item>
<v-tab-item>I'm the content for Help!</v-tab-item>

View File

@@ -9,26 +9,47 @@ export function usePreset(collection: Ref<string>, bookmark: Ref<number | null>
const presetsStore = usePresetsStore();
const userStore = useUserStore();
const busy = ref(false);
const { info: collectionInfo } = useCollection(collection);
const bookmarkExists = computed(() => {
if (!bookmark.value) return false;
return !!presetsStore.state.collectionPresets.find((preset) => preset.id === bookmark.value);
});
const localPreset = ref<Partial<Preset>>({});
initLocalPreset();
const bookmarkSaved = computed(() => localPreset.value.$saved !== false);
const bookmarkIsMine = computed(() => localPreset.value.user === userStore.state.currentUser!.id);
const savePreset = async (preset?: Partial<Preset>) => {
busy.value = true;
const updatedValues = await presetsStore.savePreset(preset ? preset : localPreset.value);
initLocalPreset();
localPreset.value.id = updatedValues.id;
busy.value = false;
return updatedValues;
};
const saveLocal = () => {
presetsStore.saveLocal(localPreset.value);
initLocalPreset();
};
const clearLocalSave = async () => {
busy.value = true;
await presetsStore.clearLocalSave(localPreset.value);
initLocalPreset();
busy.value = false;
};
const autoSave = debounce(async () => {
if (!bookmark || bookmark.value === null) {
savePreset();
} else {
saveLocal();
}
}, 450);
@@ -143,6 +164,10 @@ export function usePreset(collection: Ref<string>, bookmark: Ref<number | null>
saveCurrentAsBookmark,
title,
resetPreset,
bookmarkSaved,
bookmarkIsMine,
busy,
clearLocalSave,
};
async function resetPreset() {

View File

@@ -1,15 +1,13 @@
import { computed, Ref } from '@vue/composition-api';
import { clone } from 'lodash';
export default function useSync<T, K extends keyof T>(
props: T,
key: K,
emit: (event: string, ...args: any[]) => void
): Ref<Readonly<T[K]>> {
return computed<T[K]>({
get() {
return clone(props[key]);
return props[key];
},
set(newVal) {
emit(`update:${key}`, newVal);

View File

@@ -1,70 +0,0 @@
<template>
<div>
<value-null v-if="value === null" />
<div class="badge" :style="styles">{{ displayValue }}</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from '@vue/composition-api';
import formatTitle from '@directus/format-title';
type Choice = {
value: string;
text: string;
foreground: string | null;
background: string | null;
};
export default defineComponent({
props: {
value: {
type: String,
default: null,
},
choices: {
type: Array as PropType<Choice[]>,
default: () => [],
},
defaultBackground: {
type: String,
default: '#eceff1',
},
defaultForeground: {
type: String,
default: '#263238',
},
},
setup(props) {
const currentChoice = computed(() => {
return props.choices.find((choice) => {
return choice.value === props.value;
});
});
const displayValue = computed(() => {
if (!currentChoice.value) return formatTitle(props.value);
return currentChoice.value.text;
});
const styles = computed(() => {
return {
color: currentChoice.value?.foreground || props.defaultForeground,
backgroundColor: currentChoice.value?.background || props.defaultBackground,
};
});
return { displayValue, styles };
},
});
</script>
<style lang="scss" scoped>
.badge {
display: inline-block;
padding: 8px;
line-height: 1;
vertical-align: middle;
border-radius: var(--border-radius);
}
</style>

View File

@@ -1,12 +1,12 @@
import { defineDisplay } from '@/displays/define';
import DisplayBadge from './badge.vue';
import DisplayLabels from './labels.vue';
export default defineDisplay(({ i18n }) => ({
id: 'badge',
name: i18n.t('badge'),
types: ['string'],
id: 'labels',
name: i18n.t('labels'),
types: ['string', 'json'],
icon: 'flag',
handler: DisplayBadge,
handler: DisplayLabels,
options: [
{
field: 'defaultForeground',
@@ -32,6 +32,18 @@ export default defineDisplay(({ i18n }) => ({
default_value: '#eceff1',
},
},
{
field: 'format',
name: i18n.t('format_text'),
type: 'boolean',
meta: {
width: 'half-left',
interface: 'toggle',
},
schema: {
default_value: true,
},
},
{
field: 'choices',
name: i18n.t('choices'),

View File

@@ -0,0 +1,99 @@
<template>
<div class="display-tags">
<v-chip
v-for="item in items"
:key="item.value"
:style="{
'--v-chip-color': item.foreground,
'--v-chip-background-color': item.background,
}"
small
disabled
label
>
{{ item.text }}
</v-chip>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from '@vue/composition-api';
import formatTitle from '@directus/format-title';
type Choice = {
value: string;
text: string;
foreground: string | null;
background: string | null;
};
export default defineComponent({
props: {
value: {
type: [String, Array] as PropType<string | string[]>,
required: true,
},
format: {
type: Boolean,
default: true,
},
choices: {
type: Array as PropType<Choice[]>,
default: () => [],
},
defaultBackground: {
type: String,
default: '#eceff1',
},
defaultForeground: {
type: String,
default: '#263238',
},
type: {
type: String,
validator: (val: string) => ['json', 'string'].includes(val),
},
},
setup(props) {
const items = computed(() => {
let items: string[];
if (props.value === null) items = [];
else if (props.type === 'string') items = [props.value as string];
else items = props.value as string[];
return items.map((item) => {
const choice = props.choices.find((choice) => choice.value === item);
if (choice === undefined) {
return {
value: item,
text: props.format ? formatTitle(item) : item,
foreground: props.defaultForeground,
background: props.defaultBackground,
};
} else {
return {
value: item,
text: choice.text || (props.format ? formatTitle(item) : item),
foreground: choice.foreground || props.defaultForeground,
background: choice.background || props.defaultBackground,
};
}
});
});
return { items };
},
});
</script>
<style lang="scss" scoped>
.display-tags {
display: inline-block;
}
.v-chip + .v-chip {
margin-left: 4px;
}
</style>

View File

@@ -1,24 +0,0 @@
import { defineDisplay } from '@/displays/define';
import DisplayTags from './tags.vue';
export default defineDisplay(({ i18n }) => ({
id: 'tags',
name: i18n.t('tags'),
types: ['json'],
icon: 'label',
handler: DisplayTags,
options: [
{
field: 'format',
name: i18n.t('format_text'),
type: 'boolean',
meta: {
width: 'half',
interface: 'toggle',
},
schema: {
default_value: true,
},
},
],
}));

View File

@@ -1,3 +0,0 @@
# Tags
Renders a CSV of strings as individual chips.

View File

@@ -1,24 +0,0 @@
import withPadding from '../../../.storybook/decorators/with-padding';
import { withKnobs, array } from '@storybook/addon-knobs';
import readme from './readme.md';
import { defineComponent } from '@vue/composition-api';
export default {
title: 'Displays / Tags',
decorators: [withPadding, withKnobs],
parameters: {
notes: readme,
},
};
export const basic = () =>
defineComponent({
props: {
value: {
default: array('Value', ['vip', 'executive']),
},
},
template: `
<display-tags :value="value" />
`,
});

View File

@@ -1,21 +0,0 @@
import DisplayTags from './tags.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import VChip from '@/components/v-chip';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-chip', VChip);
describe('Displays / Tags', () => {
it('Renders a chip for every value', () => {
const component = shallowMount(DisplayTags, {
localVue,
propsData: {
value: ['tag 1', 'tag 2', 'tag 3'],
},
});
expect(component.findAll(VChip).length).toBe(3);
});
});

View File

@@ -1,38 +0,0 @@
<template>
<div class="display-tags">
<v-chip v-for="val in value" :key="val" small disabled label>
{{ format ? formatTitle(val) : val }}
</v-chip>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import formatTitle from '@directus/format-title';
export default defineComponent({
props: {
value: {
type: Array as PropType<string[]>,
required: true,
},
format: {
type: Boolean,
default: true,
},
},
setup() {
return { formatTitle };
},
});
</script>
<style lang="scss" scoped>
.display-tags {
display: inline-block;
}
.v-chip + .v-chip {
margin-left: 4px;
}
</style>

View File

@@ -0,0 +1,12 @@
import InterfaceSystemLanguage from './system-language.vue';
import { defineInterface } from '@/interfaces/define';
export default defineInterface(({ i18n }) => ({
id: 'system-language',
name: i18n.t('language'),
icon: 'translate',
component: InterfaceSystemLanguage,
system: true,
types: ['string'],
options: [],
}));

View File

@@ -0,0 +1,29 @@
<template>
<v-select @input="$listeners.input" :value="value" :items="languages" :disabled="disabled" />
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { availableLanguages } from '@/lang';
export default defineComponent({
props: {
disabled: {
type: Boolean,
default: false,
},
value: {
type: String,
default: null,
},
},
setup(props) {
const languages = Object.entries(availableLanguages).map(([key, value]) => ({
text: value,
value: key,
}));
return { languages };
},
});
</script>

View File

@@ -19,6 +19,17 @@
"create_user": "Create User",
"rename_folder": "Rename Folder",
"delete_folder": "Delete Folder",
"reset_bookmark": "Reset Bookmark",
"rename_bookmark": "Rename Bookmark",
"update_bookmark": "Update Bookmark",
"delete_bookmark": "Delete Bookmark",
"delete_bookmark_copy": "Are you sure you want to delete the \"{bookmark}\" bookmark? This action cannot be undone.",
"logoutReason": {
"SIGN_OUT": "Signed out",
"SESSION_EXPIRED": "Session expired"
},
"public": "Public",
"public_description": "Controls what API data is available without authenticating.",
@@ -92,6 +103,8 @@
"color_dot": "Color Dot",
"default_color": "Default Color",
"labels": "Labels",
"global": "Global",
"admins_have_all_permissions": "Admins have all permissions",
@@ -281,7 +294,6 @@
"submit": "Submit",
"move_to_folder": "Move to Folder",
"delete_folder": "Delete Folder",
"select_folder": "Select Folder",
"move": "Move",
@@ -1022,8 +1034,6 @@
"delete": "Delete",
"delete_are_you_sure": "This action is permanent and can not be undone. Are you sure you would like to proceed?",
"delete_bookmark": "Delete Bookmark",
"delete_bookmark_body": "Are you sure you want to delete this bookmark? This action cannot be undone.",
"delete_confirmation": "Delete Confirmation",
"delete_field_are_you_sure": "Are you sure you want to delete the field \"{field}\"? This action can not be undone.",
"delete_role_are_you_sure": "Are you sure to delete the role \"{name}\"? This action cannot be undone.",

View File

@@ -143,7 +143,7 @@
<script lang="ts">
import Vue from 'vue';
import { defineComponent, PropType, ref, computed, inject, toRefs, Ref } from '@vue/composition-api';
import { defineComponent, PropType, ref, computed, inject, toRefs, Ref, watch } from '@vue/composition-api';
import { HeaderRaw, Item } from '@/components/v-table/types';
import { Field, Filter } from '@/types';
@@ -389,6 +389,13 @@ export default defineComponent({
const localWidths = ref<{ [field: string]: number }>({});
watch(
() => _viewOptions.value,
() => {
localWidths.value = {};
}
);
const saveWidthsToViewOptions = debounce(() => {
_viewOptions.value = {
...(_viewOptions.value || {}),

View File

@@ -0,0 +1,165 @@
<template>
<v-list-item exact :to="bookmark.to" class="bookmark" @contextmenu.native.prevent.stop="$refs.contextMenu.activate">
<v-list-item-icon><v-icon name="bookmark" /></v-list-item-icon>
<v-list-item-content>{{ bookmark.title }}</v-list-item-content>
<v-list-item-icon v-if="bookmark.scope !== 'user'" class="bookmark-scope">
<v-icon :name="bookmark.scope === 'role' ? 'people' : 'public'" />
</v-list-item-icon>
<v-menu ref="contextMenu" show-arrow placement="bottom-start">
<v-list dense>
<v-list-item @click="renameActive = true" :disabled="isMine === false">
<v-list-item-icon>
<v-icon name="edit" outline />
</v-list-item-icon>
<v-list-item-content>
{{ $t('rename_bookmark') }}
</v-list-item-content>
</v-list-item>
<v-list-item @click="deleteActive = true" class="danger" :disabled="isMine === false">
<v-list-item-icon>
<v-icon name="delete" outline />
</v-list-item-icon>
<v-list-item-content>
{{ $t('delete_bookmark') }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
<v-dialog v-model="renameActive" persistent>
<v-card>
<v-card-title>{{ $t('rename_bookmark') }}</v-card-title>
<v-card-text>
<v-input v-model="renameValue" autofocus @keyup.enter="renameSave" />
</v-card-text>
<v-card-actions>
<v-button secondary @click="renameActive = false">{{ $t('cancel') }}</v-button>
<v-button @click="renameSave" :loading="renameSaving">{{ $t('save') }}</v-button>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="deleteActive" persistent>
<v-card>
<v-card-title>{{ $t('delete_bookmark_copy', { bookmark: bookmark.title }) }}</v-card-title>
<v-card-actions>
<v-button secondary @click="deleteActive = false">{{ $t('cancel') }}</v-button>
<v-button @click="deleteSave" :loading="deleteSaving" class="action-delete">
{{ $t('delete') }}
</v-button>
</v-card-actions>
</v-card>
</v-dialog>
</v-list-item>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, computed } from '@vue/composition-api';
import { Preset } from '@/types';
import { useUserStore, usePresetsStore } from '@/stores';
export default defineComponent({
props: {
bookmark: {
type: Object as PropType<Preset>,
required: true,
},
},
setup(props) {
const contextMenu = ref();
const userStore = useUserStore();
const presetsStore = usePresetsStore();
const isMine = computed(() => props.bookmark.user === userStore.state.currentUser!.id);
const { renameActive, renameValue, renameSave, renameSaving } = useRenameBookmark();
const { deleteActive, deleteValue, deleteSave, deleteSaving } = useDeleteBookmark();
return {
contextMenu,
isMine,
renameActive,
renameValue,
renameSave,
renameSaving,
deleteActive,
deleteValue,
deleteSave,
deleteSaving,
};
function useRenameBookmark() {
const renameActive = ref(false);
const renameValue = ref(props.bookmark.title);
const renameSaving = ref(false);
return { renameActive, renameValue, renameSave, renameSaving };
async function renameSave() {
renameSaving.value = true;
try {
await presetsStore.savePreset({
...props.bookmark,
title: renameValue.value,
});
renameActive.value = false;
} catch (error) {
console.error(error);
} finally {
renameSaving.value = false;
}
}
}
function useDeleteBookmark() {
const deleteActive = ref(false);
const deleteValue = ref(props.bookmark.title);
const deleteSaving = ref(false);
return { deleteActive, deleteValue, deleteSave, deleteSaving };
async function deleteSave() {
deleteSaving.value = true;
try {
await presetsStore.savePreset(props.bookmark.id);
deleteActive.value = false;
} catch (error) {
console.error(error);
} finally {
deleteSaving.value = false;
}
}
}
},
});
</script>
<style lang="scss" scoped>
.bookmark-scope {
--v-icon-color: var(--foreground-subdued);
opacity: 0;
transition: opacity var(--fast) var(--transition);
}
.bookmark:hover .bookmark-scope {
opacity: 1;
}
.danger {
--v-list-item-color: var(--danger);
--v-list-item-icon-color: var(--danger);
}
.action-delete {
--v-button-background-color: var(--danger-25);
--v-button-color: var(--danger);
--v-button-background-color-hover: var(--danger-50);
--v-button-color-hover: var(--danger);
}
</style>

View File

@@ -19,10 +19,7 @@
<template v-if="bookmarks.length > 0">
<v-divider />
<v-list-item exact v-for="bookmark in bookmarks" :key="bookmark.id" :to="bookmark.to">
<v-list-item-icon><v-icon name="bookmark" /></v-list-item-icon>
<v-list-item-content>{{ bookmark.title }}</v-list-item-content>
</v-list-item>
<navigation-bookmark v-for="bookmark of bookmarks" :key="bookmark.id" :bookmark="bookmark" />
</template>
<div v-if="!customNavItems && !navItems.length && !bookmarks.length" class="empty">
@@ -33,17 +30,18 @@
{{ $t('no_collections_copy') }}
</template>
</div>
</v-list>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import useNavigation from '../composables/use-navigation';
import { usePresetsStore } from '@/stores/';
import { useUserStore } from '@/stores';
import { usePresetsStore, useUserStore } from '@/stores/';
import { orderBy } from 'lodash';
import NavigationBookmark from './navigation-bookmark.vue';
export default defineComponent({
components: { NavigationBookmark },
props: {
exact: {
type: Boolean,
@@ -57,16 +55,25 @@ export default defineComponent({
const isAdmin = computed(() => userStore.state.currentUser?.role.admin === true);
const bookmarks = computed(() => {
return presetsStore.state.collectionPresets
.filter((preset) => {
return preset.title !== null && preset.collection.startsWith('directus_') === false;
})
.map((preset) => {
return {
...preset,
to: `/collections/${preset.collection}?bookmark=${preset.id}`,
};
});
return orderBy(
presetsStore.state.collectionPresets
.filter((preset) => {
return preset.title !== null && preset.collection.startsWith('directus_') === false;
})
.map((preset) => {
let scope = 'global';
if (!!preset.role) scope = 'role';
if (!!preset.user) scope = 'user';
return {
...preset,
to: `/collections/${preset.collection}?bookmark=${preset.id}`,
scope,
};
}),
['title'],
['asc']
);
});
return { navItems, bookmarks, customNavItems, isAdmin };

View File

@@ -13,30 +13,45 @@
</template>
<template #title-outer:append>
<bookmark-add
v-if="!bookmark"
class="bookmark-add"
v-model="bookmarkDialogActive"
@save="createBookmark"
:saving="creatingBookmark"
>
<template #activator="{ on }">
<v-icon class="toggle" name="bookmark_outline" @click="on" />
</template>
</bookmark-add>
<div class="bookmark-controls">
<bookmark-add
v-if="!bookmark"
class="add"
v-model="bookmarkDialogActive"
@save="createBookmark"
:saving="creatingBookmark"
>
<template #activator="{ on }">
<v-icon class="toggle" name="bookmark_outline" @click="on" />
</template>
</bookmark-add>
<bookmark-edit
v-else
class="bookmark-edit"
v-model="bookmarkDialogActive"
:saving="editingBookmark"
:name="bookmarkName"
@save="editBookmark"
>
<template #activator="{ on }">
<v-icon class="toggle" name="bookmark" @click="on" />
<v-icon class="saved" name="bookmark" v-else-if="bookmarkSaved" />
<template v-else-if="bookmarkIsMine">
<v-icon class="save" @click="savePreset()" name="bookmark_save" v-tooltip.bottom="$t('update_bookmark')" />
</template>
</bookmark-edit>
<bookmark-add
v-else
class="add"
v-model="bookmarkDialogActive"
@save="createBookmark"
:saving="creatingBookmark"
>
<template #activator="{ on }">
<v-icon class="toggle" name="bookmark_outline" @click="on" />
</template>
</bookmark-add>
<v-icon
v-if="bookmark && !bookmarkSaving && bookmarkSaved === false"
name="settings_backup_restore"
@click="clearLocalSave"
class="clear"
v-tooltip.bottom="$t('reset_bookmark')"
/>
</div>
</template>
<template #actions:prepend>
@@ -277,6 +292,10 @@ export default defineComponent({
saveCurrentAsBookmark,
title: bookmarkName,
resetPreset,
bookmarkSaved,
bookmarkIsMine,
busy: bookmarkSaving,
clearLocalSave,
} = usePreset(collection, bookmarkID);
const {
@@ -345,6 +364,10 @@ export default defineComponent({
deleteError,
createAllowed,
resetPreset,
bookmarkSaved,
bookmarkIsMine,
bookmarkSaving,
clearLocalSave,
};
function useBreadcrumb() {
@@ -559,25 +582,50 @@ export default defineComponent({
--layout-offset-top: 64px;
}
.bookmark-add .toggle,
.bookmark-edit .toggle {
margin-left: 8px;
transition: color var(--fast) var(--transition);
}
.bookmark-add {
color: var(--foreground-subdued);
&:hover {
color: var(--foreground-normal);
}
}
.bookmark-edit {
color: var(--primary);
}
.header-icon {
--v-button-color-disabled: var(--foreground-normal);
}
.bookmark-controls {
.add,
.save,
.saved,
.clear {
display: inline-block;
margin-left: 8px;
}
.add,
.save,
.clear {
cursor: pointer;
color: var(--foreground-subdued);
transition: color var(--fast) var(--transition);
&:hover {
color: var(--foreground-normal);
}
}
.save {
color: var(--warning);
&:hover {
color: var(--warning-125);
}
}
.saved {
color: var(--primary);
}
.clear {
color: var(--foreground-subdued);
margin-left: 4px;
&:hover {
color: var(--warning);
}
}
}
</style>

View File

@@ -70,7 +70,7 @@
<v-card>
<v-card-title>{{ $t('rename_folder') }}</v-card-title>
<v-card-text>
<v-input v-model="renameValue" />
<v-input v-model="renameValue" autofocus @keyup.enter="renameSave" />
</v-card-text>
<v-card-actions>
<v-button secondary @click="renameActive = false">{{ $t('cancel') }}</v-button>

View File

@@ -98,6 +98,40 @@
<v-checkbox v-model="fieldData.schema.is_nullable" :label="$t('allow_null_label')" block />
</div>
<div class="field full">
<div class="label type-label">{{ $t('translation') }}</div>
<interface-repeater
v-model="fieldData.meta.translation"
:template="'{{ translation }} ({{ locale }})'"
:fields="[
{
field: 'locale',
type: 'string',
name: $t('language'),
meta: {
interface: 'system-language',
width: 'half',
},
schema: {
default_value: 'en-US'
},
},
{
field: 'translation',
type: 'string',
name: $t('translation'),
meta: {
interface: 'text-input',
width: 'half',
options: {
placeholder: 'Enter a translation...'
},
},
},
]"
/>
</div>
<!--
@todo add unique when the API supports it

View File

@@ -143,30 +143,30 @@ export default defineComponent({
label: 'sort',
icon: 'low_priority',
},
userCreated: {
enabled: false,
name: 'user_created',
label: 'created_by',
icon: 'account_circle',
},
userUpdated: {
enabled: false,
name: 'user_updated',
label: 'updated_by',
icon: 'account_circle',
},
dateCreated: {
enabled: false,
name: 'date_created',
label: 'created_on',
icon: 'access_time',
},
userCreated: {
enabled: false,
name: 'user_created',
label: 'created_by',
icon: 'account_circle',
},
dateUpdated: {
enabled: false,
name: 'date_updated',
label: 'updated_on',
icon: 'access_time',
},
userUpdated: {
enabled: false,
name: 'user_updated',
label: 'updated_by',
icon: 'account_circle',
},
});
const saving = ref(false);
@@ -332,7 +332,7 @@ export default defineComponent({
},
readonly: true,
hidden: true,
width: 'full',
width: 'half',
},
schema: {},
});
@@ -347,7 +347,7 @@ export default defineComponent({
interface: 'datetime',
readonly: true,
hidden: true,
width: 'full',
width: 'half',
},
schema: {},
});
@@ -366,7 +366,7 @@ export default defineComponent({
},
readonly: true,
hidden: true,
width: 'full',
width: 'half',
},
schema: {},
});
@@ -381,7 +381,7 @@ export default defineComponent({
interface: 'datetime',
readonly: true,
hidden: true,
width: 'full',
width: 'half',
},
schema: {},
});

View File

@@ -169,7 +169,7 @@ import CommentsDrawerDetail from '@/views/private/components/comments-drawer-det
import useItem from '@/composables/use-item';
import SaveOptions from '@/views/private/components/save-options';
import api from '@/api';
import { useFieldsStore } from '@/stores/';
import { useFieldsStore, useUserStore } from '@/stores/';
import useFormFields from '@/composables/use-form-fields';
import { Field } from '@/types';
import UserInfoDrawerDetail from '../components/user-info-drawer-detail.vue';
@@ -209,6 +209,7 @@ export default defineComponent({
},
setup(props) {
const fieldsStore = useFieldsStore();
const userStore = useUserStore();
const { primaryKey } = toRefs(props);
const { breadcrumb } = useBreadcrumb();
@@ -344,6 +345,7 @@ export default defineComponent({
async function saveAndQuit() {
await save();
await refreshCurrentUser();
router.push(`/users`);
}
@@ -361,6 +363,7 @@ export default defineComponent({
async function saveAndAddNew() {
await save();
await refreshCurrentUser();
router.push(`/users/+`);
}
@@ -374,6 +377,12 @@ export default defineComponent({
router.push(`/users`);
}
async function refreshCurrentUser() {
if (userStore.state.currentUser!.id === item.value.id) {
await userStore.hydrate();
}
}
function useUserPreview() {
const loading = ref(false);
const error = ref(null);

View File

@@ -20,6 +20,7 @@ export const defaultRoutes: RouteConfig[] = [
component: LoginRoute,
props: (route) => ({
ssoErrorCode: route.query.error ? route.query.code : null,
logoutReason: route.query.reason,
}),
meta: {
public: true,

View File

@@ -12,23 +12,33 @@
</template>
<template v-else #notice>
<v-icon name="lock_outlined" left />
{{ $t('not_authenticated') }}
{{
logoutReason && $te(`logoutReason.${logoutReason}`)
? $t(`logoutReason.${logoutReason}`)
: $t('not_authenticated')
}}
</template>
</public-view>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed, PropType } from '@vue/composition-api';
import LoginForm from './components/login-form/';
import ContinueAs from './components/continue-as/';
import { useAppStore, useSettingsStore } from '@/stores';
import { LogoutReason } from '@/auth';
export default defineComponent({
props: {
ssoErrorCode: {
type: String,
default: null,
},
logoutReason: {
type: String as PropType<LogoutReason>,
default: null,
},
},
components: { LoginForm, ContinueAs },
setup() {

View File

@@ -34,13 +34,13 @@ export const useCollectionsStore = createStore({
const { locale, translation } = collection.meta.translation[i];
i18n.mergeLocaleMessage(locale, {
collections: {
collection_names: {
[collection.collection]: translation,
},
});
}
name = i18n.t(`collections.${collection.collection}`);
name = i18n.t(`collection_names.${collection.collection}`);
} else {
name = formatTitle(collection.collection);
}

View File

@@ -22,7 +22,6 @@ export const usePresetsStore = createStore({
actions: {
async hydrate() {
// Hydrate is only called for logged in users, therefore, currentUser exists
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { id, role } = useUserStore().state.currentUser!;
const values = await Promise.all([
@@ -65,6 +64,7 @@ export const usePresetsStore = createStore({
this.state.collectionPresets = this.state.collectionPresets.map((preset) => {
const updatedPreset = response.data.data;
if (preset.id === updatedPreset.id) {
return updatedPreset;
}
@@ -167,5 +167,31 @@ export const usePresetsStore = createStore({
return await this.update(id, preset);
}
},
saveLocal(updatedPreset: Preset) {
this.state.collectionPresets = this.state.collectionPresets.map((preset) => {
if (preset.id === updatedPreset.id) {
return {
...updatedPreset,
$saved: false,
};
}
return preset;
});
},
async clearLocalSave(preset: Preset) {
const response = await api.get(`/presets/${preset.id}`);
this.state.collectionPresets = this.state.collectionPresets.map((preset) => {
if (preset.id === response.data.data.id) {
console.log('replace');
return response.data.data;
}
return preset;
});
},
},
});

View File

@@ -37,8 +37,9 @@ export type Preset = {
search_query: string | null;
filters: readonly Filter[] | null;
view_type: string | null;
view_query: { [view_type: string]: any } | null;
view_options: { [view_type: string]: any } | null;
// App flag to indicate that the local copy hasn't been saved to the API yet
$saved?: false;
};

View File

@@ -4,7 +4,7 @@
"app",
"packages/*"
],
"version": "9.0.0-alpha.32",
"version": "9.0.0-alpha.33",
"command": {
"bootstrap": {
"npmClientArgs": [

View File

@@ -1,6 +1,6 @@
{
"name": "create-directus-project",
"version": "9.0.0-alpha.32",
"version": "9.0.0-alpha.33",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-directus-project",
"version": "9.0.0-alpha.32",
"version": "9.0.0-alpha.33",
"description": "A small installer util that will create a directory, add boilerplate folders, and install Directus through npm.",
"main": "lib/index.js",
"bin": "./lib/index.js",