script[setup]: modules/settings/data-model (#18445)

This commit is contained in:
Rijk van Zanten
2023-05-03 11:41:20 -04:00
committed by GitHub
parent d95215672a
commit 2f1c069df8
26 changed files with 1718 additions and 2204 deletions

View File

@@ -107,111 +107,93 @@
</private-view>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, computed, ref } from 'vue';
import SettingsNavigation from '../../../components/navigation.vue';
<script setup lang="ts">
import api from '@/api';
import { useCollectionsStore } from '@/stores/collections';
import { Collection } from '@/types/collections';
import CollectionOptions from './components/collection-options.vue';
import { sortBy, merge } from 'lodash';
import CollectionItem from './components/collection-item.vue';
import { translate } from '@/utils/translate-object-values';
import Draggable from 'vuedraggable';
import { unexpectedError } from '@/utils/unexpected-error';
import api from '@/api';
import { merge, sortBy } from 'lodash';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Draggable from 'vuedraggable';
import SettingsNavigation from '../../../components/navigation.vue';
import CollectionDialog from './components/collection-dialog.vue';
import CollectionItem from './components/collection-item.vue';
import CollectionOptions from './components/collection-options.vue';
export default defineComponent({
components: { SettingsNavigation, CollectionItem, CollectionOptions, Draggable, CollectionDialog },
setup() {
const { t } = useI18n();
const { t } = useI18n();
const collectionDialogActive = ref(false);
const editCollection = ref<Collection | null>();
const collectionDialogActive = ref(false);
const editCollection = ref<Collection | null>();
const collectionsStore = useCollectionsStore();
const collectionsStore = useCollectionsStore();
const collections = computed(() => {
return translate(
sortBy(
collectionsStore.collections.filter(
(collection) => collection.collection.startsWith('directus_') === false && collection.meta
),
['meta.sort', 'collection']
)
);
});
const rootCollections = computed(() => {
return collections.value.filter((collection) => !collection.meta?.group);
});
const tableCollections = computed(() => {
return translate(
sortBy(
collectionsStore.collections.filter(
(collection) =>
collection.collection.startsWith('directus_') === false &&
!!collection.meta === false &&
collection.schema
),
['meta.sort', 'collection']
)
);
});
const systemCollections = computed(() => {
return translate(
sortBy(
collectionsStore.collections
.filter((collection) => collection.collection.startsWith('directus_') === true)
.map((collection) => ({ ...collection, icon: 'settings' })),
'collection'
)
);
});
return {
collectionDialogActive,
t,
collections,
tableCollections,
systemCollections,
onSort,
rootCollections,
editCollection,
};
async function onSort(updates: Collection[], removeGroup = false) {
const updatesWithSortValue = updates.map((collection, index) =>
merge(collection, { meta: { sort: index + 1, group: removeGroup ? null : collection.meta?.group } })
);
collectionsStore.collections = collectionsStore.collections.map((collection) => {
const updatedValues = updatesWithSortValue.find(
(updatedCollection) => updatedCollection.collection === collection.collection
);
return updatedValues ? merge({}, collection, updatedValues) : collection;
});
try {
api.patch(
`/collections`,
updatesWithSortValue.map((collection) => {
return {
collection: collection.collection,
meta: { sort: collection.meta.sort, group: collection.meta.group },
};
})
);
} catch (err: any) {
unexpectedError(err);
}
}
},
const collections = computed(() => {
return translate(
sortBy(
collectionsStore.collections.filter(
(collection) => collection.collection.startsWith('directus_') === false && collection.meta
),
['meta.sort', 'collection']
)
);
});
const rootCollections = computed(() => {
return collections.value.filter((collection) => !collection.meta?.group);
});
const tableCollections = computed(() => {
return translate(
sortBy(
collectionsStore.collections.filter(
(collection) =>
collection.collection.startsWith('directus_') === false && !!collection.meta === false && collection.schema
),
['meta.sort', 'collection']
)
);
});
const systemCollections = computed(() => {
return translate(
sortBy(
collectionsStore.collections
.filter((collection) => collection.collection.startsWith('directus_') === true)
.map((collection) => ({ ...collection, icon: 'settings' })),
'collection'
)
);
});
async function onSort(updates: Collection[], removeGroup = false) {
const updatesWithSortValue = updates.map((collection, index) =>
merge(collection, { meta: { sort: index + 1, group: removeGroup ? null : collection.meta?.group } })
);
collectionsStore.collections = collectionsStore.collections.map((collection) => {
const updatedValues = updatesWithSortValue.find(
(updatedCollection) => updatedCollection.collection === collection.collection
);
return updatedValues ? merge({}, collection, updatedValues) : collection;
});
try {
api.patch(
`/collections`,
updatesWithSortValue.map((collection) => {
return {
collection: collection.collection,
meta: { sort: collection.meta.sort, group: collection.meta.group },
};
})
);
} catch (err: any) {
unexpectedError(err);
}
}
</script>
<style scoped lang="scss">

View File

@@ -70,83 +70,77 @@
</v-dialog>
</template>
<script lang="ts">
<script setup lang="ts">
import api from '@/api';
import { unexpectedError } from '@/utils/unexpected-error';
import { defineComponent, ref, reactive, PropType, watch } from 'vue';
import { ref, reactive, watch } from 'vue';
import { useCollectionsStore } from '@/stores/collections';
import { useI18n } from 'vue-i18n';
import { isEqual } from 'lodash';
import { Collection } from '@/types/collections';
export default defineComponent({
name: 'CollectionDialog',
props: {
modelValue: {
type: Boolean,
default: false,
},
collection: {
type: Object as PropType<Collection>,
default: null,
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const { t } = useI18n();
const props = withDefaults(
defineProps<{
modelValue?: boolean;
collection?: Collection;
}>(),
{
modelValue: false,
}
);
const collectionsStore = useCollectionsStore();
const emit = defineEmits(['update:modelValue']);
const values = reactive({
collection: props.collection?.collection ?? null,
icon: props.collection?.icon ?? 'folder',
note: props.collection?.meta?.note ?? null,
color: props.collection?.color ?? null,
translations: props.collection?.meta?.translations ?? null,
});
const { t } = useI18n();
watch(
() => props.modelValue,
(newValue, oldValue) => {
if (isEqual(newValue, oldValue) === false) {
values.collection = props.collection?.collection ?? null;
values.icon = props.collection?.icon ?? 'folder';
values.note = props.collection?.meta?.note ?? null;
values.color = props.collection?.color ?? null;
values.translations = props.collection?.meta?.translations ?? null;
}
}
);
const collectionsStore = useCollectionsStore();
const saving = ref(false);
return { values, cancel, saving, save, t };
function cancel() {
emit('update:modelValue', false);
}
async function save() {
saving.value = true;
try {
if (props.collection) {
await api.patch(`/collections/${props.collection.collection}`, { meta: values });
await collectionsStore.hydrate();
} else {
await api.post<any>('/collections', { collection: values.collection, meta: values });
await collectionsStore.hydrate();
}
emit('update:modelValue', false);
} catch (err) {
unexpectedError(err);
} finally {
saving.value = false;
}
}
},
const values = reactive({
collection: props.collection?.collection ?? null,
icon: props.collection?.icon ?? 'folder',
note: props.collection?.meta?.note ?? null,
color: props.collection?.color ?? null,
translations: props.collection?.meta?.translations ?? null,
});
watch(
() => props.modelValue,
(newValue, oldValue) => {
if (isEqual(newValue, oldValue) === false) {
values.collection = props.collection?.collection ?? null;
values.icon = props.collection?.icon ?? 'folder';
values.note = props.collection?.meta?.note ?? null;
values.color = props.collection?.color ?? null;
values.translations = props.collection?.meta?.translations ?? null;
}
}
);
const saving = ref(false);
function cancel() {
emit('update:modelValue', false);
}
async function save() {
saving.value = true;
try {
if (props.collection) {
await api.patch(`/collections/${props.collection.collection}`, { meta: values });
await collectionsStore.hydrate();
} else {
await api.post<any>('/collections', { collection: values.collection, meta: values });
await collectionsStore.hydrate();
}
emit('update:modelValue', false);
} catch (err) {
unexpectedError(err);
} finally {
saving.value = false;
}
}
</script>
<style scoped>

View File

@@ -56,8 +56,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed, ref } from 'vue';
<script setup lang="ts">
import { computed, ref } from 'vue';
import CollectionOptions from './collection-options.vue';
import { Collection } from '@/types/collections';
import Draggable from 'vuedraggable';
@@ -66,108 +66,85 @@ import { DeepPartial } from '@directus/types';
import { useI18n } from 'vue-i18n';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
name: 'CollectionItem',
components: { CollectionOptions, Draggable },
props: {
collection: {
type: Object as PropType<Collection>,
required: true,
},
collections: {
type: Array as PropType<Collection[]>,
required: true,
},
disableDrag: {
type: Boolean,
default: false,
},
},
emits: ['setNestedSort', 'editCollection'],
setup(props, { emit }) {
const collectionsStore = useCollectionsStore();
const { t } = useI18n();
const props = defineProps<{
collection: Collection;
collections: Collection[];
disableDrag?: boolean;
}>();
const nestedCollections = computed(() =>
props.collections.filter((collection) => collection.meta?.group === props.collection.collection)
);
const emit = defineEmits(['setNestedSort', 'editCollection']);
const collapseIcon = computed(() => {
switch (props.collection.meta?.collapse) {
case 'open':
return 'folder_open';
case 'closed':
return 'folder';
case 'locked':
return 'folder_lock';
}
const collectionsStore = useCollectionsStore();
const { t } = useI18n();
return undefined;
});
const nestedCollections = computed(() =>
props.collections.filter((collection) => collection.meta?.group === props.collection.collection)
);
const collapseTooltip = computed(() => {
switch (props.collection.meta?.collapse) {
case 'open':
return t('start_open');
case 'closed':
return t('start_collapsed');
case 'locked':
return t('always_open');
}
const collapseIcon = computed(() => {
switch (props.collection.meta?.collapse) {
case 'open':
return 'folder_open';
case 'closed':
return 'folder';
case 'locked':
return 'folder_lock';
}
return undefined;
});
const collapseLoading = ref(false);
return {
collapseIcon,
onGroupSortChange,
nestedCollections,
update,
toggleCollapse,
collapseTooltip,
collapseLoading,
};
async function toggleCollapse() {
if (collapseLoading.value === true) return;
collapseLoading.value = true;
let newCollapse: 'open' | 'closed' | 'locked' = 'open';
if (props.collection.meta?.collapse === 'open') {
newCollapse = 'closed';
} else if (props.collection.meta?.collapse === 'closed') {
newCollapse = 'locked';
}
try {
await update({ meta: { collapse: newCollapse } });
} catch (err: any) {
unexpectedError(err);
} finally {
collapseLoading.value = false;
}
}
async function update(updates: DeepPartial<Collection>) {
await collectionsStore.updateCollection(props.collection.collection, updates);
}
function onGroupSortChange(collections: Collection[]) {
const updates = collections.map((collection) => ({
collection: collection.collection,
meta: {
group: props.collection.collection,
},
}));
emit('setNestedSort', updates);
}
},
return undefined;
});
const collapseTooltip = computed(() => {
switch (props.collection.meta?.collapse) {
case 'open':
return t('start_open');
case 'closed':
return t('start_collapsed');
case 'locked':
return t('always_open');
}
return undefined;
});
const collapseLoading = ref(false);
async function toggleCollapse() {
if (collapseLoading.value === true) return;
collapseLoading.value = true;
let newCollapse: 'open' | 'closed' | 'locked' = 'open';
if (props.collection.meta?.collapse === 'open') {
newCollapse = 'closed';
} else if (props.collection.meta?.collapse === 'closed') {
newCollapse = 'locked';
}
try {
await update({ meta: { collapse: newCollapse } });
} catch (err: any) {
unexpectedError(err);
} finally {
collapseLoading.value = false;
}
}
async function update(updates: DeepPartial<Collection>) {
await collectionsStore.updateCollection(props.collection.collection, updates);
}
function onGroupSortChange(collections: Collection[]) {
const updates = collections.map((collection) => ({
collection: collection.collection,
meta: {
group: props.collection.collection,
},
}));
emit('setNestedSort', updates);
}
</script>
<style scoped>

View File

@@ -11,21 +11,15 @@
</v-button>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useFieldDetailStore } from '../store';
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useFieldDetailStore } from '../store';
export default defineComponent({
emits: ['save'],
setup() {
const fieldDetailStore = useFieldDetailStore();
const { saving, readyToSave } = storeToRefs(fieldDetailStore);
defineEmits(['save']);
const { t } = useI18n();
const fieldDetailStore = useFieldDetailStore();
const { saving, readyToSave } = storeToRefs(fieldDetailStore);
return { saving, t, readyToSave };
},
});
const { t } = useI18n();
</script>

View File

@@ -22,95 +22,89 @@
</div>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, computed } from 'vue';
import { clone } from 'lodash';
import { useFieldDetailStore, syncFieldDetailStoreProperty } from '../store';
import { storeToRefs } from 'pinia';
import ExtensionOptions from '../shared/extension-options.vue';
<script setup lang="ts">
import { FancySelectItem } from '@/components/v-fancy-select.vue';
import { useExtension } from '@/composables/use-extension';
import { clone } from 'lodash';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import ExtensionOptions from '../shared/extension-options.vue';
import { syncFieldDetailStoreProperty, useFieldDetailStore } from '../store';
export default defineComponent({
components: { ExtensionOptions },
setup() {
const { t } = useI18n();
const { t } = useI18n();
const fieldDetailStore = useFieldDetailStore();
const fieldDetailStore = useFieldDetailStore();
const { loading, field, displaysForType } = storeToRefs(fieldDetailStore);
const { loading, field, displaysForType } = storeToRefs(fieldDetailStore);
const interfaceId = computed(() => field.value.meta?.interface ?? null);
const display = syncFieldDetailStoreProperty('field.meta.display');
const interfaceId = computed(() => field.value.meta?.interface ?? null);
const display = syncFieldDetailStoreProperty('field.meta.display');
const selectedInterface = useExtension('interface', interfaceId);
const selectedDisplay = useExtension('display', display);
const selectedInterface = useExtension('interface', interfaceId);
const selectedDisplay = useExtension('display', display);
const selectItems = computed(() => {
let recommended = clone(selectedInterface.value?.recommendedDisplays) || [];
const selectItems = computed(() => {
let recommended = clone(selectedInterface.value?.recommendedDisplays) || [];
recommended.push('raw', 'formatted-value');
recommended = [...new Set(recommended)];
recommended.push('raw', 'formatted-value');
recommended = [...new Set(recommended)];
const displayItems: FancySelectItem[] = displaysForType.value.map((display) => {
const item: FancySelectItem = {
text: display.name,
description: display.description,
value: display.id,
icon: display.icon,
};
const displayItems: FancySelectItem[] = displaysForType.value.map((display) => {
const item: FancySelectItem = {
text: display.name,
description: display.description,
value: display.id,
icon: display.icon,
};
if (recommended.includes(item.value as string)) {
item.iconRight = 'star';
}
if (recommended.includes(item.value as string)) {
item.iconRight = 'star';
}
return item;
});
return item;
});
const recommendedItems: (FancySelectItem | { divider: boolean } | undefined)[] = [];
const recommendedItems: (FancySelectItem | { divider: boolean } | undefined)[] = [];
const recommendedList = recommended.map((key: any) => displayItems.find((item) => item.value === key));
const recommendedList = recommended.map((key: any) => displayItems.find((item) => item.value === key));
if (recommendedList !== undefined) {
recommendedItems.push(...recommendedList.filter((i: any) => i));
}
if (recommendedList !== undefined) {
recommendedItems.push(...recommendedList.filter((i: any) => i));
}
if (displayItems.length >= 5 && recommended.length > 0) {
recommendedItems.push({ divider: true });
}
if (displayItems.length >= 5 && recommended.length > 0) {
recommendedItems.push({ divider: true });
}
const displayList = displayItems.filter((item) => recommended.includes(item.value as string) === false);
const displayList = displayItems.filter((item) => recommended.includes(item.value as string) === false);
if (displayList !== undefined) {
recommendedItems.push(...displayList.filter((i) => i));
}
if (displayList !== undefined) {
recommendedItems.push(...displayList.filter((i) => i));
}
return recommendedItems;
return recommendedItems;
});
const customOptionsFields = computed(() => {
if (typeof selectedDisplay.value?.options === 'function') {
return selectedDisplay.value?.options(fieldDetailStore);
}
return null;
});
const options = computed({
get() {
return fieldDetailStore.field.meta?.display_options ?? {};
},
set(newOptions: Record<string, any>) {
fieldDetailStore.$patch((state) => {
state.field.meta = {
...(state.field.meta ?? {}),
display_options: newOptions,
};
});
const customOptionsFields = computed(() => {
if (typeof selectedDisplay.value?.options === 'function') {
return selectedDisplay.value?.options(fieldDetailStore);
}
return null;
});
const options = computed({
get() {
return fieldDetailStore.field.meta?.display_options ?? {};
},
set(newOptions: Record<string, any>) {
fieldDetailStore.$patch((state) => {
state.field.meta = {
...(state.field.meta ?? {}),
display_options: newOptions,
};
});
},
});
return { t, loading, selectItems, selectedDisplay, display, options, customOptionsFields };
},
});
</script>

View File

@@ -71,27 +71,22 @@
</div>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, computed } from 'vue';
import { useFieldDetailStore, syncFieldDetailStoreProperty } from '../store';
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { syncFieldDetailStoreProperty, useFieldDetailStore } from '../store';
export default defineComponent({
setup() {
const { t } = useI18n();
const fieldDetailStore = useFieldDetailStore();
const readonly = syncFieldDetailStoreProperty('field.meta.readonly', false);
const hidden = syncFieldDetailStoreProperty('field.meta.hidden', false);
const required = syncFieldDetailStoreProperty('field.meta.required', false);
const note = syncFieldDetailStoreProperty('field.meta.note');
const translations = syncFieldDetailStoreProperty('field.meta.translations');
const { loading, field } = storeToRefs(fieldDetailStore);
const type = computed(() => field.value.type);
const isGenerated = computed(() => field.value.schema?.is_generated);
return { t, loading, readonly, hidden, required, note, translations, type, isGenerated };
},
});
const { t } = useI18n();
const fieldDetailStore = useFieldDetailStore();
const readonly = syncFieldDetailStoreProperty('field.meta.readonly', false);
const hidden = syncFieldDetailStoreProperty('field.meta.hidden', false);
const required = syncFieldDetailStoreProperty('field.meta.required', false);
const note = syncFieldDetailStoreProperty('field.meta.note');
const translations = syncFieldDetailStoreProperty('field.meta.translations');
const { loading, field } = storeToRefs(fieldDetailStore);
const type = computed(() => field.value.type);
const isGenerated = computed(() => field.value.schema?.is_generated);
</script>
<style lang="scss" scoped>

View File

@@ -23,107 +23,101 @@
</div>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, computed } from 'vue';
import { useFieldDetailStore, syncFieldDetailStoreProperty } from '../store/';
import { storeToRefs } from 'pinia';
import ExtensionOptions from '../shared/extension-options.vue';
<script setup lang="ts">
import { FancySelectItem } from '@/components/v-fancy-select.vue';
import { useExtension } from '@/composables/use-extension';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import ExtensionOptions from '../shared/extension-options.vue';
import { syncFieldDetailStoreProperty, useFieldDetailStore } from '../store/';
export default defineComponent({
components: { ExtensionOptions },
setup() {
const { t } = useI18n();
const { t } = useI18n();
const fieldDetailStore = useFieldDetailStore();
const fieldDetailStore = useFieldDetailStore();
const interfaceId = syncFieldDetailStoreProperty('field.meta.interface');
const interfaceId = syncFieldDetailStoreProperty('field.meta.interface');
const { loading, field, interfacesForType } = storeToRefs(fieldDetailStore);
const type = computed(() => field.value.type);
const { loading, field, interfacesForType } = storeToRefs(fieldDetailStore);
const type = computed(() => field.value.type);
const selectItems = computed(() => {
const recommendedInterfacesPerType: { [type: string]: string[] } = {
string: ['input', 'select-dropdown'],
text: ['input-rich-text-html'],
boolean: ['boolean'],
integer: ['input'],
bigInteger: ['input'],
float: ['input'],
decimal: ['input'],
timestamp: ['datetime'],
datetime: ['datetime'],
date: ['datetime'],
time: ['datetime'],
json: ['select-multiple-checkbox', 'tags'],
uuid: ['input'],
csv: ['tags'],
const selectItems = computed(() => {
const recommendedInterfacesPerType: { [type: string]: string[] } = {
string: ['input', 'select-dropdown'],
text: ['input-rich-text-html'],
boolean: ['boolean'],
integer: ['input'],
bigInteger: ['input'],
float: ['input'],
decimal: ['input'],
timestamp: ['datetime'],
datetime: ['datetime'],
date: ['datetime'],
time: ['datetime'],
json: ['select-multiple-checkbox', 'tags'],
uuid: ['input'],
csv: ['tags'],
};
const recommended = recommendedInterfacesPerType[type.value ?? 'alias'] || [];
const interfaceItems: FancySelectItem[] = interfacesForType.value.map((inter) => {
const item: FancySelectItem = {
text: inter.name,
description: inter.description,
value: inter.id,
icon: inter.icon,
};
if (recommended.includes(item.value as string)) {
item.iconRight = 'star';
}
return item;
});
const recommendedItems: (FancySelectItem | { divider: boolean } | undefined)[] = [];
const recommendedList = recommended.map((key) => interfaceItems.find((item) => item.value === key));
if (recommendedList !== undefined) {
recommendedItems.push(...recommendedList.filter((i) => i));
}
if (interfaceItems.length >= 5 && recommended.length > 0) {
recommendedItems.push({ divider: true });
}
const interfaceList = interfaceItems.filter((item) => recommended.includes(item.value as string) === false);
if (interfaceList !== undefined) {
recommendedItems.push(...interfaceList.filter((i) => i));
}
return recommendedItems;
});
const selectedInterface = useExtension('interface', interfaceId);
const customOptionsFields = computed(() => {
if (typeof selectedInterface.value?.options === 'function') {
return selectedInterface.value?.options(fieldDetailStore);
}
return null;
});
const options = computed({
get() {
return fieldDetailStore.field.meta?.options ?? {};
},
set(newOptions: Record<string, any>) {
fieldDetailStore.$patch((state) => {
state.field.meta = {
...(state.field.meta ?? {}),
options: newOptions,
};
const recommended = recommendedInterfacesPerType[type.value ?? 'alias'] || [];
const interfaceItems: FancySelectItem[] = interfacesForType.value.map((inter) => {
const item: FancySelectItem = {
text: inter.name,
description: inter.description,
value: inter.id,
icon: inter.icon,
};
if (recommended.includes(item.value as string)) {
item.iconRight = 'star';
}
return item;
});
const recommendedItems: (FancySelectItem | { divider: boolean } | undefined)[] = [];
const recommendedList = recommended.map((key) => interfaceItems.find((item) => item.value === key));
if (recommendedList !== undefined) {
recommendedItems.push(...recommendedList.filter((i) => i));
}
if (interfaceItems.length >= 5 && recommended.length > 0) {
recommendedItems.push({ divider: true });
}
const interfaceList = interfaceItems.filter((item) => recommended.includes(item.value as string) === false);
if (interfaceList !== undefined) {
recommendedItems.push(...interfaceList.filter((i) => i));
}
return recommendedItems;
});
const selectedInterface = useExtension('interface', interfaceId);
const customOptionsFields = computed(() => {
if (typeof selectedInterface.value?.options === 'function') {
return selectedInterface.value?.options(fieldDetailStore);
}
return null;
});
const options = computed({
get() {
return fieldDetailStore.field.meta?.options ?? {};
},
set(newOptions: Record<string, any>) {
fieldDetailStore.$patch((state) => {
state.field.meta = {
...(state.field.meta ?? {}),
options: newOptions,
};
});
},
});
return { t, loading, selectItems, selectedInterface, interfaceId, customOptionsFields, options };
},
});
</script>

View File

@@ -153,91 +153,67 @@
</div>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, computed } from 'vue';
import { useFieldDetailStore, syncFieldDetailStoreProperty } from '../store';
import { storeToRefs } from 'pinia';
import RelatedCollectionSelect from '../shared/related-collection-select.vue';
import RelatedFieldSelect from '../shared/related-field-select.vue';
import { useFieldsStore } from '@/stores/fields';
<script setup lang="ts">
import { useCollectionsStore } from '@/stores/collections';
import { useFieldsStore } from '@/stores/fields';
import { useRelationsStore } from '@/stores/relations';
import { orderBy } from 'lodash';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import RelatedCollectionSelect from '../shared/related-collection-select.vue';
import RelatedFieldSelect from '../shared/related-field-select.vue';
import { syncFieldDetailStoreProperty, useFieldDetailStore } from '../store';
export default defineComponent({
components: { RelatedCollectionSelect, RelatedFieldSelect },
setup() {
const { t } = useI18n();
const { t } = useI18n();
const fieldDetailStore = useFieldDetailStore();
const collectionsStore = useCollectionsStore();
const relationsStore = useRelationsStore();
const fieldsStore = useFieldsStore();
const fieldDetailStore = useFieldDetailStore();
const collectionsStore = useCollectionsStore();
const relationsStore = useRelationsStore();
const fieldsStore = useFieldsStore();
const { collection, editing, generationInfo } = storeToRefs(fieldDetailStore);
const { collection, editing, generationInfo } = storeToRefs(fieldDetailStore);
const autoGenerateJunctionRelation = syncFieldDetailStoreProperty('autoGenerateJunctionRelation');
const junctionCollection = syncFieldDetailStoreProperty('relations.o2m.collection');
const junctionFieldCurrent = syncFieldDetailStoreProperty('relations.o2m.field');
const junctionFieldRelated = syncFieldDetailStoreProperty('relations.m2o.field');
const oneCollectionField = syncFieldDetailStoreProperty('relations.m2o.meta.one_collection_field');
const oneAllowedCollections = syncFieldDetailStoreProperty('relations.m2o.meta.one_allowed_collections', []);
const sortField = syncFieldDetailStoreProperty('relations.o2m.meta.sort_field');
const onDelete = syncFieldDetailStoreProperty('relations.o2m.schema.on_delete');
const onDeselect = syncFieldDetailStoreProperty('relations.o2m.meta.one_deselect_action');
const autoGenerateJunctionRelation = syncFieldDetailStoreProperty('autoGenerateJunctionRelation');
const junctionCollection = syncFieldDetailStoreProperty('relations.o2m.collection');
const junctionFieldCurrent = syncFieldDetailStoreProperty('relations.o2m.field');
const junctionFieldRelated = syncFieldDetailStoreProperty('relations.m2o.field');
const oneCollectionField = syncFieldDetailStoreProperty('relations.m2o.meta.one_collection_field');
const oneAllowedCollections = syncFieldDetailStoreProperty('relations.m2o.meta.one_allowed_collections', []);
const sortField = syncFieldDetailStoreProperty('relations.o2m.meta.sort_field');
const onDelete = syncFieldDetailStoreProperty('relations.o2m.schema.on_delete');
const onDeselect = syncFieldDetailStoreProperty('relations.o2m.meta.one_deselect_action');
const isExisting = computed(() => editing.value !== '+');
const currentPrimaryKey = computed(() => fieldsStore.getPrimaryKeyFieldForCollection(collection.value!)?.field);
const isExisting = computed(() => editing.value !== '+');
const currentPrimaryKey = computed(() => fieldsStore.getPrimaryKeyFieldForCollection(collection.value!)?.field);
const availableCollections = computed(() => {
return orderBy(
[
...collectionsStore.databaseCollections,
{
divider: true,
},
{
name: t('system'),
selectable: false,
children: collectionsStore.crudSafeSystemCollections,
},
],
['collection'],
['asc']
);
});
const availableCollections = computed(() => {
return orderBy(
[
...collectionsStore.databaseCollections,
{
divider: true,
},
{
name: t('system'),
selectable: false,
children: collectionsStore.crudSafeSystemCollections,
},
],
['collection'],
['asc']
);
});
const unsortableJunctionFields = computed(() => {
let fields = ['item', 'collection'];
const unsortableJunctionFields = computed(() => {
let fields = ['item', 'collection'];
if (junctionCollection.value) {
const relations = relationsStore.getRelationsForCollection(junctionCollection.value);
fields.push(...relations.map((field) => field.field));
}
if (junctionCollection.value) {
const relations = relationsStore.getRelationsForCollection(junctionCollection.value);
fields.push(...relations.map((field) => field.field));
}
return fields;
});
return {
t,
availableCollections,
generationInfo,
collection,
isExisting,
autoGenerateJunctionRelation,
junctionCollection,
oneAllowedCollections,
currentPrimaryKey,
junctionFieldCurrent,
junctionFieldRelated,
oneCollectionField,
sortField,
onDelete,
onDeselect,
unsortableJunctionFields,
};
},
return fields;
});
</script>

View File

@@ -198,110 +198,82 @@
</div>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, computed } from 'vue';
import { useFieldDetailStore, syncFieldDetailStoreProperty } from '../store';
import { storeToRefs } from 'pinia';
import RelatedCollectionSelect from '../shared/related-collection-select.vue';
import RelatedFieldSelect from '../shared/related-field-select.vue';
<script setup lang="ts">
import { useFieldsStore } from '@/stores/fields';
import { useRelationsStore } from '@/stores/relations';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import RelatedCollectionSelect from '../shared/related-collection-select.vue';
import RelatedFieldSelect from '../shared/related-field-select.vue';
import { syncFieldDetailStoreProperty, useFieldDetailStore } from '../store';
export default defineComponent({
components: { RelatedCollectionSelect, RelatedFieldSelect },
setup() {
const { t } = useI18n();
const { t } = useI18n();
const fieldDetailStore = useFieldDetailStore();
const relationsStore = useRelationsStore();
const fieldsStore = useFieldsStore();
const fieldDetailStore = useFieldDetailStore();
const relationsStore = useRelationsStore();
const fieldsStore = useFieldsStore();
const { collection, editing, generationInfo, localType } = storeToRefs(fieldDetailStore);
const { collection, editing, generationInfo, localType } = storeToRefs(fieldDetailStore);
const sortField = syncFieldDetailStoreProperty('relations.o2m.meta.sort_field');
const junctionCollection = syncFieldDetailStoreProperty('relations.o2m.collection');
const junctionFieldCurrent = syncFieldDetailStoreProperty('relations.o2m.field');
const junctionFieldRelated = syncFieldDetailStoreProperty('relations.m2o.field');
const relatedCollection = syncFieldDetailStoreProperty('relations.m2o.related_collection');
const autoGenerateJunctionRelation = syncFieldDetailStoreProperty('autoGenerateJunctionRelation');
const onDeleteCurrent = syncFieldDetailStoreProperty('relations.o2m.schema.on_delete');
const onDeleteRelated = syncFieldDetailStoreProperty('relations.m2o.schema.on_delete');
const deselectAction = syncFieldDetailStoreProperty('relations.o2m.meta.one_deselect_action');
const correspondingField = syncFieldDetailStoreProperty('fields.corresponding');
const correspondingFieldKey = syncFieldDetailStoreProperty('fields.corresponding.field');
const sortField = syncFieldDetailStoreProperty('relations.o2m.meta.sort_field');
const junctionCollection = syncFieldDetailStoreProperty('relations.o2m.collection');
const junctionFieldCurrent = syncFieldDetailStoreProperty('relations.o2m.field');
const junctionFieldRelated = syncFieldDetailStoreProperty('relations.m2o.field');
const relatedCollection = syncFieldDetailStoreProperty('relations.m2o.related_collection');
const autoGenerateJunctionRelation = syncFieldDetailStoreProperty('autoGenerateJunctionRelation');
const onDeleteCurrent = syncFieldDetailStoreProperty('relations.o2m.schema.on_delete');
const onDeleteRelated = syncFieldDetailStoreProperty('relations.m2o.schema.on_delete');
const deselectAction = syncFieldDetailStoreProperty('relations.o2m.meta.one_deselect_action');
const correspondingField = syncFieldDetailStoreProperty('fields.corresponding');
const correspondingFieldKey = syncFieldDetailStoreProperty('fields.corresponding.field');
const isExisting = computed(() => editing.value !== '+');
const isExisting = computed(() => editing.value !== '+');
const currentPrimaryKey = computed(() => fieldsStore.getPrimaryKeyFieldForCollection(collection.value!)?.field);
const currentPrimaryKey = computed(() => fieldsStore.getPrimaryKeyFieldForCollection(collection.value!)?.field);
const relatedPrimaryKey = computed(
() => fieldsStore.getPrimaryKeyFieldForCollection(relatedCollection.value)?.field ?? 'id'
);
const relatedPrimaryKey = computed(
() => fieldsStore.getPrimaryKeyFieldForCollection(relatedCollection.value)?.field ?? 'id'
);
const hasCorresponding = computed({
get() {
return !!correspondingField.value;
},
set(enabled: boolean) {
if (enabled) {
correspondingField.value = {
field: collection.value,
collection: relatedCollection.value,
type: 'alias',
meta: {
special: ['m2m'],
interface: 'list-m2m',
},
};
} else {
correspondingField.value = null;
}
},
});
const correspondingLabel = computed(() => {
if (junctionCollection.value) {
return t('add_m2m_to_collection', { collection: relatedCollection.value });
}
return t('add_field_related');
});
const unsortableJunctionFields = computed(() => {
let fields = [];
if (junctionCollection.value) {
const relations = relationsStore.getRelationsForCollection(junctionCollection.value);
fields.push(...relations.map((field) => field.field));
}
return fields;
});
return {
t,
autoGenerateJunctionRelation,
collection,
localType,
isExisting,
junctionCollection,
junctionFieldCurrent,
relatedCollection,
sortField,
currentPrimaryKey,
junctionFieldRelated,
relatedPrimaryKey,
onDeleteCurrent,
onDeleteRelated,
deselectAction,
hasCorresponding,
correspondingLabel,
correspondingFieldKey,
generationInfo,
unsortableJunctionFields,
};
const hasCorresponding = computed({
get() {
return !!correspondingField.value;
},
set(enabled: boolean) {
if (enabled) {
correspondingField.value = {
field: collection.value,
collection: relatedCollection.value,
type: 'alias',
meta: {
special: ['m2m'],
interface: 'list-m2m',
},
};
} else {
correspondingField.value = null;
}
},
});
const correspondingLabel = computed(() => {
if (junctionCollection.value) {
return t('add_m2m_to_collection', { collection: relatedCollection.value });
}
return t('add_field_related');
});
const unsortableJunctionFields = computed(() => {
let fields = [];
if (junctionCollection.value) {
const relations = relationsStore.getRelationsForCollection(junctionCollection.value);
fields.push(...relations.map((field) => field.field));
}
return fields;
});
</script>

View File

@@ -70,106 +70,86 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, computed } from 'vue';
import { computed } from 'vue';
import { useFieldDetailStore, syncFieldDetailStoreProperty } from '../store';
import { storeToRefs } from 'pinia';
import RelatedCollectionSelect from '../shared/related-collection-select.vue';
import { useFieldsStore } from '@/stores/fields';
export default defineComponent({
components: { RelatedCollectionSelect },
setup() {
const { t } = useI18n();
const { t } = useI18n();
const fieldDetailStore = useFieldDetailStore();
const fieldsStore = useFieldsStore();
const fieldDetailStore = useFieldDetailStore();
const fieldsStore = useFieldsStore();
const relatedCollection = syncFieldDetailStoreProperty('relations.m2o.related_collection');
const correspondingField = syncFieldDetailStoreProperty('fields.corresponding');
const correspondingFieldKey = syncFieldDetailStoreProperty('fields.corresponding.field');
const onDeleteRelated = syncFieldDetailStoreProperty('relations.m2o.schema.on_delete');
const relatedCollection = syncFieldDetailStoreProperty('relations.m2o.related_collection');
const correspondingField = syncFieldDetailStoreProperty('fields.corresponding');
const correspondingFieldKey = syncFieldDetailStoreProperty('fields.corresponding.field');
const onDeleteRelated = syncFieldDetailStoreProperty('relations.m2o.schema.on_delete');
const { field, collection, editing, generationInfo } = storeToRefs(fieldDetailStore);
const { field, collection, editing, generationInfo } = storeToRefs(fieldDetailStore);
const isExisting = computed(() => editing.value !== '+');
const isExisting = computed(() => editing.value !== '+');
const relatedPrimaryKey = computed(
() => fieldsStore.getPrimaryKeyFieldForCollection(relatedCollection.value)?.field ?? 'id'
);
const relatedPrimaryKey = computed(
() => fieldsStore.getPrimaryKeyFieldForCollection(relatedCollection.value)?.field ?? 'id'
);
const currentField = computed(() => field.value.field);
const currentField = computed(() => field.value.field);
const hasCorresponding = computed({
get() {
return !!correspondingField.value;
},
set(enabled: boolean) {
if (enabled) {
correspondingField.value = {
field: collection.value,
collection: relatedCollection.value,
type: 'alias',
meta: {
special: ['o2m'],
interface: 'list-o2m',
},
};
} else {
correspondingField.value = null;
}
},
});
const correspondingLabel = computed(() => {
if (relatedCollection.value) {
return t('add_o2m_to_collection', { collection: relatedCollection.value });
}
return t('add_field_related');
});
const onDeleteOptions = computed(() =>
[
{
text: t('referential_action_set_null', { field: currentField.value }),
value: 'SET NULL',
const hasCorresponding = computed({
get() {
return !!correspondingField.value;
},
set(enabled: boolean) {
if (enabled) {
correspondingField.value = {
field: collection.value,
collection: relatedCollection.value,
type: 'alias',
meta: {
special: ['o2m'],
interface: 'list-o2m',
},
{
text: t('referential_action_set_default', { field: currentField.value }),
value: 'SET DEFAULT',
},
{
text: t('referential_action_cascade', {
collection: collection.value,
field: currentField.value,
}),
value: 'CASCADE',
},
{
text: t('referential_action_no_action', { field: currentField.value }),
value: 'NO ACTION',
},
].filter((o) => !(o.value === 'SET NULL' && field.value.schema?.is_nullable === false))
);
return {
t,
collection,
relatedCollection,
isExisting,
relatedPrimaryKey,
currentField,
hasCorresponding,
correspondingLabel,
correspondingFieldKey,
generationInfo,
onDeleteRelated,
onDeleteOptions,
};
};
} else {
correspondingField.value = null;
}
},
});
const correspondingLabel = computed(() => {
if (relatedCollection.value) {
return t('add_o2m_to_collection', { collection: relatedCollection.value });
}
return t('add_field_related');
});
const onDeleteOptions = computed(() =>
[
{
text: t('referential_action_set_null', { field: currentField.value }),
value: 'SET NULL',
},
{
text: t('referential_action_set_default', { field: currentField.value }),
value: 'SET DEFAULT',
},
{
text: t('referential_action_cascade', {
collection: collection.value,
field: currentField.value,
}),
value: 'CASCADE',
},
{
text: t('referential_action_no_action', { field: currentField.value }),
value: 'NO ACTION',
},
].filter((o) => !(o.value === 'SET NULL' && field.value.schema?.is_nullable === false))
);
</script>
<style lang="scss" scoped>

View File

@@ -112,9 +112,9 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, computed } from 'vue';
import { computed } from 'vue';
import { useFieldDetailStore, syncFieldDetailStoreProperty } from '../store';
import { storeToRefs } from 'pinia';
import RelatedCollectionSelect from '../shared/related-collection-select.vue';
@@ -122,51 +122,32 @@ import RelatedFieldSelect from '../shared/related-field-select.vue';
import { useFieldsStore } from '@/stores/fields';
import { useRelationsStore } from '@/stores/relations';
export default defineComponent({
components: { RelatedCollectionSelect, RelatedFieldSelect },
setup() {
const { t } = useI18n();
const { t } = useI18n();
const fieldDetailStore = useFieldDetailStore();
const relationsStore = useRelationsStore();
const fieldsStore = useFieldsStore();
const fieldDetailStore = useFieldDetailStore();
const relationsStore = useRelationsStore();
const fieldsStore = useFieldsStore();
const relatedCollection = syncFieldDetailStoreProperty('relations.o2m.collection');
const relatedField = syncFieldDetailStoreProperty('relations.o2m.field');
const sortField = syncFieldDetailStoreProperty('relations.o2m.meta.sort_field');
const onDelete = syncFieldDetailStoreProperty('relations.o2m.schema.on_delete');
const onDeselect = syncFieldDetailStoreProperty('relations.o2m.meta.one_deselect_action');
const relatedCollection = syncFieldDetailStoreProperty('relations.o2m.collection');
const relatedField = syncFieldDetailStoreProperty('relations.o2m.field');
const sortField = syncFieldDetailStoreProperty('relations.o2m.meta.sort_field');
const onDelete = syncFieldDetailStoreProperty('relations.o2m.schema.on_delete');
const onDeselect = syncFieldDetailStoreProperty('relations.o2m.meta.one_deselect_action');
const { collection, editing, generationInfo } = storeToRefs(fieldDetailStore);
const { collection, editing, generationInfo } = storeToRefs(fieldDetailStore);
const isExisting = computed(() => editing.value !== '+');
const currentPrimaryKey = computed(() => fieldsStore.getPrimaryKeyFieldForCollection(collection.value!)?.field);
const isExisting = computed(() => editing.value !== '+');
const currentPrimaryKey = computed(() => fieldsStore.getPrimaryKeyFieldForCollection(collection.value!)?.field);
const unsortableJunctionFields = computed(() => {
let fields = [];
const unsortableJunctionFields = computed(() => {
let fields = [];
if (relatedCollection.value) {
const relations = relationsStore.getRelationsForCollection(relatedCollection.value);
fields.push(...relations.map((field) => field.field));
}
if (relatedCollection.value) {
const relations = relationsStore.getRelationsForCollection(relatedCollection.value);
fields.push(...relations.map((field) => field.field));
}
return fields;
});
return {
t,
isExisting,
collection,
relatedCollection,
currentPrimaryKey,
relatedField,
generationInfo,
sortField,
onDelete,
onDeselect,
unsortableJunctionFields,
};
},
return fields;
});
</script>

View File

@@ -15,7 +15,7 @@
</div>
<div class="field">
<div class="type-label">{{ t('languages_collection') }}</div>
<related-collection-select v-model="relatedCollection" :disabled="type === 'files' || isExisting" />
<related-collection-select v-model="relatedCollection" :disabled="isExisting" />
</div>
<v-input disabled :model-value="currentPrimaryKey" />
<related-field-select
@@ -145,110 +145,38 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, computed } from 'vue';
import { computed } from 'vue';
import { useFieldDetailStore, syncFieldDetailStoreProperty } from '../store';
import { storeToRefs } from 'pinia';
import RelatedCollectionSelect from '../shared/related-collection-select.vue';
import RelatedFieldSelect from '../shared/related-field-select.vue';
import { useFieldsStore } from '@/stores/fields';
export default defineComponent({
components: { RelatedCollectionSelect, RelatedFieldSelect },
setup() {
const { t } = useI18n();
const { t } = useI18n();
const fieldDetailStore = useFieldDetailStore();
const fieldsStore = useFieldsStore();
const fieldDetailStore = useFieldDetailStore();
const fieldsStore = useFieldsStore();
const { field, collection, editing, generationInfo } = storeToRefs(fieldDetailStore);
const { collection, editing } = storeToRefs(fieldDetailStore);
const sortField = syncFieldDetailStoreProperty('relations.o2m.meta.sort_field');
const junctionCollection = syncFieldDetailStoreProperty('relations.o2m.collection');
const junctionFieldCurrent = syncFieldDetailStoreProperty('relations.o2m.field');
const junctionFieldRelated = syncFieldDetailStoreProperty('relations.m2o.field');
const relatedCollection = syncFieldDetailStoreProperty('relations.m2o.related_collection');
const autoGenerateJunctionRelation = syncFieldDetailStoreProperty('autoGenerateJunctionRelation');
const onDeleteCurrent = syncFieldDetailStoreProperty('relations.o2m.schema.on_delete');
const onDeleteRelated = syncFieldDetailStoreProperty('relations.m2o.schema.on_delete');
const deselectAction = syncFieldDetailStoreProperty('relations.o2m.meta.one_deselect_action');
const correspondingField = syncFieldDetailStoreProperty('fields.corresponding');
const junctionCollection = syncFieldDetailStoreProperty('relations.o2m.collection');
const junctionFieldCurrent = syncFieldDetailStoreProperty('relations.o2m.field');
const junctionFieldRelated = syncFieldDetailStoreProperty('relations.m2o.field');
const relatedCollection = syncFieldDetailStoreProperty('relations.m2o.related_collection');
const autoGenerateJunctionRelation = syncFieldDetailStoreProperty('autoGenerateJunctionRelation');
const onDeleteCurrent = syncFieldDetailStoreProperty('relations.o2m.schema.on_delete');
const onDeleteRelated = syncFieldDetailStoreProperty('relations.m2o.schema.on_delete');
const deselectAction = syncFieldDetailStoreProperty('relations.o2m.meta.one_deselect_action');
const type = computed(() => field.value.type);
const isExisting = computed(() => editing.value !== '+');
const isExisting = computed(() => editing.value !== '+');
const currentPrimaryKey = computed(() => fieldsStore.getPrimaryKeyFieldForCollection(collection.value!)?.field);
const currentPrimaryKey = computed(() => fieldsStore.getPrimaryKeyFieldForCollection(collection.value!)?.field);
const relatedPrimaryKey = computed(
() => fieldsStore.getPrimaryKeyFieldForCollection(relatedCollection.value)?.field ?? 'id'
);
const hasCorresponding = computed({
get() {
return !!correspondingField.value;
},
set(enabled: boolean) {
if (enabled) {
correspondingField.value = {
field: collection.value,
collection: relatedCollection.value,
type: 'alias',
meta: {
special: ['m2m'],
interface: 'list-m2m',
},
};
} else {
correspondingField.value = null;
}
},
});
const correspondingLabel = computed(() => {
if (junctionCollection.value) {
return t('add_m2m_to_collection', { collection: relatedCollection.value });
}
return t('add_field_related');
});
const correspondingFieldKey = computed({
get() {
return correspondingField.value?.field;
},
set(key: string | undefined) {
if (!hasCorresponding.value) {
hasCorresponding.value = true;
}
correspondingField.value!.field = key;
},
});
return {
t,
autoGenerateJunctionRelation,
collection,
type,
isExisting,
junctionCollection,
junctionFieldCurrent,
relatedCollection,
sortField,
currentPrimaryKey,
junctionFieldRelated,
relatedPrimaryKey,
onDeleteCurrent,
onDeleteRelated,
deselectAction,
hasCorresponding,
correspondingLabel,
correspondingFieldKey,
generationInfo,
};
},
});
const relatedPrimaryKey = computed(
() => fieldsStore.getPrimaryKeyFieldForCollection(relatedCollection.value)?.field ?? 'id'
);
</script>
<style lang="scss" scoped>

View File

@@ -6,32 +6,16 @@
<relationship-translations v-else-if="localType === 'translations'" />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import RelationshipM2o from './field-detail-advanced-relationship-m2o.vue';
import RelationshipO2m from './field-detail-advanced-relationship-o2m.vue';
import RelationshipM2m from './field-detail-advanced-relationship-m2m.vue';
import RelationshipM2a from './field-detail-advanced-relationship-m2a.vue';
import RelationshipTranslations from './field-detail-advanced-relationship-translations.vue';
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useFieldDetailStore } from '../store';
import RelationshipM2a from './field-detail-advanced-relationship-m2a.vue';
import RelationshipM2m from './field-detail-advanced-relationship-m2m.vue';
import RelationshipM2o from './field-detail-advanced-relationship-m2o.vue';
import RelationshipO2m from './field-detail-advanced-relationship-o2m.vue';
import RelationshipTranslations from './field-detail-advanced-relationship-translations.vue';
export default defineComponent({
components: {
RelationshipM2o,
RelationshipO2m,
RelationshipM2m,
RelationshipM2a,
RelationshipTranslations,
},
setup() {
const fieldDetailStore = useFieldDetailStore();
const fieldDetailStore = useFieldDetailStore();
const { collection, localType } = storeToRefs(fieldDetailStore);
return { collection, localType };
},
});
const { localType } = storeToRefs(fieldDetailStore);
</script>

View File

@@ -132,18 +132,17 @@
</div>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, computed } from 'vue';
import { GEOMETRY_TYPES } from '@directus/constants';
<script setup lang="ts">
import { translate } from '@/utils/translate-object-values';
import { Type } from '@directus/types';
import { TranslateResult } from 'vue-i18n';
import { useFieldDetailStore, syncFieldDetailStoreProperty } from '../store';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { TranslateResult, useI18n } from 'vue-i18n';
import { syncFieldDetailStoreProperty, useFieldDetailStore } from '../store';
type FieldTypeOption = { value: Type; text: TranslateResult | string; children?: FieldTypeOption[] };
export const fieldTypes: Array<FieldTypeOption | { divider: true }> = [
const fieldTypes: Array<FieldTypeOption | { divider: true }> = [
{
text: '$t:string',
value: 'string',
@@ -239,250 +238,216 @@ export const fieldTypes: Array<FieldTypeOption | { divider: true }> = [
},
];
export default defineComponent({
setup() {
const fieldDetailStore = useFieldDetailStore();
const fieldDetailStore = useFieldDetailStore();
const { localType, relations, editing } = storeToRefs(fieldDetailStore);
const { localType, relations, editing } = storeToRefs(fieldDetailStore);
const isExisting = computed(() => editing.value !== '+');
const isExisting = computed(() => editing.value !== '+');
const type = syncFieldDetailStoreProperty('field.type');
const defaultValue = syncFieldDetailStoreProperty('field.schema.default_value');
const field = syncFieldDetailStoreProperty('field.field');
const special = syncFieldDetailStoreProperty('field.meta.special');
const maxLength = syncFieldDetailStoreProperty('field.schema.max_length');
const numericPrecision = syncFieldDetailStoreProperty('field.schema.numeric_precision');
const nullable = syncFieldDetailStoreProperty('field.schema.is_nullable', true);
const unique = syncFieldDetailStoreProperty('field.schema.is_unique', false);
const numericScale = syncFieldDetailStoreProperty('field.schema.numeric_scale');
const type = syncFieldDetailStoreProperty('field.type');
const defaultValue = syncFieldDetailStoreProperty('field.schema.default_value');
const field = syncFieldDetailStoreProperty('field.field');
const special = syncFieldDetailStoreProperty('field.meta.special');
const maxLength = syncFieldDetailStoreProperty('field.schema.max_length');
const numericPrecision = syncFieldDetailStoreProperty('field.schema.numeric_precision');
const nullable = syncFieldDetailStoreProperty('field.schema.is_nullable', true);
const unique = syncFieldDetailStoreProperty('field.schema.is_unique', false);
const numericScale = syncFieldDetailStoreProperty('field.schema.numeric_scale');
const { t } = useI18n();
const { t } = useI18n();
const typesWithLabels = computed(() => translate(fieldTypes));
const typesWithLabels = computed(() => translate(fieldTypes));
const typeDisabled = computed(() => localType.value !== 'standard');
const typeDisabled = computed(() => localType.value !== 'standard');
const typePlaceholder = computed(() => {
if (localType.value === 'm2o') {
return t('determined_by_relationship');
const typePlaceholder = computed(() => {
if (localType.value === 'm2o') {
return t('determined_by_relationship');
}
return t('choose_a_type');
});
const { onCreateOptions, onCreateValue } = useOnCreate();
const { onUpdateOptions, onUpdateValue } = useOnUpdate();
const hasCreateUpdateTriggers = computed(() => {
return ['uuid', 'date', 'time', 'dateTime', 'timestamp'].includes(type.value) && localType.value !== 'file';
});
const isAlias = computed(() => {
return !fieldDetailStore.field.schema;
});
const isPrimaryKey = computed(() => {
return fieldDetailStore.field.schema?.is_primary_key === true;
});
const isGenerated = computed(() => {
return fieldDetailStore.field.schema?.is_generated;
});
function useOnCreate() {
const onCreateSpecials = ['uuid', 'user-created', 'role-created', 'date-created'];
const onCreateOptions = computed(() => {
if (type.value === 'uuid') {
const options = [
{
text: t('do_nothing'),
value: null,
},
{
text: t('generate_and_save_uuid'),
value: 'uuid',
},
{
text: t('save_current_user_id'),
value: 'user-created',
},
{
text: t('save_current_user_role'),
value: 'role-created',
},
];
if (localType.value === 'm2o' && relations.value.m2o?.related_collection === 'directus_users') {
return options.filter(({ value }) => [null, 'user-created'].includes(value));
}
return t('choose_a_type');
});
if (localType.value === 'm2o' && relations.value.m2o?.related_collection === 'directus_roles') {
return options.filter(({ value }) => [null, 'role-created'].includes(value));
}
const { onCreateOptions, onCreateValue } = useOnCreate();
const { onUpdateOptions, onUpdateValue } = useOnUpdate();
const hasCreateUpdateTriggers = computed(() => {
return ['uuid', 'date', 'time', 'dateTime', 'timestamp'].includes(type.value) && localType.value !== 'file';
});
const isAlias = computed(() => {
return !fieldDetailStore.field.schema;
});
const isPrimaryKey = computed(() => {
return fieldDetailStore.field.schema?.is_primary_key === true;
});
const isGenerated = computed(() => {
return fieldDetailStore.field.schema?.is_generated;
});
return {
t,
typesWithLabels,
GEOMETRY_TYPES,
typeDisabled,
typePlaceholder,
defaultValue,
onCreateOptions,
onCreateValue,
onUpdateOptions,
onUpdateValue,
hasCreateUpdateTriggers,
field,
isAlias,
type,
maxLength,
numericPrecision,
numericScale,
special,
nullable,
unique,
isPrimaryKey,
isExisting,
isGenerated,
};
function useOnCreate() {
const onCreateSpecials = ['uuid', 'user-created', 'role-created', 'date-created'];
const onCreateOptions = computed(() => {
if (type.value === 'uuid') {
const options = [
{
text: t('do_nothing'),
value: null,
},
{
text: t('generate_and_save_uuid'),
value: 'uuid',
},
{
text: t('save_current_user_id'),
value: 'user-created',
},
{
text: t('save_current_user_role'),
value: 'role-created',
},
];
if (localType.value === 'm2o' && relations.value.m2o?.related_collection === 'directus_users') {
return options.filter(({ value }) => [null, 'user-created'].includes(value));
}
if (localType.value === 'm2o' && relations.value.m2o?.related_collection === 'directus_roles') {
return options.filter(({ value }) => [null, 'role-created'].includes(value));
}
return options;
} else if (['date', 'time', 'dateTime', 'timestamp'].includes(type.value!)) {
return [
{
text: t('do_nothing'),
value: null,
},
{
text: t('save_current_datetime'),
value: 'date-created',
},
];
}
return [];
});
const onCreateValue = computed({
get() {
const specials = special.value ?? [];
for (const special of onCreateSpecials) {
if (specials.includes(special)) {
return special;
}
}
return null;
return options;
} else if (['date', 'time', 'dateTime', 'timestamp'].includes(type.value!)) {
return [
{
text: t('do_nothing'),
value: null,
},
set(newOption: string | null) {
// In case of previously persisted empty string
if (typeof special.value === 'string') {
special.value = [];
}
special.value = (special.value ?? []).filter(
(special: string) => onCreateSpecials.includes(special) === false
);
if (newOption) {
special.value = [...(special.value ?? []), newOption];
}
// Prevent empty array saved as empty string
if (special.value && special.value.length === 0) {
special.value = null;
}
{
text: t('save_current_datetime'),
value: 'date-created',
},
});
return { onCreateSpecials, onCreateOptions, onCreateValue };
];
}
function useOnUpdate() {
const onUpdateSpecials = ['user-updated', 'role-updated', 'date-updated'];
return [];
});
const onUpdateOptions = computed(() => {
if (type.value === 'uuid') {
const options = [
{
text: t('do_nothing'),
value: null,
},
{
text: t('save_current_user_id'),
value: 'user-updated',
},
{
text: t('save_current_user_role'),
value: 'role-updated',
},
];
const onCreateValue = computed({
get() {
const specials = special.value ?? [];
if (localType.value === 'm2o' && relations.value.m2o?.related_collection === 'directus_users') {
return options.filter(({ value }) => [null, 'user-updated'].includes(value));
}
if (localType.value === 'm2o' && relations.value.m2o?.related_collection === 'directus_roles') {
return options.filter(({ value }) => [null, 'role-updated'].includes(value));
}
return options;
} else if (['date', 'time', 'dateTime', 'timestamp'].includes(type.value!)) {
return [
{
text: t('do_nothing'),
value: null,
},
{
text: t('save_current_datetime'),
value: 'date-updated',
},
];
for (const special of onCreateSpecials) {
if (specials.includes(special)) {
return special;
}
}
return [];
});
return null;
},
set(newOption: string | null) {
// In case of previously persisted empty string
if (typeof special.value === 'string') {
special.value = [];
}
const onUpdateValue = computed({
get() {
const specials = special.value ?? [];
special.value = (special.value ?? []).filter((special: string) => onCreateSpecials.includes(special) === false);
for (const special of onUpdateSpecials) {
if (specials.includes(special)) {
return special;
}
}
if (newOption) {
special.value = [...(special.value ?? []), newOption];
}
return null;
// Prevent empty array saved as empty string
if (special.value && special.value.length === 0) {
special.value = null;
}
},
});
return { onCreateSpecials, onCreateOptions, onCreateValue };
}
function useOnUpdate() {
const onUpdateSpecials = ['user-updated', 'role-updated', 'date-updated'];
const onUpdateOptions = computed(() => {
if (type.value === 'uuid') {
const options = [
{
text: t('do_nothing'),
value: null,
},
set(newOption: string | null) {
// In case of previously persisted empty string
if (typeof special.value === 'string') {
special.value = [];
}
special.value = (special.value ?? []).filter(
(special: string) => onUpdateSpecials.includes(special) === false
);
if (newOption) {
special.value = [...(special.value ?? []), newOption];
}
// Prevent empty array saved as empty string
if (special.value && special.value.length === 0) {
special.value = null;
}
{
text: t('save_current_user_id'),
value: 'user-updated',
},
});
{
text: t('save_current_user_role'),
value: 'role-updated',
},
];
return { onUpdateSpecials, onUpdateOptions, onUpdateValue };
if (localType.value === 'm2o' && relations.value.m2o?.related_collection === 'directus_users') {
return options.filter(({ value }) => [null, 'user-updated'].includes(value));
}
if (localType.value === 'm2o' && relations.value.m2o?.related_collection === 'directus_roles') {
return options.filter(({ value }) => [null, 'role-updated'].includes(value));
}
return options;
} else if (['date', 'time', 'dateTime', 'timestamp'].includes(type.value!)) {
return [
{
text: t('do_nothing'),
value: null,
},
{
text: t('save_current_datetime'),
value: 'date-updated',
},
];
}
},
});
return [];
});
const onUpdateValue = computed({
get() {
const specials = special.value ?? [];
for (const special of onUpdateSpecials) {
if (specials.includes(special)) {
return special;
}
}
return null;
},
set(newOption: string | null) {
// In case of previously persisted empty string
if (typeof special.value === 'string') {
special.value = [];
}
special.value = (special.value ?? []).filter((special: string) => onUpdateSpecials.includes(special) === false);
if (newOption) {
special.value = [...(special.value ?? []), newOption];
}
// Prevent empty array saved as empty string
if (special.value && special.value.length === 0) {
special.value = null;
}
},
});
return { onUpdateSpecials, onUpdateOptions, onUpdateValue };
}
</script>
<style lang="scss" scoped>

View File

@@ -6,87 +6,80 @@
</v-tabs>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from 'vue';
import { useI18n } from 'vue-i18n';
<script setup lang="ts">
import { useSync } from '@directus/composables';
import { useFieldDetailStore } from '../store';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useFieldDetailStore } from '../store';
export default defineComponent({
props: {
currentTab: {
type: Array as PropType<string[]>,
required: true,
const props = defineProps<{
currentTab: string[];
}>();
const emit = defineEmits(['update:currentTab']);
const fieldDetail = useFieldDetailStore();
const { localType } = storeToRefs(fieldDetail);
const currentTabSync = useSync(props, 'currentTab', emit);
const { t } = useI18n();
const tabs = computed(() => {
const tabs = [
{
text: t('schema'),
value: 'schema',
},
},
emits: ['update:currentTab'],
setup(props, { emit }) {
const fieldDetail = useFieldDetailStore();
{
text: t('field', 1),
value: 'field',
},
{
text: t('interface_label'),
value: 'interface',
},
];
const { localType } = storeToRefs(fieldDetail);
const currentTabSync = useSync(props, 'currentTab', emit);
const { t } = useI18n();
const tabs = computed(() => {
const tabs = [
{
text: t('schema'),
value: 'schema',
},
{
text: t('field', 1),
value: 'field',
},
{
text: t('interface_label'),
value: 'interface',
},
];
if (localType.value !== 'presentation' && localType.value !== 'group') {
tabs.push({
text: t('display'),
value: 'display',
});
}
if (['o2m', 'm2o', 'm2m', 'm2a', 'files', 'file'].includes(localType.value)) {
tabs.splice(1, 0, {
text: t('relationship'),
value: 'relationship',
});
}
if (localType.value === 'translations') {
tabs.splice(
1,
0,
...[
{
text: t('translations'),
value: 'relationship',
},
]
);
}
tabs.push({
text: t('validation'),
value: 'validation',
});
tabs.push({
text: t('conditions'),
value: 'conditions',
});
return tabs;
if (localType.value !== 'presentation' && localType.value !== 'group') {
tabs.push({
text: t('display'),
value: 'display',
});
}
return { tabs, currentTabSync };
},
if (['o2m', 'm2o', 'm2m', 'm2a', 'files', 'file'].includes(localType.value)) {
tabs.splice(1, 0, {
text: t('relationship'),
value: 'relationship',
});
}
if (localType.value === 'translations') {
tabs.splice(
1,
0,
...[
{
text: t('translations'),
value: 'relationship',
},
]
);
}
tabs.push({
text: t('validation'),
value: 'validation',
});
tabs.push({
text: t('conditions'),
value: 'conditions',
});
return tabs;
});
</script>

View File

@@ -19,11 +19,9 @@ import FieldDetailAdvancedDisplay from './field-detail-advanced-display.vue';
import FieldDetailAdvancedValidation from './field-detail-advanced-validation.vue';
import FieldDetailAdvancedConditions from './field-detail-advanced-conditions.vue';
interface Props {
defineProps<{
currentTab: string;
}
defineProps<Props>();
}>();
</script>
<style scoped>

View File

@@ -61,113 +61,86 @@
</div>
</template>
<script lang="ts">
import { defineComponent, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useFieldDetailStore, syncFieldDetailStoreProperty } from '../store/';
import { storeToRefs } from 'pinia';
import ExtensionOptions from '../shared/extension-options.vue';
import RelationshipConfiguration from './relationship-configuration.vue';
import { useExtensions } from '@/extensions';
<script setup lang="ts">
import { useExtension } from '@/composables/use-extension';
import { useExtensions } from '@/extensions';
import { nanoid } from 'nanoid/non-secure';
import { storeToRefs } from 'pinia';
import { computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import ExtensionOptions from '../shared/extension-options.vue';
import { syncFieldDetailStoreProperty, useFieldDetailStore } from '../store/';
import RelationshipConfiguration from './relationship-configuration.vue';
export default defineComponent({
components: { ExtensionOptions, RelationshipConfiguration },
props: {
row: {
type: Number,
default: null,
},
defineProps<{
row?: number;
}>();
defineEmits(['save', 'toggleAdvanced']);
const fieldDetailStore = useFieldDetailStore();
const { readyToSave, saving, localType } = storeToRefs(fieldDetailStore);
const { t } = useI18n();
const key = syncFieldDetailStoreProperty('field.field');
const type = syncFieldDetailStoreProperty('field.type');
const defaultValue = syncFieldDetailStoreProperty('field.schema.default_value');
const chosenInterface = syncFieldDetailStoreProperty('field.meta.interface');
const required = syncFieldDetailStoreProperty('field.meta.required', false);
const chosenInterfaceConfig = useExtension('interface', chosenInterface);
const typeOptions = computed(() => {
if (!chosenInterfaceConfig.value) return [];
return chosenInterfaceConfig.value.types.map((type) => ({
text: t(type),
value: type,
}));
});
const typeDisabled = computed(() => typeOptions.value.length === 1 || localType.value !== 'standard');
const { interfaces } = useExtensions();
const interfaceIdsToInterface = computed(() => Object.fromEntries(interfaces.value.map((inter) => [inter.id, inter])));
const customOptionsFields = computed(() => {
if (typeof chosenInterfaceConfig.value?.options === 'function') {
return chosenInterfaceConfig.value?.options(fieldDetailStore);
}
return null;
});
watch(
chosenInterface,
(newVal, oldVal) => {
if (!newVal) return;
if (interfaceIdsToInterface.value[newVal].autoKey) {
const simplifiedId = newVal.includes('-') ? newVal.split('-')[1] : newVal;
key.value = `${simplifiedId}-${nanoid(6).toLowerCase()}`;
} else if (oldVal && interfaceIdsToInterface.value[oldVal].autoKey) {
key.value = null;
}
},
emits: ['save', 'toggleAdvanced'],
setup() {
const fieldDetailStore = useFieldDetailStore();
{ immediate: true }
);
const { readyToSave, saving, localType } = storeToRefs(fieldDetailStore);
const { t } = useI18n();
const key = syncFieldDetailStoreProperty('field.field');
const type = syncFieldDetailStoreProperty('field.type');
const defaultValue = syncFieldDetailStoreProperty('field.schema.default_value');
const chosenInterface = syncFieldDetailStoreProperty('field.meta.interface');
const required = syncFieldDetailStoreProperty('field.meta.required', false);
const note = syncFieldDetailStoreProperty('field.meta.note');
const chosenInterfaceConfig = useExtension('interface', chosenInterface);
const typeOptions = computed(() => {
if (!chosenInterfaceConfig.value) return [];
return chosenInterfaceConfig.value.types.map((type) => ({
text: t(type),
value: type,
}));
const options = computed({
get() {
return fieldDetailStore.field.meta?.options ?? {};
},
set(newOptions: Record<string, any>) {
fieldDetailStore.$patch((state) => {
state.field.meta = {
...(state.field.meta ?? {}),
options: newOptions,
};
});
const typeDisabled = computed(() => typeOptions.value.length === 1 || localType.value !== 'standard');
const { interfaces } = useExtensions();
const interfaceIdsToInterface = computed(() =>
Object.fromEntries(interfaces.value.map((inter) => [inter.id, inter]))
);
const customOptionsFields = computed(() => {
if (typeof chosenInterfaceConfig.value?.options === 'function') {
return chosenInterfaceConfig.value?.options(fieldDetailStore);
}
return null;
});
watch(
chosenInterface,
(newVal, oldVal) => {
if (!newVal) return;
if (interfaceIdsToInterface.value[newVal].autoKey) {
const simplifiedId = newVal.includes('-') ? newVal.split('-')[1] : newVal;
key.value = `${simplifiedId}-${nanoid(6).toLowerCase()}`;
} else if (oldVal && interfaceIdsToInterface.value[oldVal].autoKey) {
key.value = null;
}
},
{ immediate: true }
);
const options = computed({
get() {
return fieldDetailStore.field.meta?.options ?? {};
},
set(newOptions: Record<string, any>) {
fieldDetailStore.$patch((state) => {
state.field.meta = {
...(state.field.meta ?? {}),
options: newOptions,
};
});
},
});
return {
key,
t,
type,
typeDisabled,
typeOptions,
defaultValue,
chosenInterface,
chosenInterfaceConfig,
required,
note,
readyToSave,
saving,
localType,
customOptionsFields,
options,
};
},
});
</script>

View File

@@ -67,53 +67,44 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from 'vue';
<script setup lang="ts">
import { useCollectionsStore } from '@/stores/collections';
import { LOCAL_TYPES } from '@directus/constants';
import { syncFieldDetailStoreProperty } from '../store';
import { orderBy } from 'lodash';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import RelatedCollectionSelect from '../shared/related-collection-select.vue';
import RelatedFieldSelect from '../shared/related-field-select.vue';
import { orderBy } from 'lodash';
import { useCollectionsStore } from '@/stores/collections';
import { syncFieldDetailStoreProperty } from '../store';
export default defineComponent({
components: { RelatedCollectionSelect, RelatedFieldSelect },
props: {
localType: {
type: String as PropType<(typeof LOCAL_TYPES)[number]>,
required: true,
},
},
setup() {
const collectionsStore = useCollectionsStore();
defineProps<{
localType: (typeof LOCAL_TYPES)[number];
}>();
const { t } = useI18n();
const relatedCollectionM2O = syncFieldDetailStoreProperty('relations.m2o.related_collection');
const o2mCollection = syncFieldDetailStoreProperty('relations.o2m.collection');
const o2mField = syncFieldDetailStoreProperty('relations.o2m.field');
const oneAllowedCollections = syncFieldDetailStoreProperty('relations.m2o.meta.one_allowed_collections', []);
const collectionsStore = useCollectionsStore();
const availableCollections = computed(() => {
return orderBy(
[
...collectionsStore.databaseCollections,
{
divider: true,
},
{
name: t('system'),
selectable: false,
children: collectionsStore.crudSafeSystemCollections,
},
],
['collection'],
['asc']
);
});
const { t } = useI18n();
const relatedCollectionM2O = syncFieldDetailStoreProperty('relations.m2o.related_collection');
const o2mCollection = syncFieldDetailStoreProperty('relations.o2m.collection');
const o2mField = syncFieldDetailStoreProperty('relations.o2m.field');
const oneAllowedCollections = syncFieldDetailStoreProperty('relations.m2o.meta.one_allowed_collections', []);
return { availableCollections, oneAllowedCollections, relatedCollectionM2O, o2mCollection, o2mField, t };
},
const availableCollections = computed(() => {
return orderBy(
[
...collectionsStore.databaseCollections,
{
divider: true,
},
{
name: t('system'),
selectable: false,
children: collectionsStore.crudSafeSystemCollections,
},
],
['collection'],
['asc']
);
});
</script>

View File

@@ -28,103 +28,78 @@
</v-error-boundary>
</template>
<script lang="ts">
import { defineComponent, PropType, computed, toRefs } from 'vue';
<script setup lang="ts">
import { useExtension } from '@/composables/use-extension';
import { storeToRefs } from 'pinia';
import { computed, toRefs } from 'vue';
import { useI18n } from 'vue-i18n';
import { useFieldDetailStore } from '../store';
import { storeToRefs } from 'pinia';
import { useExtension } from '@/composables/use-extension';
export default defineComponent({
props: {
type: {
type: String as PropType<'interface' | 'display' | 'panel' | 'operation'>,
required: true,
},
extension: {
type: String,
default: null,
},
showAdvanced: {
type: Boolean,
default: false,
},
options: {
type: Object,
default: null,
},
modelValue: {
type: Object,
default: () => ({}),
},
disabled: {
type: Boolean,
default: false,
},
rawEditorEnabled: {
type: Boolean,
default: false,
},
const props = withDefaults(
defineProps<{
type: 'interface' | 'display' | 'panel' | 'operation';
extension?: string;
showAdvanced?: boolean;
options?: Record<string, any>;
modelValue?: Record<string, any>;
disabled?: boolean;
rawEditorEnabled?: boolean;
}>(),
{
showAdvanced: false,
modelValue: () => ({}),
disabled: false,
rawEditorEnabled: false,
}
);
const emit = defineEmits(['update:modelValue']);
const { t } = useI18n();
const fieldDetailStore = useFieldDetailStore();
const { collection, field } = storeToRefs(fieldDetailStore);
const { extension, type } = toRefs(props);
const extensionInfo = useExtension(type, extension);
const usesCustomComponent = computed(() => {
if (!extensionInfo.value) return false;
return extensionInfo.value.options && 'render' in extensionInfo.value.options;
});
const optionsFields = computed(() => {
if (usesCustomComponent.value === true) return [];
let optionsObjectOrArray;
if (props.options) {
optionsObjectOrArray = props.options;
} else {
if (!extensionInfo.value) return [];
if (!extensionInfo.value?.options) return [];
optionsObjectOrArray = extensionInfo.value.options;
}
if (!optionsObjectOrArray) return [];
if (Array.isArray(optionsObjectOrArray)) return optionsObjectOrArray;
if (props.showAdvanced) {
return [...optionsObjectOrArray.standard, ...optionsObjectOrArray.advanced];
}
return optionsObjectOrArray.standard;
});
const optionsValues = computed({
get() {
return props.modelValue;
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const { t } = useI18n();
const fieldDetailStore = useFieldDetailStore();
const { collection, field } = storeToRefs(fieldDetailStore);
const { extension, type } = toRefs(props);
const extensionInfo = useExtension(type, extension);
const usesCustomComponent = computed(() => {
if (!extensionInfo.value) return false;
return extensionInfo.value.options && 'render' in extensionInfo.value.options;
});
const optionsFields = computed(() => {
if (usesCustomComponent.value === true) return [];
let optionsObjectOrArray;
if (props.options) {
optionsObjectOrArray = props.options;
} else {
if (!extensionInfo.value) return [];
if (!extensionInfo.value?.options) return [];
optionsObjectOrArray = extensionInfo.value.options;
}
if (!optionsObjectOrArray) return [];
if (Array.isArray(optionsObjectOrArray)) return optionsObjectOrArray;
if (props.showAdvanced) {
return [...optionsObjectOrArray.standard, ...optionsObjectOrArray.advanced];
}
return optionsObjectOrArray.standard;
});
const optionsValues = computed({
get() {
return props.modelValue;
},
set(values: Record<string, any>) {
emit('update:modelValue', values);
},
});
return {
usesCustomComponent,
extensionInfo,
optionsValues,
optionsFields,
t,
collection,
field,
};
set(values: Record<string, any>) {
emit('update:modelValue', values);
},
});
</script>

View File

@@ -55,39 +55,34 @@
</v-input>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { useI18n } from 'vue-i18n';
<script setup lang="ts">
import { useCollectionsStore } from '@/stores/collections';
import { orderBy } from 'lodash';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
export default defineComponent({
props: {
modelValue: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue'],
setup(props) {
const { t } = useI18n();
const collectionsStore = useCollectionsStore();
const props = withDefaults(
defineProps<{
modelValue?: string;
disabled?: boolean;
}>(),
{
disabled: false,
}
);
const collectionExists = computed(() => {
return !!collectionsStore.getCollection(props.modelValue);
});
defineEmits(['update:modelValue']);
const availableCollections = computed(() => {
return orderBy(collectionsStore.databaseCollections, ['sort', 'collection'], ['asc']);
});
const { t } = useI18n();
const collectionsStore = useCollectionsStore();
const systemCollections = collectionsStore.crudSafeSystemCollections;
return { t, collectionExists, availableCollections, systemCollections };
},
const collectionExists = computed(() => {
return !!collectionsStore.getCollection(props.modelValue);
});
const availableCollections = computed(() => {
return orderBy(collectionsStore.databaseCollections, ['sort', 'collection'], ['asc']);
});
const systemCollections = collectionsStore.crudSafeSystemCollections;
</script>

View File

@@ -37,73 +37,56 @@
</v-input>
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from 'vue';
import { useI18n } from 'vue-i18n';
import { useFieldsStore } from '@/stores/fields';
<script setup lang="ts">
import { i18n } from '@/lang';
import { useFieldsStore } from '@/stores/fields';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
export default defineComponent({
props: {
modelValue: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
collection: {
type: String,
default: null,
},
disabledFields: {
type: Array as PropType<string[]>,
default: () => [],
},
typeDenyList: {
type: Array as PropType<string[]>,
default: () => [],
},
typeAllowList: {
type: Array as PropType<string[]>,
default: undefined,
},
placeholder: {
type: String,
default: () => i18n.global.t('foreign_key') + '...',
},
nullable: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue'],
setup(props) {
const { t } = useI18n();
const fieldsStore = useFieldsStore();
const props = withDefaults(
defineProps<{
modelValue?: string;
disabled?: boolean;
collection?: string;
disabledFields?: string[];
const fields = computed(() => {
if (!props.collection) return [];
typeDenyList?: string[];
typeAllowList?: string[];
return fieldsStore.getFieldsForCollectionAlphabetical(props.collection).map((field) => ({
text: field.field,
value: field.field,
disabled:
!field.schema ||
!!field.schema?.is_primary_key ||
props.disabledFields.includes(field.field) ||
props.typeDenyList.includes(field.type) ||
(props.typeAllowList && !props.typeAllowList.includes(field.type)),
}));
});
placeholder?: string;
nullable?: boolean;
}>(),
{
disabled: false,
disabledFields: () => [],
typeDenyList: () => [],
placeholder: () => i18n.global.t('foreign_key') + '...',
nullable: false,
}
);
const fieldExists = computed(() => {
if (!props.collection || !props.modelValue) return false;
return !!fieldsStore.getField(props.collection, props.modelValue);
});
defineEmits(['update:modelValue']);
return { t, fields, fieldExists };
},
const { t } = useI18n();
const fieldsStore = useFieldsStore();
const fields = computed(() => {
if (!props.collection) return [];
return fieldsStore.getFieldsForCollectionAlphabetical(props.collection).map((field) => ({
text: field.field,
value: field.field,
disabled:
!field.schema ||
!!field.schema?.is_primary_key ||
props.disabledFields.includes(field.field) ||
props.typeDenyList.includes(field.type) ||
(props.typeAllowList && !props.typeAllowList.includes(field.type)),
}));
});
const fieldExists = computed(() => {
if (!props.collection || !props.modelValue) return false;
return !!fieldsStore.getField(props.collection, props.modelValue);
});
</script>

View File

@@ -76,36 +76,30 @@
</v-menu>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import { Field } from '@directus/types';
import { useI18n } from 'vue-i18n';
<script setup lang="ts">
import { getLocalTypeForField } from '@/utils/get-local-type';
import { Field } from '@directus/types';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
export default defineComponent({
name: 'FieldSelectMenu',
props: {
field: {
type: Object as PropType<Field>,
required: true,
},
noDelete: {
type: Boolean,
default: false,
},
},
emits: ['toggleVisibility', 'duplicate', 'delete', 'setWidth'],
setup(props) {
const { t } = useI18n();
const props = withDefaults(
defineProps<{
field: Field;
noDelete?: boolean;
}>(),
{
noDelete: false,
}
);
const localType = computed(() => getLocalTypeForField(props.field.collection, props.field.field));
const isPrimaryKey = computed(() => props.field.schema?.is_primary_key === true);
defineEmits(['toggleVisibility', 'duplicate', 'delete', 'setWidth']);
const duplicable = computed(() => localType.value === 'standard' && isPrimaryKey.value === false);
const { t } = useI18n();
return { t, localType, isPrimaryKey, duplicable };
},
});
const localType = computed(() => getLocalTypeForField(props.field.collection, props.field.field));
const isPrimaryKey = computed(() => props.field.schema?.is_primary_key === true);
const duplicable = computed(() => localType.value === 'standard' && isPrimaryKey.value === false);
</script>
<style scoped>

View File

@@ -151,9 +151,9 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, ref, computed, unref } from 'vue';
import { ref, computed, unref } from 'vue';
import { useCollectionsStore } from '@/stores/collections';
import { useFieldsStore } from '@/stores/fields';
import { useRouter } from 'vue-router';
@@ -170,193 +170,158 @@ import formatTitle from '@directus/format-title';
import { useExtension } from '@/composables/use-extension';
import { getRelatedCollection } from '@/utils/get-related-collection';
export default defineComponent({
name: 'FieldSelect',
components: { FieldSelectMenu, Draggable },
props: {
field: {
type: Object as PropType<Field>,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
fields: {
type: Array as PropType<Field[]>,
default: () => [],
},
},
emits: ['setNestedSort'],
setup(props, { emit }) {
const { t } = useI18n();
const props = withDefaults(
defineProps<{
field: Field;
disabled?: boolean;
fields?: Field[];
}>(),
{
disabled: false,
fields: () => [],
}
);
const router = useRouter();
const emit = defineEmits(['setNestedSort']);
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const { t } = useI18n();
const editActive = ref(false);
const router = useRouter();
const { deleteActive, deleting, deleteField } = useDeleteField();
const { duplicateActive, duplicateName, collections, duplicateTo, saveDuplicate, duplicating } = useDuplicate();
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const inter = useExtension(
'interface',
computed(() => props.field.meta?.interface ?? null)
);
const { deleteActive, deleting, deleteField } = useDeleteField();
const { duplicateActive, duplicateName, collections, duplicateTo, saveDuplicate, duplicating } = useDuplicate();
const interfaceName = computed(() => inter.value?.name ?? null);
const inter = useExtension(
'interface',
computed(() => props.field.meta?.interface ?? null)
);
const hidden = computed(() => props.field.meta?.hidden === true);
const interfaceName = computed(() => inter.value?.name ?? null);
const localType = computed(() => getLocalTypeForField(props.field.collection, props.field.field));
const hidden = computed(() => props.field.meta?.hidden === true);
const nestedFields = computed(() => props.fields.filter((field) => field.meta?.group === props.field.field));
const localType = computed(() => getLocalTypeForField(props.field.collection, props.field.field));
const relatedCollectionInfo = computed(() => getRelatedCollection(props.field.collection, props.field.field));
const nestedFields = computed(() => props.fields.filter((field) => field.meta?.group === props.field.field));
const showRelatedCollectionLink = computed(
() =>
unref(relatedCollectionInfo) !== null &&
props.field.collection !== unref(relatedCollectionInfo)?.relatedCollection &&
['translations', 'm2o', 'm2m', 'o2m', 'files'].includes(unref(localType) as string)
);
const relatedCollectionInfo = computed(() => getRelatedCollection(props.field.collection, props.field.field));
return {
t,
interfaceName,
formatTitle,
editActive,
setWidth,
deleteActive,
deleting,
deleteField,
duplicateActive,
collections,
duplicateName,
duplicateTo,
saveDuplicate,
duplicating,
openFieldDetail,
hidden,
toggleVisibility,
localType,
showRelatedCollectionLink,
relatedCollectionInfo,
hideDragImage,
onGroupSortChange,
nestedFields,
const showRelatedCollectionLink = computed(
() =>
unref(relatedCollectionInfo) !== null &&
props.field.collection !== unref(relatedCollectionInfo)?.relatedCollection &&
['translations', 'm2o', 'm2m', 'o2m', 'files'].includes(unref(localType) as string)
);
function setWidth(width: string) {
fieldsStore.updateField(props.field.collection, props.field.field, { meta: { width } });
}
function toggleVisibility() {
fieldsStore.updateField(props.field.collection, props.field.field, {
meta: { hidden: !props.field.meta?.hidden },
});
}
function useDeleteField() {
const deleteActive = ref(false);
const deleting = ref(false);
return {
deleteActive,
deleting,
deleteField,
};
async function deleteField() {
await fieldsStore.deleteField(props.field.collection, props.field.field);
deleting.value = false;
deleteActive.value = false;
}
}
function useDuplicate() {
const duplicateActive = ref(false);
const duplicateName = ref(props.field.field + '_copy');
const duplicating = ref(false);
const collections = computed(() =>
collectionsStore.collections
.map(({ collection }) => collection)
.filter((collection) => collection.startsWith('directus_') === false)
);
const duplicateTo = ref(props.field.collection);
return {
duplicateActive,
duplicateName,
collections,
duplicateTo,
saveDuplicate,
duplicating,
};
async function saveDuplicate() {
const newField: Record<string, any> = {
...cloneDeep(props.field),
field: duplicateName.value,
collection: duplicateTo.value,
};
function setWidth(width: string) {
fieldsStore.updateField(props.field.collection, props.field.field, { meta: { width } });
if (newField.meta) {
delete newField.meta.id;
delete newField.meta.sort;
delete newField.meta.group;
}
function toggleVisibility() {
fieldsStore.updateField(props.field.collection, props.field.field, {
meta: { hidden: !props.field.meta?.hidden },
if (newField.schema) {
delete newField.schema.comment;
}
delete newField.name;
duplicating.value = true;
try {
await fieldsStore.createField(duplicateTo.value, newField);
notify({
title: t('field_create_success', { field: newField.field }),
});
duplicateActive.value = false;
} catch (err: any) {
unexpectedError(err);
} finally {
duplicating.value = false;
}
}
}
function useDeleteField() {
const deleteActive = ref(false);
const deleting = ref(false);
async function openFieldDetail() {
if (!props.field.meta) {
const special = getSpecialForType(props.field.type);
await fieldsStore.updateField(props.field.collection, props.field.field, { meta: { special } });
}
return {
deleteActive,
deleting,
deleteField,
};
router.push(`/settings/data-model/${props.field.collection}/${props.field.field}`);
}
async function deleteField() {
await fieldsStore.deleteField(props.field.collection, props.field.field);
deleting.value = false;
deleteActive.value = false;
}
}
async function onGroupSortChange(fields: Field[]) {
const updates = fields.map((field, index) => ({
field: field.field,
meta: {
sort: index + 1,
group: props.field.meta!.field,
},
}));
function useDuplicate() {
const duplicateActive = ref(false);
const duplicateName = ref(props.field.field + '_copy');
const duplicating = ref(false);
const collections = computed(() =>
collectionsStore.collections
.map(({ collection }) => collection)
.filter((collection) => collection.startsWith('directus_') === false)
);
const duplicateTo = ref(props.field.collection);
return {
duplicateActive,
duplicateName,
collections,
duplicateTo,
saveDuplicate,
duplicating,
};
async function saveDuplicate() {
const newField: Record<string, any> = {
...cloneDeep(props.field),
field: duplicateName.value,
collection: duplicateTo.value,
};
if (newField.meta) {
delete newField.meta.id;
delete newField.meta.sort;
delete newField.meta.group;
}
if (newField.schema) {
delete newField.schema.comment;
}
delete newField.name;
duplicating.value = true;
try {
await fieldsStore.createField(duplicateTo.value, newField);
notify({
title: t('field_create_success', { field: newField.field }),
});
duplicateActive.value = false;
} catch (err: any) {
unexpectedError(err);
} finally {
duplicating.value = false;
}
}
}
async function openFieldDetail() {
if (!props.field.meta) {
const special = getSpecialForType(props.field.type);
await fieldsStore.updateField(props.field.collection, props.field.field, { meta: { special } });
}
router.push(`/settings/data-model/${props.field.collection}/${props.field.field}`);
}
async function onGroupSortChange(fields: Field[]) {
const updates = fields.map((field, index) => ({
field: field.field,
meta: {
sort: index + 1,
group: props.field.meta!.field,
},
}));
emit('setNestedSort', updates);
}
},
});
emit('setNestedSort', updates);
}
</script>
<style lang="scss" scoped>

View File

@@ -49,133 +49,122 @@
</div>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, computed, toRefs } from 'vue';
import { useCollection } from '@directus/composables';
import Draggable from 'vuedraggable';
import { Field } from '@directus/types';
<script setup lang="ts">
import { useFieldsStore } from '@/stores/fields';
import FieldSelect from './field-select.vue';
import { hideDragImage } from '@/utils/hide-drag-image';
import { orderBy, isNil } from 'lodash';
import { LocalType } from '@directus/types';
import { useCollection } from '@directus/composables';
import { Field, LocalType } from '@directus/types';
import { isNil, orderBy } from 'lodash';
import { computed, toRefs } from 'vue';
import { useI18n } from 'vue-i18n';
import Draggable from 'vuedraggable';
import FieldSelect from './field-select.vue';
export default defineComponent({
name: 'FieldsManagement',
components: { Draggable, FieldSelect },
props: {
collection: {
type: String,
required: true,
},
},
setup(props) {
const { t } = useI18n();
const props = defineProps<{
collection: string;
}>();
const { collection } = toRefs(props);
const { fields } = useCollection(collection);
const fieldsStore = useFieldsStore();
const { t } = useI18n();
const parsedFields = computed(() => {
return orderBy(fields.value, [(o) => (o.meta?.sort ? Number(o.meta?.sort) : null), (o) => o.meta?.id]).filter(
(field) => field.field.startsWith('$') === false
);
});
const { collection } = toRefs(props);
const { fields } = useCollection(collection);
const fieldsStore = useFieldsStore();
const lockedFields = computed(() => {
return parsedFields.value.filter((field) => field.meta?.system === true);
});
const usableFields = computed(() => {
return parsedFields.value.filter((field) => field.meta?.system !== true);
});
const addOptions = computed<Array<{ type: LocalType; icon: string; text: any } | { divider: boolean }>>(() => [
{
type: 'standard',
icon: 'create',
text: t('standard_field'),
},
{
type: 'presentation',
icon: 'scatter_plot',
text: t('presentation_and_aliases'),
},
{
type: 'group',
icon: 'view_in_ar',
text: t('field_group'),
},
{
divider: true,
},
{
type: 'file',
icon: 'photo',
text: t('single_file'),
},
{
type: 'files',
icon: 'collections',
text: t('multiple_files'),
},
{
divider: true,
},
{
type: 'm2o',
icon: 'call_merge',
text: t('m2o_relationship'),
},
{
type: 'o2m',
icon: 'call_split',
text: t('o2m_relationship'),
},
{
type: 'm2m',
icon: 'import_export',
text: t('m2m_relationship'),
},
{
type: 'm2a',
icon: 'gesture',
text: t('m2a_relationship'),
},
{
divider: true,
},
{
type: 'translations',
icon: 'translate',
text: t('translations'),
},
]);
return { t, usableFields, lockedFields, setSort, hideDragImage, addOptions, setNestedSort, isNil };
async function setSort(fields: Field[]) {
const updates = fields.map((field, index) => ({
field: field.field,
meta: {
sort: index + 1,
group: null,
},
}));
await fieldsStore.updateFields(collection.value, updates);
}
async function setNestedSort(updates?: Field[]) {
updates = (updates || []).filter((val) => isNil(val) === false);
if (updates.length > 0) {
await fieldsStore.updateFields(collection.value, updates);
}
}
},
const parsedFields = computed(() => {
return orderBy(fields.value, [(o) => (o.meta?.sort ? Number(o.meta?.sort) : null), (o) => o.meta?.id]).filter(
(field) => field.field.startsWith('$') === false
);
});
const lockedFields = computed(() => {
return parsedFields.value.filter((field) => field.meta?.system === true);
});
const usableFields = computed(() => {
return parsedFields.value.filter((field) => field.meta?.system !== true);
});
const addOptions = computed<Array<{ type: LocalType; icon: string; text: any } | { divider: boolean }>>(() => [
{
type: 'standard',
icon: 'create',
text: t('standard_field'),
},
{
type: 'presentation',
icon: 'scatter_plot',
text: t('presentation_and_aliases'),
},
{
type: 'group',
icon: 'view_in_ar',
text: t('field_group'),
},
{
divider: true,
},
{
type: 'file',
icon: 'photo',
text: t('single_file'),
},
{
type: 'files',
icon: 'collections',
text: t('multiple_files'),
},
{
divider: true,
},
{
type: 'm2o',
icon: 'call_merge',
text: t('m2o_relationship'),
},
{
type: 'o2m',
icon: 'call_split',
text: t('o2m_relationship'),
},
{
type: 'm2m',
icon: 'import_export',
text: t('m2m_relationship'),
},
{
type: 'm2a',
icon: 'gesture',
text: t('m2a_relationship'),
},
{
divider: true,
},
{
type: 'translations',
icon: 'translate',
text: t('translations'),
},
]);
async function setSort(fields: Field[]) {
const updates = fields.map((field, index) => ({
field: field.field,
meta: {
sort: index + 1,
group: null,
},
}));
await fieldsStore.updateFields(collection.value, updates);
}
async function setNestedSort(updates?: Field[]) {
updates = (updates || []).filter((val) => isNil(val) === false);
if (updates.length > 0) {
await fieldsStore.updateFields(collection.value, updates);
}
}
</script>
<style lang="scss" scoped>

View File

@@ -99,116 +99,77 @@
</private-view>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, computed, toRefs, ref } from 'vue';
import SettingsNavigation from '../../../components/navigation.vue';
import { useCollection } from '@directus/composables';
import FieldsManagement from './components/fields-management.vue';
<script setup lang="ts">
import { useEditsGuard } from '@/composables/use-edits-guard';
import { useItem } from '@/composables/use-item';
import { useRouter } from 'vue-router';
import { useShortcut } from '@/composables/use-shortcut';
import { useCollectionsStore } from '@/stores/collections';
import { useFieldsStore } from '@/stores/fields';
import { useShortcut } from '@/composables/use-shortcut';
import { useEditsGuard } from '@/composables/use-edits-guard';
import { useCollection } from '@directus/composables';
import { computed, ref, toRefs } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import SettingsNavigation from '../../../components/navigation.vue';
import FieldsManagement from './components/fields-management.vue';
export default defineComponent({
components: { SettingsNavigation, FieldsManagement },
props: {
collection: {
type: String,
required: true,
},
const props = defineProps<{
collection: string;
// Field detail modal only
field?: string;
type?: string;
}>();
// Field detail modal only
field: {
type: String,
default: null,
},
type: {
type: String,
default: null,
},
},
setup(props) {
const { t } = useI18n();
const { t } = useI18n();
const router = useRouter();
const router = useRouter();
const { collection } = toRefs(props);
const { info: collectionInfo, fields } = useCollection(collection);
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const { collection } = toRefs(props);
const { info: collectionInfo } = useCollection(collection);
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const { isNew, edits, item, saving, loading, error, save, remove, deleting, saveAsCopy, isBatch } = useItem(
ref('directus_collections'),
collection
);
const { edits, item, saving, loading, save, remove, deleting, isBatch } = useItem(
ref('directus_collections'),
collection
);
const hasEdits = computed<boolean>(() => {
if (!edits.value.meta) return false;
return Object.keys(edits.value.meta).length > 0;
});
useShortcut('meta+s', () => {
if (hasEdits.value) saveAndStay();
});
const confirmDelete = ref(false);
const { confirmLeave, leaveTo } = useEditsGuard(hasEdits);
return {
t,
collectionInfo,
fields,
confirmDelete,
isNew,
edits,
item,
saving,
loading,
error,
save,
remove,
deleting,
saveAsCopy,
isBatch,
deleteAndQuit,
saveAndQuit,
hasEdits,
confirmLeave,
leaveTo,
discardAndLeave,
};
async function deleteAndQuit() {
await remove();
await Promise.all([collectionsStore.hydrate(), fieldsStore.hydrate()]);
edits.value = {};
router.replace(`/settings/data-model`);
}
async function saveAndStay() {
await save();
await Promise.all([collectionsStore.hydrate(), fieldsStore.hydrate()]);
}
async function saveAndQuit() {
await save();
await Promise.all([collectionsStore.hydrate(), fieldsStore.hydrate()]);
router.push(`/settings/data-model`);
}
function discardAndLeave() {
if (!leaveTo.value) return;
edits.value = {};
confirmLeave.value = false;
router.push(leaveTo.value);
}
},
const hasEdits = computed<boolean>(() => {
if (!edits.value.meta) return false;
return Object.keys(edits.value.meta).length > 0;
});
useShortcut('meta+s', () => {
if (hasEdits.value) saveAndStay();
});
const confirmDelete = ref(false);
const { confirmLeave, leaveTo } = useEditsGuard(hasEdits);
async function deleteAndQuit() {
await remove();
await Promise.all([collectionsStore.hydrate(), fieldsStore.hydrate()]);
edits.value = {};
router.replace(`/settings/data-model`);
}
async function saveAndStay() {
await save();
await Promise.all([collectionsStore.hydrate(), fieldsStore.hydrate()]);
}
async function saveAndQuit() {
await save();
await Promise.all([collectionsStore.hydrate(), fieldsStore.hydrate()]);
router.push(`/settings/data-model`);
}
function discardAndLeave() {
if (!leaveTo.value) return;
edits.value = {};
confirmLeave.value = false;
router.push(leaveTo.value);
}
</script>
<style lang="scss" scoped>

View File

@@ -126,20 +126,19 @@
</v-drawer>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { cloneDeep } from 'lodash';
import { defineComponent, ref, reactive, watch } from 'vue';
<script setup lang="ts">
import api from '@/api';
import { Field, Relation } from '@directus/types';
import { useFieldsStore } from '@/stores/fields';
import { useDialogRoute } from '@/composables/use-dialog-route';
import { useCollectionsStore } from '@/stores/collections';
import { useFieldsStore } from '@/stores/fields';
import { useRelationsStore } from '@/stores/relations';
import { notify } from '@/utils/notify';
import { useDialogRoute } from '@/composables/use-dialog-route';
import { useRouter } from 'vue-router';
import { unexpectedError } from '@/utils/unexpected-error';
import { DeepPartial } from '@directus/types';
import { DeepPartial, Field, Relation } from '@directus/types';
import { cloneDeep } from 'lodash';
import { reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
const defaultSystemFields = {
status: {
@@ -186,329 +185,311 @@ const defaultSystemFields = {
},
};
export default defineComponent({
setup() {
const { t } = useI18n();
const { t } = useI18n();
const router = useRouter();
const router = useRouter();
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const relationsStore = useRelationsStore();
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const relationsStore = useRelationsStore();
const isOpen = useDialogRoute();
const isOpen = useDialogRoute();
const currentTab = ref(['collection_setup']);
const currentTab = ref(['collection_setup']);
const collectionName = ref(null);
const singleton = ref(false);
const primaryKeyFieldName = ref('id');
const primaryKeyFieldType = ref<'auto_int' | 'auto_big_int' | 'uuid' | 'manual'>('auto_int');
const collectionName = ref(null);
const singleton = ref(false);
const primaryKeyFieldName = ref('id');
const primaryKeyFieldType = ref<'auto_int' | 'auto_big_int' | 'uuid' | 'manual'>('auto_int');
const sortField = ref<string>();
const sortField = ref<string>();
const archiveField = ref<string>();
const archiveValue = ref<string>();
const unarchiveValue = ref<string>();
const archiveField = ref<string>();
const archiveValue = ref<string>();
const unarchiveValue = ref<string>();
const systemFields = reactive(cloneDeep(defaultSystemFields));
const systemFields = reactive(cloneDeep(defaultSystemFields));
const saving = ref(false);
const saving = ref(false);
watch(() => singleton.value, setOptionsForSingleton);
watch(() => singleton.value, setOptionsForSingleton);
function setOptionsForSingleton() {
systemFields.sort = { ...defaultSystemFields.sort };
systemFields.sort.inputDisabled = singleton.value;
}
async function save() {
saving.value = true;
try {
await api.post(`/collections`, {
collection: collectionName.value,
fields: [getPrimaryKeyField(), ...getSystemFields()],
schema: {},
meta: {
sort_field: sortField.value,
archive_field: archiveField.value,
archive_value: archiveValue.value,
unarchive_value: unarchiveValue.value,
singleton: singleton.value,
},
});
const storeHydrations: Promise<void>[] = [];
const relations = getSystemRelations();
if (relations.length > 0) {
const requests = relations.map((relation) => api.post('/relations', relation));
await Promise.all(requests);
storeHydrations.push(relationsStore.hydrate());
}
storeHydrations.push(collectionsStore.hydrate(), fieldsStore.hydrate());
await Promise.all(storeHydrations);
notify({
title: t('collection_created'),
});
router.replace(`/settings/data-model/${collectionName.value}`);
} catch (err: any) {
unexpectedError(err);
} finally {
saving.value = false;
}
}
function getPrimaryKeyField() {
if (primaryKeyFieldType.value === 'uuid') {
return {
t,
router,
isOpen,
currentTab,
save,
systemFields,
primaryKeyFieldName,
primaryKeyFieldType,
collectionName,
saving,
singleton,
field: primaryKeyFieldName.value,
type: 'uuid',
meta: {
hidden: true,
readonly: true,
interface: 'input',
special: ['uuid'],
},
schema: {
is_primary_key: true,
length: 36,
has_auto_increment: false,
},
};
} else if (primaryKeyFieldType.value === 'manual') {
return {
field: primaryKeyFieldName.value,
type: 'string',
meta: {
interface: 'input',
readonly: false,
hidden: false,
},
schema: {
is_primary_key: true,
length: 255,
has_auto_increment: false,
},
};
} else {
return {
field: primaryKeyFieldName.value,
type: primaryKeyFieldType.value === 'auto_big_int' ? 'bigInteger' : 'integer',
meta: {
hidden: true,
interface: 'input',
readonly: true,
},
schema: {
is_primary_key: true,
has_auto_increment: true,
},
};
}
}
function setOptionsForSingleton() {
systemFields.sort = { ...defaultSystemFields.sort };
systemFields.sort.inputDisabled = singleton.value;
}
function getSystemFields() {
const fields: DeepPartial<Field>[] = [];
async function save() {
saving.value = true;
try {
await api.post(`/collections`, {
collection: collectionName.value,
fields: [getPrimaryKeyField(), ...getSystemFields()],
schema: {},
meta: {
sort_field: sortField.value,
archive_field: archiveField.value,
archive_value: archiveValue.value,
unarchive_value: unarchiveValue.value,
singleton: singleton.value,
},
});
const storeHydrations: Promise<void>[] = [];
const relations = getSystemRelations();
if (relations.length > 0) {
const requests = relations.map((relation) => api.post('/relations', relation));
await Promise.all(requests);
storeHydrations.push(relationsStore.hydrate());
}
storeHydrations.push(collectionsStore.hydrate(), fieldsStore.hydrate());
await Promise.all(storeHydrations);
notify({
title: t('collection_created'),
});
router.replace(`/settings/data-model/${collectionName.value}`);
} catch (err: any) {
unexpectedError(err);
} finally {
saving.value = false;
}
}
function getPrimaryKeyField() {
if (primaryKeyFieldType.value === 'uuid') {
return {
field: primaryKeyFieldName.value,
type: 'uuid',
meta: {
hidden: true,
readonly: true,
interface: 'input',
special: ['uuid'],
},
schema: {
is_primary_key: true,
length: 36,
has_auto_increment: false,
},
};
} else if (primaryKeyFieldType.value === 'manual') {
return {
field: primaryKeyFieldName.value,
type: 'string',
meta: {
interface: 'input',
readonly: false,
hidden: false,
},
schema: {
is_primary_key: true,
length: 255,
has_auto_increment: false,
},
};
} else {
return {
field: primaryKeyFieldName.value,
type: primaryKeyFieldType.value === 'auto_big_int' ? 'bigInteger' : 'integer',
meta: {
hidden: true,
interface: 'input',
readonly: true,
},
schema: {
is_primary_key: true,
has_auto_increment: true,
},
};
}
}
function getSystemFields() {
const fields: DeepPartial<Field>[] = [];
// Status
if (systemFields.status.enabled === true) {
fields.push({
field: systemFields.status.name,
type: 'string',
meta: {
width: 'full',
options: {
choices: [
{
text: '$t:published',
value: 'published',
},
{
text: '$t:draft',
value: 'draft',
},
{
text: '$t:archived',
value: 'archived',
},
],
// Status
if (systemFields.status.enabled === true) {
fields.push({
field: systemFields.status.name,
type: 'string',
meta: {
width: 'full',
options: {
choices: [
{
text: '$t:published',
value: 'published',
},
interface: 'select-dropdown',
display: 'labels',
display_options: {
showAsDot: true,
choices: [
{
text: '$t:published',
value: 'published',
foreground: '#FFFFFF',
background: 'var(--primary)',
},
{
text: '$t:draft',
value: 'draft',
foreground: '#18222F',
background: '#D3DAE4',
},
{
text: '$t:archived',
value: 'archived',
foreground: '#FFFFFF',
background: 'var(--warning)',
},
],
{
text: '$t:draft',
value: 'draft',
},
},
schema: {
default_value: 'draft',
is_nullable: false,
},
});
archiveField.value = systemFields.status.name;
archiveValue.value = 'archived';
unarchiveValue.value = 'draft';
}
// Sort
if (systemFields.sort.enabled === true) {
fields.push({
field: systemFields.sort.name,
type: 'integer',
meta: {
interface: 'input',
hidden: true,
},
schema: {},
});
sortField.value = systemFields.sort.name;
}
if (systemFields.userCreated.enabled === true) {
fields.push({
field: systemFields.userCreated.name,
type: 'uuid',
meta: {
special: ['user-created'],
interface: 'select-dropdown-m2o',
options: {
template: '{{avatar.$thumbnail}} {{first_name}} {{last_name}}',
{
text: '$t:archived',
value: 'archived',
},
display: 'user',
readonly: true,
hidden: true,
width: 'half',
},
schema: {},
});
}
if (systemFields.dateCreated.enabled === true) {
fields.push({
field: systemFields.dateCreated.name,
type: 'timestamp',
meta: {
special: ['date-created'],
interface: 'datetime',
readonly: true,
hidden: true,
width: 'half',
display: 'datetime',
display_options: {
relative: true,
],
},
interface: 'select-dropdown',
display: 'labels',
display_options: {
showAsDot: true,
choices: [
{
text: '$t:published',
value: 'published',
foreground: '#FFFFFF',
background: 'var(--primary)',
},
},
schema: {},
});
}
if (systemFields.userUpdated.enabled === true) {
fields.push({
field: systemFields.userUpdated.name,
type: 'uuid',
meta: {
special: ['user-updated'],
interface: 'select-dropdown-m2o',
options: {
template: '{{avatar.$thumbnail}} {{first_name}} {{last_name}}',
{
text: '$t:draft',
value: 'draft',
foreground: '#18222F',
background: '#D3DAE4',
},
display: 'user',
readonly: true,
hidden: true,
width: 'half',
},
schema: {},
});
}
if (systemFields.dateUpdated.enabled === true) {
fields.push({
field: systemFields.dateUpdated.name,
type: 'timestamp',
meta: {
special: ['date-updated'],
interface: 'datetime',
readonly: true,
hidden: true,
width: 'half',
display: 'datetime',
display_options: {
relative: true,
{
text: '$t:archived',
value: 'archived',
foreground: '#FFFFFF',
background: 'var(--warning)',
},
},
schema: {},
});
}
],
},
},
schema: {
default_value: 'draft',
is_nullable: false,
},
});
return fields;
}
archiveField.value = systemFields.status.name;
archiveValue.value = 'archived';
unarchiveValue.value = 'draft';
}
function getSystemRelations() {
const relations: Partial<Relation>[] = [];
// Sort
if (systemFields.sort.enabled === true) {
fields.push({
field: systemFields.sort.name,
type: 'integer',
meta: {
interface: 'input',
hidden: true,
},
schema: {},
});
if (systemFields.userCreated.enabled === true) {
relations.push({
collection: collectionName.value!,
field: systemFields.userCreated.name,
related_collection: 'directus_users',
schema: {},
});
}
sortField.value = systemFields.sort.name;
}
if (systemFields.userUpdated.enabled === true) {
relations.push({
collection: collectionName.value!,
field: systemFields.userUpdated.name,
related_collection: 'directus_users',
schema: {},
});
}
if (systemFields.userCreated.enabled === true) {
fields.push({
field: systemFields.userCreated.name,
type: 'uuid',
meta: {
special: ['user-created'],
interface: 'select-dropdown-m2o',
options: {
template: '{{avatar.$thumbnail}} {{first_name}} {{last_name}}',
},
display: 'user',
readonly: true,
hidden: true,
width: 'half',
},
schema: {},
});
}
return relations;
}
},
});
if (systemFields.dateCreated.enabled === true) {
fields.push({
field: systemFields.dateCreated.name,
type: 'timestamp',
meta: {
special: ['date-created'],
interface: 'datetime',
readonly: true,
hidden: true,
width: 'half',
display: 'datetime',
display_options: {
relative: true,
},
},
schema: {},
});
}
if (systemFields.userUpdated.enabled === true) {
fields.push({
field: systemFields.userUpdated.name,
type: 'uuid',
meta: {
special: ['user-updated'],
interface: 'select-dropdown-m2o',
options: {
template: '{{avatar.$thumbnail}} {{first_name}} {{last_name}}',
},
display: 'user',
readonly: true,
hidden: true,
width: 'half',
},
schema: {},
});
}
if (systemFields.dateUpdated.enabled === true) {
fields.push({
field: systemFields.dateUpdated.name,
type: 'timestamp',
meta: {
special: ['date-updated'],
interface: 'datetime',
readonly: true,
hidden: true,
width: 'half',
display: 'datetime',
display_options: {
relative: true,
},
},
schema: {},
});
}
return fields;
}
function getSystemRelations() {
const relations: Partial<Relation>[] = [];
if (systemFields.userCreated.enabled === true) {
relations.push({
collection: collectionName.value!,
field: systemFields.userCreated.name,
related_collection: 'directus_users',
schema: {},
});
}
if (systemFields.userUpdated.enabled === true) {
relations.push({
collection: collectionName.value!,
field: systemFields.userUpdated.name,
related_collection: 'directus_users',
schema: {},
});
}
return relations;
}
</script>
<style lang="scss" scoped>