mirror of
https://github.com/directus/directus.git
synced 2026-01-28 18:18:10 -05:00
Merge branch 'main' into room-cleaning
This commit is contained in:
7
api/package-lock.json
generated
7
api/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
21
app/src/components/v-icon/custom-icons/bookmark_save.vue
Normal file
21
app/src/components/v-icon/custom-icons/bookmark_save.vue
Normal 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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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'),
|
||||
99
app/src/displays/labels/labels.vue
Normal file
99
app/src/displays/labels/labels.vue
Normal 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>
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
@@ -1,3 +0,0 @@
|
||||
# Tags
|
||||
|
||||
Renders a CSV of strings as individual chips.
|
||||
@@ -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" />
|
||||
`,
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
12
app/src/interfaces/system-language/index.ts
Normal file
12
app/src/interfaces/system-language/index.ts
Normal 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: [],
|
||||
}));
|
||||
29
app/src/interfaces/system-language/system-language.vue
Normal file
29
app/src/interfaces/system-language/system-language.vue
Normal 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>
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 || {}),
|
||||
|
||||
165
app/src/modules/collections/components/navigation-bookmark.vue
Normal file
165
app/src/modules/collections/components/navigation-bookmark.vue
Normal 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>
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: {},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"app",
|
||||
"packages/*"
|
||||
],
|
||||
"version": "9.0.0-alpha.32",
|
||||
"version": "9.0.0-alpha.33",
|
||||
"command": {
|
||||
"bootstrap": {
|
||||
"npmClientArgs": [
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user