mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Interface one to many (#533)
* Extract edit modal to standalone component * Fix creating new item from m2o edit modal * Rename item-modal to modal-detail * Extract selection modal in standalone component * Add required primary-key prop to v-form * Add inline prop to table * Fetch items in o2m * Accept numbers for primary key in v-form * Use correct collection in render template in m2o * Render modal detail * Fix edit existing * Add add-new * Do things * Finish o2m
This commit is contained in:
@@ -92,6 +92,7 @@
|
||||
:type="field.type"
|
||||
:collection="field.collection"
|
||||
:field="field.field"
|
||||
:primary-key="primaryKey"
|
||||
@input="setValue(field, $event)"
|
||||
/>
|
||||
</div>
|
||||
@@ -146,6 +147,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
primaryKey: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const el = ref<Element>(null);
|
||||
|
||||
@@ -8,6 +8,7 @@ export type HeaderRaw = {
|
||||
align?: Alignment;
|
||||
sortable?: boolean;
|
||||
width?: number | null;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type Header = Required<HeaderRaw>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="v-table" :class="{ loading }">
|
||||
<div class="v-table" :class="{ loading, inline }">
|
||||
<table
|
||||
:summary="_headers.map((header) => header.text).join(', ')"
|
||||
:style="{
|
||||
@@ -163,6 +163,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit, listeners, slots }) {
|
||||
const _headers = computed({
|
||||
@@ -382,6 +386,10 @@ body {
|
||||
grid-template-columns: var(--grid-columns);
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
color: var(--foreground-normal);
|
||||
@@ -448,4 +456,9 @@ body {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inline {
|
||||
border: 2px solid var(--border-normal);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,7 @@ import InterfaceDateTime from './datetime';
|
||||
import InterfaceImage from './image';
|
||||
import InterfaceIcon from './icon';
|
||||
import InterfaceManyToOne from './many-to-one';
|
||||
import InterfaceOneToMany from './one-to-many';
|
||||
|
||||
export const interfaces = [
|
||||
InterfaceTextInput,
|
||||
@@ -32,6 +33,7 @@ export const interfaces = [
|
||||
InterfaceImage,
|
||||
InterfaceIcon,
|
||||
InterfaceManyToOne,
|
||||
InterfaceOneToMany,
|
||||
];
|
||||
|
||||
export default interfaces;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<template #input v-if="currentItem">
|
||||
<div class="preview">
|
||||
<render-template
|
||||
:collection="collection"
|
||||
:collection="relatedCollection.collection"
|
||||
:item="currentItem"
|
||||
:template="displayTemplate"
|
||||
/>
|
||||
@@ -31,7 +31,7 @@
|
||||
name="open_in_new"
|
||||
class="edit"
|
||||
v-tooltip="$t('edit')"
|
||||
@click.stop="startEditing"
|
||||
@click.stop="editModalActive = true"
|
||||
/>
|
||||
<v-icon
|
||||
name="close"
|
||||
@@ -45,7 +45,7 @@
|
||||
class="add"
|
||||
name="add"
|
||||
v-tooltip="$t('add_new_item')"
|
||||
@click.stop="startEditing"
|
||||
@click.stop="editModalActive = true"
|
||||
/>
|
||||
<v-icon class="expand" :class="{ active }" name="expand_more" />
|
||||
</template>
|
||||
@@ -65,8 +65,8 @@
|
||||
<template v-else>
|
||||
<v-list-item
|
||||
v-for="item in items"
|
||||
:key="item[primaryKeyField.field]"
|
||||
:active="value === item[primaryKeyField.field]"
|
||||
:key="item[relatedPrimaryKeyField.field]"
|
||||
:active="value === item[relatedPrimaryKeyField.field]"
|
||||
@click="setCurrent(item)"
|
||||
>
|
||||
<v-list-item-content>
|
||||
@@ -81,49 +81,33 @@
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-modal
|
||||
v-model="editModalActive"
|
||||
:title="$t('editing_in', { collection: relatedCollection.name })"
|
||||
persistent
|
||||
>
|
||||
<v-form
|
||||
:loading="editLoading"
|
||||
:initial-values="existingItem"
|
||||
:collection="relatedCollection.collection"
|
||||
v-model="edits"
|
||||
/>
|
||||
<modal-detail
|
||||
:active.sync="editModalActive"
|
||||
:collection="relatedCollection.collection"
|
||||
:primary-key="currentPrimaryKey"
|
||||
:edits="edits"
|
||||
@input="stageEdits"
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<v-button @click="cancelEditing" secondary>{{ $t('cancel') }}</v-button>
|
||||
<v-button @click="stopEditing">{{ $t('save') }}</v-button>
|
||||
</template>
|
||||
</v-modal>
|
||||
|
||||
<v-modal v-model="selectModalActive" :title="$t('select_item')" no-padding>
|
||||
<layout-tabular
|
||||
class="layout"
|
||||
:collection="relatedCollection.collection"
|
||||
:selection="selection"
|
||||
@update:selection="onSelect"
|
||||
select-mode
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<v-button @click="cancelSelecting" secondary>{{ $t('cancel') }}</v-button>
|
||||
<v-button @click="stopSelecting">{{ $t('save') }}</v-button>
|
||||
</template>
|
||||
</v-modal>
|
||||
<modal-browse
|
||||
:active.sync="selectModalActive"
|
||||
:collection="relatedCollection.collection"
|
||||
:selection="selection"
|
||||
@input="stageSelection"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref, toRefs, watch, PropType } from '@vue/composition-api';
|
||||
import { defineComponent, computed, ref, toRefs, watch, PropType, Ref } from '@vue/composition-api';
|
||||
import { useRelationsStore } from '@/stores/relations';
|
||||
import useCollection from '@/composables/use-collection';
|
||||
import getFieldsFromTemplate from '@/utils/get-fields-from-template';
|
||||
import api from '@/api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import useCollectionsStore from '@/stores/collections';
|
||||
import ModalDetail from '@/views/private/components/modal-detail';
|
||||
import ModalBrowse from '@/views/private/components/modal-browse';
|
||||
|
||||
/**
|
||||
* @NOTE
|
||||
@@ -134,6 +118,7 @@ import useCollectionsStore from '@/stores/collections';
|
||||
*/
|
||||
|
||||
export default defineComponent({
|
||||
components: { ModalDetail, ModalBrowse },
|
||||
props: {
|
||||
value: {
|
||||
type: [Number, String, Object],
|
||||
@@ -163,61 +148,45 @@ export default defineComponent({
|
||||
const relationsStore = useRelationsStore();
|
||||
const collectionsStore = useCollectionsStore();
|
||||
|
||||
const { relation, relatedCollection } = useRelation();
|
||||
const { relation, relatedCollection, relatedPrimaryKeyField } = useRelation();
|
||||
const { usesMenu, menuActive } = useMenu();
|
||||
const { info: collectionInfo, primaryKeyField } = useCollection(collection);
|
||||
const { info: collectionInfo } = useCollection(collection);
|
||||
const { selection, stageSelection, selectModalActive } = useSelection();
|
||||
const { displayTemplate, onPreviewClick, requiredFields } = usePreview();
|
||||
const { totalCount, loading: itemsLoading, fetchItems, items } = useItems();
|
||||
const { setCurrent, currentItem, loading: loadingCurrent } = useCurrent();
|
||||
const {
|
||||
edits,
|
||||
editModalActive,
|
||||
startEditing,
|
||||
stopEditing,
|
||||
loading: editLoading,
|
||||
error: editError,
|
||||
existingItem,
|
||||
cancelEditing,
|
||||
} = useEdit();
|
||||
|
||||
const {
|
||||
startSelecting,
|
||||
stopSelecting,
|
||||
cancelSelecting,
|
||||
active: selectModalActive,
|
||||
selection,
|
||||
onSelect,
|
||||
} = useSelectionModal();
|
||||
setCurrent,
|
||||
currentItem,
|
||||
loading: loadingCurrent,
|
||||
currentPrimaryKey,
|
||||
} = useCurrent();
|
||||
|
||||
const { edits, stageEdits } = useEdits();
|
||||
|
||||
const editModalActive = ref(false);
|
||||
|
||||
return {
|
||||
cancelEditing,
|
||||
cancelSelecting,
|
||||
collectionInfo,
|
||||
currentItem,
|
||||
displayTemplate,
|
||||
editError,
|
||||
editLoading,
|
||||
editModalActive,
|
||||
edits,
|
||||
existingItem,
|
||||
items,
|
||||
itemsLoading,
|
||||
loadingCurrent,
|
||||
menuActive,
|
||||
onPreviewClick,
|
||||
primaryKeyField,
|
||||
relatedCollection,
|
||||
relation,
|
||||
selection,
|
||||
selectModalActive,
|
||||
setCurrent,
|
||||
startEditing,
|
||||
startSelecting,
|
||||
stopEditing,
|
||||
stopSelecting,
|
||||
totalCount,
|
||||
onSelect,
|
||||
stageSelection,
|
||||
useMenu,
|
||||
currentPrimaryKey,
|
||||
edits,
|
||||
stageEdits,
|
||||
editModalActive,
|
||||
};
|
||||
|
||||
function useCurrent() {
|
||||
@@ -233,7 +202,7 @@ export default defineComponent({
|
||||
if (
|
||||
newValue !== null &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
newValue !== currentItem.value?.[primaryKeyField.value!.field] &&
|
||||
newValue !== currentItem.value?.[relatedPrimaryKeyField.value!.field] &&
|
||||
(typeof newValue === 'string' || typeof newValue === 'number')
|
||||
) {
|
||||
fetchCurrent();
|
||||
@@ -248,11 +217,29 @@ export default defineComponent({
|
||||
}
|
||||
);
|
||||
|
||||
return { setCurrent, currentItem, loading };
|
||||
const currentPrimaryKey = computed<string | number>(() => {
|
||||
if (!currentItem.value) return '+';
|
||||
if (!props.value) return '+';
|
||||
|
||||
if (typeof props.value === 'number' || typeof props.value === 'string') {
|
||||
return props.value;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof props.value === 'object' &&
|
||||
props.value.hasOwnProperty(relatedPrimaryKeyField.value.field)
|
||||
) {
|
||||
return props.value[relatedPrimaryKeyField.value.field];
|
||||
}
|
||||
|
||||
return '+';
|
||||
});
|
||||
|
||||
return { setCurrent, currentItem, loading, currentPrimaryKey };
|
||||
|
||||
function setCurrent(item: Record<string, any>) {
|
||||
currentItem.value = item;
|
||||
emit('input', item[primaryKeyField.value.field]);
|
||||
emit('input', item[relatedPrimaryKeyField.value.field]);
|
||||
}
|
||||
|
||||
async function fetchCurrent() {
|
||||
@@ -261,8 +248,8 @@ export default defineComponent({
|
||||
|
||||
const fields = requiredFields.value || [];
|
||||
|
||||
if (fields.includes(primaryKeyField.value.field) === false) {
|
||||
fields.push(primaryKeyField.value.field);
|
||||
if (fields.includes(relatedPrimaryKeyField.value.field) === false) {
|
||||
fields.push(relatedPrimaryKeyField.value.field);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -306,8 +293,8 @@ export default defineComponent({
|
||||
|
||||
const fields = requiredFields.value || [];
|
||||
|
||||
if (fields.includes(primaryKeyField.value.field) === false) {
|
||||
fields.push(primaryKeyField.value.field);
|
||||
if (fields.includes(relatedPrimaryKeyField.value.field) === false) {
|
||||
fields.push(relatedPrimaryKeyField.value.field);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -345,97 +332,6 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
function useEdit() {
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const existingItem = ref<any>(null);
|
||||
|
||||
const edits = ref<any>(null);
|
||||
const editModalActive = ref(false);
|
||||
|
||||
return {
|
||||
edits,
|
||||
editModalActive,
|
||||
startEditing,
|
||||
stopEditing,
|
||||
loading,
|
||||
error,
|
||||
existingItem,
|
||||
cancelEditing,
|
||||
};
|
||||
|
||||
async function startEditing() {
|
||||
editModalActive.value = true;
|
||||
loading.value = true;
|
||||
|
||||
// If the current value is an object, it's the previously created changes to the item
|
||||
if (props.value && typeof props.value === 'object') {
|
||||
edits.value = props.value;
|
||||
}
|
||||
// if not, it's the primary key of the existing item (fresh load). It's important for
|
||||
// us to stage the existing ID back up in the object of edits, otherwise the API will
|
||||
// treat the edits as a creation of a new item instead of editing the values of an
|
||||
// existing one
|
||||
else if (
|
||||
props.value &&
|
||||
(typeof props.value === 'number' || typeof props.value === 'string')
|
||||
) {
|
||||
edits.value = {
|
||||
[primaryKeyField.value.field]: props.value,
|
||||
};
|
||||
}
|
||||
|
||||
// If the current item has a primary key, it means that it's an existing item we're
|
||||
// about to edit. In that case, we want to fetch the whole existing item, so we can
|
||||
// render the full form inline
|
||||
if (currentItem.value?.hasOwnProperty(primaryKeyField.value.field)) {
|
||||
loading.value = true;
|
||||
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
|
||||
try {
|
||||
const response = await api.get(
|
||||
`/${currentProjectKey}/items/${relatedCollection.value.collection}/${
|
||||
currentItem.value[primaryKeyField.value.field]
|
||||
}`
|
||||
);
|
||||
existingItem.value = response.data.data;
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
// When the current item doesn't have a primary key, it means it's new. In that case
|
||||
// we don't have to bother fetching anything, as all the edits are already stored
|
||||
// in current item
|
||||
else {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEditing() {
|
||||
editModalActive.value = false;
|
||||
error.value = null;
|
||||
loading.value = false;
|
||||
existingItem.value = null;
|
||||
edits.value = null;
|
||||
}
|
||||
|
||||
function stopEditing() {
|
||||
emit('input', edits.value);
|
||||
|
||||
// Merging the previously fetched existing current item makes sure we don't remove
|
||||
// any fields in the preview that wasn't edited, but still used in the preview
|
||||
currentItem.value = {
|
||||
...currentItem.value,
|
||||
...edits.value,
|
||||
};
|
||||
|
||||
cancelEditing();
|
||||
}
|
||||
}
|
||||
|
||||
function useRelation() {
|
||||
const relation = computed(() => {
|
||||
return relationsStore.getRelationsForField(props.collection, props.field)?.[0];
|
||||
@@ -446,7 +342,12 @@ export default defineComponent({
|
||||
return collectionsStore.getCollection(relation.value.collection_one);
|
||||
});
|
||||
|
||||
return { relation, relatedCollection };
|
||||
const { collection } = toRefs(relatedCollection.value);
|
||||
const { primaryKeyField: relatedPrimaryKeyField } = useCollection(
|
||||
collection as Ref<string>
|
||||
);
|
||||
|
||||
return { relation, relatedCollection, relatedPrimaryKeyField };
|
||||
}
|
||||
|
||||
function useMenu() {
|
||||
@@ -482,52 +383,78 @@ export default defineComponent({
|
||||
menuActive.value = newActive;
|
||||
if (newActive === true) fetchItems();
|
||||
} else {
|
||||
startSelecting();
|
||||
selectModalActive.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useSelectionModal() {
|
||||
const active = ref(false);
|
||||
const selection = ref<any[]>([]);
|
||||
function useSelection() {
|
||||
const selectModalActive = ref(false);
|
||||
|
||||
return { active, selection, onSelect, startSelecting, stopSelecting, cancelSelecting };
|
||||
const selection = computed<(number | string)[]>(() => {
|
||||
if (!props.value) return [];
|
||||
|
||||
function onSelect(newSelection: any[]) {
|
||||
if (newSelection.length > 0) {
|
||||
selection.value = [newSelection[newSelection.length - 1]];
|
||||
} else {
|
||||
selection.value = [];
|
||||
if (
|
||||
typeof props.value === 'object' &&
|
||||
props.value.hasOwnProperty(relatedPrimaryKeyField.value.field)
|
||||
) {
|
||||
return [props.value[relatedPrimaryKeyField.value.field]];
|
||||
}
|
||||
}
|
||||
|
||||
function startSelecting() {
|
||||
active.value = true;
|
||||
|
||||
if (props.value) {
|
||||
if (
|
||||
typeof props.value === 'object' &&
|
||||
props.value.hasOwnProperty(primaryKeyField.value.field)
|
||||
) {
|
||||
selection.value = [props.value[primaryKeyField.value.field]];
|
||||
} else if (typeof props.value === 'string' || typeof props.value === 'number') {
|
||||
selection.value = [props.value];
|
||||
}
|
||||
if (typeof props.value === 'string' || typeof props.value === 'number') {
|
||||
return [props.value];
|
||||
}
|
||||
}
|
||||
|
||||
function stopSelecting() {
|
||||
if (!selection.value[0]) {
|
||||
return [];
|
||||
});
|
||||
|
||||
return { selection, stageSelection, selectModalActive };
|
||||
|
||||
function stageSelection(newSelection: (number | string)[]) {
|
||||
if (newSelection.length === 0) {
|
||||
emit('input', null);
|
||||
} else {
|
||||
emit('input', selection.value[0]);
|
||||
emit('input', newSelection[0]);
|
||||
}
|
||||
cancelSelecting();
|
||||
}
|
||||
}
|
||||
|
||||
function cancelSelecting() {
|
||||
active.value = false;
|
||||
selection.value = [];
|
||||
function useEdits() {
|
||||
const edits = computed(() => {
|
||||
// If the current value isn't a primitive, it means we've already staged some changes
|
||||
// This ensures we continue on those changes instead of starting over
|
||||
if (props.value && typeof props.value === 'object') {
|
||||
return props.value;
|
||||
}
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
return { edits, stageEdits };
|
||||
|
||||
function stageEdits(newEdits: Record<string, any>) {
|
||||
// Make sure we stage the primary key if it exists. This is needed to have the API
|
||||
// update the existing item instead of create a new one
|
||||
if (currentPrimaryKey.value && currentPrimaryKey.value !== '+') {
|
||||
emit('input', {
|
||||
[relatedPrimaryKeyField.value.field]: currentPrimaryKey.value,
|
||||
...newEdits,
|
||||
});
|
||||
} else {
|
||||
if (
|
||||
newEdits.hasOwnProperty(relatedPrimaryKeyField.value.field) &&
|
||||
newEdits[relatedPrimaryKeyField.value.field] === '+'
|
||||
) {
|
||||
delete newEdits[relatedPrimaryKeyField.value.field];
|
||||
}
|
||||
|
||||
emit('input', newEdits);
|
||||
}
|
||||
|
||||
currentItem.value = {
|
||||
...currentItem.value,
|
||||
...newEdits,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -572,8 +499,4 @@ export default defineComponent({
|
||||
.deselect:hover {
|
||||
--v-icon-color: var(--danger);
|
||||
}
|
||||
|
||||
.layout {
|
||||
--layout-offset-top: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
10
src/interfaces/one-to-many/index.ts
Normal file
10
src/interfaces/one-to-many/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineInterface } from '../define';
|
||||
import InterfaceOneToMany from './one-to-many.vue';
|
||||
|
||||
export default defineInterface(({ i18n }) => ({
|
||||
id: 'one-to-many',
|
||||
name: i18n.t('one-to-many'),
|
||||
icon: 'arrow_right_alt',
|
||||
component: InterfaceOneToMany,
|
||||
options: [],
|
||||
}));
|
||||
538
src/interfaces/one-to-many/one-to-many.vue
Normal file
538
src/interfaces/one-to-many/one-to-many.vue
Normal file
@@ -0,0 +1,538 @@
|
||||
<template>
|
||||
<v-notice warning v-if="!relation">
|
||||
{{ $t('relationship_not_setup') }}
|
||||
</v-notice>
|
||||
<div class="one-to-many" v-else>
|
||||
<v-table
|
||||
:loading="currentLoading"
|
||||
:items="currentItems"
|
||||
:headers.sync="tableHeaders"
|
||||
show-resize
|
||||
inline
|
||||
@click:row="editItem"
|
||||
>
|
||||
<template v-for="header in tableHeaders" v-slot:[`item.${header.value}`]="{ item }">
|
||||
<render-display
|
||||
:key="header.value"
|
||||
:value="item[header.value]"
|
||||
:display="header.field.display"
|
||||
:options="header.field.displayOptions"
|
||||
:interface="header.field.interface"
|
||||
:interface-options="header.field.interfaceOptions"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #item-append="{ item }">
|
||||
<v-icon
|
||||
name="close"
|
||||
v-tooltip="$t('deselect')"
|
||||
class="deselect"
|
||||
@click.stop="deselect(item)"
|
||||
/>
|
||||
</template>
|
||||
</v-table>
|
||||
|
||||
<div class="actions">
|
||||
<v-button class="new" @click="currentlyEditing = '+'">{{ $t('add_new') }}</v-button>
|
||||
<v-button class="existing" @click="selectModalActive = true">
|
||||
{{ $t('add_existing') }}
|
||||
</v-button>
|
||||
</div>
|
||||
|
||||
<modal-detail
|
||||
:active="currentlyEditing !== null"
|
||||
:collection="relatedCollection.collection"
|
||||
:primary-key="currentlyEditing || '+'"
|
||||
:edits="editsAtStart"
|
||||
@input="stageEdits"
|
||||
@update:active="cancelEdit"
|
||||
/>
|
||||
|
||||
<modal-browse
|
||||
:active.sync="selectModalActive"
|
||||
:collection="relatedCollection.collection"
|
||||
:selection="[]"
|
||||
:filters="selectionFilters"
|
||||
@input="stageSelection"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, watch, toRefs, Ref } from '@vue/composition-api';
|
||||
import api from '@/api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import useRelationsStore from '@/stores/relations';
|
||||
import useCollection from '@/composables/use-collection';
|
||||
import useCollectionsStore from '@/stores/collections';
|
||||
import useFieldsStore from '@/stores/fields';
|
||||
import ModalDetail from '@/views/private/components/modal-detail';
|
||||
import ModalBrowse from '@/views/private/components/modal-browse';
|
||||
import { Filter } from '@/stores/collection-presets/types';
|
||||
import { Header } from '@/components/v-table/types';
|
||||
|
||||
export default defineComponent({
|
||||
components: { ModalDetail, ModalBrowse },
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
primaryKey: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
field: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fields: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const projectsStore = useProjectsStore();
|
||||
const relationsStore = useRelationsStore();
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const fieldsStore = useFieldsStore();
|
||||
|
||||
const { relation, relatedCollection, relatedPrimaryKeyField } = useRelation();
|
||||
const { loading: currentLoading, items: currentItems } = useCurrent();
|
||||
const { tableHeaders } = useTable();
|
||||
const { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit } = useEdits();
|
||||
const { stageSelection, selectModalActive, selectionFilters } = useSelection();
|
||||
|
||||
return {
|
||||
currentLoading,
|
||||
currentItems,
|
||||
relation,
|
||||
tableHeaders,
|
||||
currentlyEditing,
|
||||
editItem,
|
||||
relatedCollection,
|
||||
editsAtStart,
|
||||
stageEdits,
|
||||
cancelEdit,
|
||||
stageSelection,
|
||||
selectModalActive,
|
||||
selectionFilters,
|
||||
deselect,
|
||||
};
|
||||
|
||||
/**
|
||||
* Holds info about the current relationship, like related collection, primary key field
|
||||
* of the other collection etc
|
||||
*/
|
||||
function useRelation() {
|
||||
const relation = computed(() => {
|
||||
return relationsStore.getRelationsForField(props.collection, props.field)?.[0];
|
||||
});
|
||||
|
||||
const relatedCollection = computed(() => {
|
||||
if (!relation.value) return null;
|
||||
return collectionsStore.getCollection(relation.value.collection_many);
|
||||
});
|
||||
|
||||
const { collection } = toRefs(relatedCollection.value);
|
||||
|
||||
const { primaryKeyField: relatedPrimaryKeyField } = useCollection(
|
||||
collection as Ref<string>
|
||||
);
|
||||
|
||||
return { relation, relatedCollection, relatedPrimaryKeyField };
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the current display value (the rows in the table)
|
||||
* This listens to changes in props.value to make sure we always display the correct info
|
||||
* in the table itself
|
||||
*/
|
||||
function useCurrent() {
|
||||
const loading = ref(false);
|
||||
const items = ref<any[]>([]);
|
||||
const error = ref(null);
|
||||
|
||||
// This is the primary key of the parent form, not the related items
|
||||
// By watching the primary key prop for this, it'll load the items fresh on load, but
|
||||
// also when we navigate from edit form to another edit form.
|
||||
watch(
|
||||
() => props.primaryKey,
|
||||
(newKey) => {
|
||||
if (newKey !== null && newKey !== '+' && Array.isArray(props.value) !== true) {
|
||||
fetchCurrent();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// The value can either be null (no changes), or an array of primary key / object with changes
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue) => {
|
||||
// When the value is null, there aren't any changes. It does not mean that all
|
||||
// related items are deselected
|
||||
if (newValue === null) {
|
||||
fetchCurrent();
|
||||
}
|
||||
|
||||
if (Array.isArray(newValue)) {
|
||||
mergeWithItems(newValue);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return { loading, items, error, fetchCurrent };
|
||||
|
||||
/**
|
||||
* Fetch all related items based on the primary key of the current field. This is only
|
||||
* run on first load (or when the parent form primary key changes)
|
||||
*/
|
||||
async function fetchCurrent() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
loading.value = true;
|
||||
|
||||
let fields = props.fields.split(',');
|
||||
|
||||
if (fields.includes(relatedPrimaryKeyField.value.field) === false) {
|
||||
fields.push(relatedPrimaryKeyField.value.field);
|
||||
}
|
||||
|
||||
// We're fetching these fields nested on the current item, so nest them in the current
|
||||
// field-key
|
||||
fields = fields.map((fieldKey) => `${props.field}.${fieldKey}`);
|
||||
|
||||
try {
|
||||
const response = await api.get(
|
||||
`/${currentProjectKey}/items/${props.collection}/${props.primaryKey}`,
|
||||
{
|
||||
params: {
|
||||
fields: fields,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
items.value = response.data.data[props.field];
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges all changes / newly selected items with the current value array, so we can
|
||||
* display the most up to date information in the table. This will merge edits with the
|
||||
* existing items, and fetch the full item info when the item is newly selected (as it
|
||||
* will only have a pk in the array of changes)
|
||||
*/
|
||||
async function mergeWithItems(changes: any[]) {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
loading.value = true;
|
||||
|
||||
const pkField = relatedPrimaryKeyField.value.field;
|
||||
|
||||
const updatedItems = items.value
|
||||
.map((item: any) => {
|
||||
const changeForThisItem = changes.find(
|
||||
(change) => change[pkField] === item[pkField]
|
||||
);
|
||||
|
||||
if (changeForThisItem) {
|
||||
return {
|
||||
...item,
|
||||
...changeForThisItem,
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item.hasOwnProperty(pkField))
|
||||
.filter((item) => item[relation.value.field_many] !== null);
|
||||
|
||||
const newlyAddedItems = changes.filter(
|
||||
(change) =>
|
||||
typeof change !== 'string' &&
|
||||
typeof change !== 'number' &&
|
||||
change.hasOwnProperty(pkField) === false
|
||||
);
|
||||
|
||||
const selectedPrimaryKeys = changes
|
||||
.filter((change) => typeof change === 'string' || typeof change === 'number')
|
||||
.filter((primaryKey) => {
|
||||
const isAlsoUpdate = updatedItems.some(
|
||||
(update) => update[pkField] === primaryKey
|
||||
);
|
||||
|
||||
return isAlsoUpdate === false;
|
||||
});
|
||||
|
||||
let selectedItems: any[] = [];
|
||||
|
||||
if (selectedPrimaryKeys.length > 0) {
|
||||
const fields = props.fields.split(',');
|
||||
|
||||
if (fields.includes(relatedPrimaryKeyField.value.field) === false) {
|
||||
fields.push(relatedPrimaryKeyField.value.field);
|
||||
}
|
||||
|
||||
const response = await api.get(
|
||||
`/${currentProjectKey}/items/${
|
||||
relatedCollection.value.collection
|
||||
}/${selectedPrimaryKeys.join(',')}`,
|
||||
{
|
||||
params: {
|
||||
fields: fields,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (Array.isArray(response.data.data)) {
|
||||
selectedItems = response.data.data;
|
||||
} else {
|
||||
selectedItems = [response.data.data];
|
||||
}
|
||||
}
|
||||
|
||||
items.value = [...updatedItems, ...newlyAddedItems, ...selectedItems];
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function useTable() {
|
||||
// Using a ref for the table headers here means that the table itself can update the
|
||||
// values if it needs to. This allows the user to manually resize the columns for example
|
||||
const tableHeaders = ref<Header[]>([]);
|
||||
|
||||
// Seeing we don't care about saving those tableHeaders, we can reset it whenever the
|
||||
// fields prop changes (most likely when we're navigating to a different o2m context)
|
||||
watch(
|
||||
() => props.fields,
|
||||
() => {
|
||||
tableHeaders.value = props.fields
|
||||
.split(',')
|
||||
.map((fieldKey) => {
|
||||
const field = fieldsStore.getField(
|
||||
relatedCollection.value.collection,
|
||||
fieldKey
|
||||
);
|
||||
|
||||
if (!field) return null;
|
||||
|
||||
const header: Header = {
|
||||
text: field.name,
|
||||
value: fieldKey,
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
width: null,
|
||||
field: {
|
||||
display: field.display,
|
||||
displayOptions: field.display_options,
|
||||
interface: field.interface,
|
||||
interfaceOptions: field.options,
|
||||
},
|
||||
};
|
||||
|
||||
return header;
|
||||
})
|
||||
.filter((h) => h) as Header[];
|
||||
}
|
||||
);
|
||||
|
||||
return { tableHeaders };
|
||||
}
|
||||
|
||||
function useEdits() {
|
||||
// Primary key of the item we're currently editing. If null, the edit modal should be
|
||||
// closed
|
||||
const currentlyEditing = ref<string | number>(null);
|
||||
|
||||
// This keeps track of the starting values so we can match with it
|
||||
const editsAtStart = ref({});
|
||||
|
||||
return { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit };
|
||||
|
||||
function editItem(item: any) {
|
||||
const primaryKey = item[relatedPrimaryKeyField.value.field];
|
||||
|
||||
// When the currently staged value is an array, we know we made changes / added / removed
|
||||
// certain items. In that case, we have to extract the previously made edits so we can
|
||||
// keep moving forwards with those
|
||||
if (props.value && Array.isArray(props.value)) {
|
||||
const existingEdits = props.value.find((existingChange) => {
|
||||
const existingPK = existingChange[relatedPrimaryKeyField.value.field];
|
||||
if (!existingPK) return item === existingChange;
|
||||
return existingPK === primaryKey;
|
||||
});
|
||||
|
||||
if (existingEdits) {
|
||||
editsAtStart.value = existingEdits;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the edits have the primary key included, otherwise the api will create
|
||||
// the item as a new one instead of update the existing
|
||||
if (
|
||||
primaryKey &&
|
||||
editsAtStart.value.hasOwnProperty(relatedPrimaryKeyField.value.field) === false
|
||||
) {
|
||||
editsAtStart.value = {
|
||||
...editsAtStart.value,
|
||||
[relatedPrimaryKeyField.value.field]: primaryKey,
|
||||
};
|
||||
}
|
||||
|
||||
currentlyEditing.value = primaryKey;
|
||||
}
|
||||
|
||||
function stageEdits(edits: any) {
|
||||
const pkField = relatedPrimaryKeyField.value.field;
|
||||
|
||||
const hasPrimaryKey = edits.hasOwnProperty(pkField);
|
||||
|
||||
if (props.value && Array.isArray(props.value)) {
|
||||
const newValue = props.value.map((existingChange) => {
|
||||
if (
|
||||
existingChange[pkField] &&
|
||||
edits[pkField] &&
|
||||
existingChange[pkField] === edits[pkField]
|
||||
) {
|
||||
return edits;
|
||||
}
|
||||
|
||||
if (existingChange === edits[pkField]) {
|
||||
return edits;
|
||||
}
|
||||
|
||||
if (editsAtStart.value === existingChange) {
|
||||
return edits;
|
||||
}
|
||||
|
||||
return existingChange;
|
||||
});
|
||||
|
||||
if (hasPrimaryKey === false && newValue.includes(edits) === false) {
|
||||
newValue.push(edits);
|
||||
}
|
||||
|
||||
emit('input', newValue);
|
||||
} else {
|
||||
emit('input', [edits]);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editsAtStart.value = {};
|
||||
currentlyEditing.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function useSelection() {
|
||||
const selectModalActive = ref(false);
|
||||
|
||||
const selectedPrimaryKeys = computed<(number | string)[]>(() => {
|
||||
if (!currentItems.value) return [];
|
||||
const pkField = relatedPrimaryKeyField.value.field;
|
||||
return currentItems.value
|
||||
.filter((currentItem) => currentItem.hasOwnProperty(pkField))
|
||||
.map((currentItem) => currentItem[pkField]);
|
||||
});
|
||||
|
||||
const selectionFilters = computed<Filter[]>(() => {
|
||||
const filter: Filter = {
|
||||
key: 'selection',
|
||||
field: relatedPrimaryKeyField.value.field,
|
||||
operator: 'nin',
|
||||
value: selectedPrimaryKeys.value.join(','),
|
||||
};
|
||||
|
||||
return [filter];
|
||||
});
|
||||
|
||||
return { stageSelection, selectModalActive, selectionFilters };
|
||||
|
||||
function stageSelection(newSelection: (number | string)[]) {
|
||||
if (props.value && Array.isArray(props.value)) {
|
||||
emit('input', [...props.value, ...newSelection]);
|
||||
} else {
|
||||
emit('input', newSelection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deselect(item: any) {
|
||||
const pkField = relatedPrimaryKeyField.value.field;
|
||||
|
||||
const itemPrimaryKey = item[pkField];
|
||||
|
||||
if (itemPrimaryKey) {
|
||||
if (props.value && Array.isArray(props.value)) {
|
||||
const itemHasEdits =
|
||||
props.value.find((stagedItem) => stagedItem[pkField] === itemPrimaryKey) !==
|
||||
undefined;
|
||||
|
||||
if (itemHasEdits) {
|
||||
emit(
|
||||
'input',
|
||||
props.value.map((stagedValue) => {
|
||||
if (stagedValue[pkField] === itemPrimaryKey) {
|
||||
return {
|
||||
[pkField]: itemPrimaryKey,
|
||||
[relation.value.field_many]: null,
|
||||
};
|
||||
}
|
||||
|
||||
return stagedValue;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
emit('input', [
|
||||
...props.value,
|
||||
{
|
||||
[pkField]: itemPrimaryKey,
|
||||
[relation.value.field_many]: null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
emit('input', [
|
||||
{
|
||||
[pkField]: itemPrimaryKey,
|
||||
[relation.value.field_many]: null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// If the edited item doesn't have a primary key, it's new. In that case, filtering
|
||||
// it out of props.value should be enough to remove it
|
||||
emit(
|
||||
'input',
|
||||
props.value.filter((stagedValue) => stagedValue !== item)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.existing {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.deselect {
|
||||
--v-icon-color: var(--foreground-subdued);
|
||||
|
||||
&:hover {
|
||||
--v-icon-color: var(--danger);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -138,6 +138,8 @@
|
||||
"save_and_stay": "Save and Stay",
|
||||
"save_as_copy": "Save as Copy",
|
||||
|
||||
"add_existing": "Add Existing",
|
||||
|
||||
"comments": "Comments",
|
||||
|
||||
"select_item": "Select Item",
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
<activity-navigation />
|
||||
</template>
|
||||
|
||||
<v-form collection="directus_activity" :loading="loading" :initial-values="item" />
|
||||
<v-form
|
||||
collection="directus_activity"
|
||||
:loading="loading"
|
||||
:initial-values="item"
|
||||
:primary-key="primaryKey"
|
||||
/>
|
||||
</private-view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
:initial-values="item"
|
||||
:collection="collection"
|
||||
:batch-mode="isBatch"
|
||||
:primary-key="primaryKey"
|
||||
v-model="edits"
|
||||
/>
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
:initial-values="item"
|
||||
collection="directus_files"
|
||||
:batch-mode="isBatch"
|
||||
:primary-key="primaryKey"
|
||||
v-model="edits"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<v-tab-item value="advanced">
|
||||
<h2 class="title" v-if="isNew">{{ $t('advanced_options_title') }}</h2>
|
||||
|
||||
<v-form :initial-values="existingField" v-model="_edits" :fields="fields" />
|
||||
<v-form :initial-values="existingField" v-model="_edits" :fields="fields" primary-key="+" />
|
||||
</v-tab-item>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
Array.isArray(selectedDisplay.options)
|
||||
"
|
||||
:fields="selectedDisplay.options"
|
||||
primary-key="+"
|
||||
v-model="_options"
|
||||
/>
|
||||
</transition-expand>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
Array.isArray(selectedInterface.options)
|
||||
"
|
||||
:fields="selectedInterface.options"
|
||||
primary-key="+"
|
||||
v-model="_options"
|
||||
/>
|
||||
</transition-expand>
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
:loading="loading"
|
||||
:initial-values="item"
|
||||
:batch-mode="isBatch"
|
||||
:primary-key="collection"
|
||||
v-model="edits"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
</template>
|
||||
|
||||
<div class="settings">
|
||||
<v-form :initial-values="initialValues" v-model="edits" :fields="fields" />
|
||||
<v-form
|
||||
:initial-values="initialValues"
|
||||
v-model="edits"
|
||||
:fields="fields"
|
||||
:primary-key="1"
|
||||
/>
|
||||
</div>
|
||||
</private-view>
|
||||
</template>
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
:fields="fields"
|
||||
:loading="loading"
|
||||
:initial-values="initialValues"
|
||||
:primary-key="id"
|
||||
v-model="edits"
|
||||
/>
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
:initial-values="item"
|
||||
collection="directus_webhooks"
|
||||
:batch-mode="isBatch"
|
||||
:primary-key="primaryKey"
|
||||
v-model="edits"
|
||||
/>
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
:initial-values="item"
|
||||
collection="directus_users"
|
||||
:batch-mode="isBatch"
|
||||
:primary-key="primaryKey"
|
||||
v-model="edits"
|
||||
/>
|
||||
|
||||
|
||||
@@ -65,7 +65,11 @@ export const useFieldsStore = createStore({
|
||||
* between settings and regular collections.
|
||||
*/
|
||||
|
||||
const settingsResponse = await api.get(`/${currentProjectKey}/settings/fields`);
|
||||
const settingsResponse = await api.get(`/${currentProjectKey}/settings/fields`, {
|
||||
params: {
|
||||
limit: -1,
|
||||
},
|
||||
});
|
||||
fields.push(...settingsResponse.data.data);
|
||||
|
||||
/**
|
||||
|
||||
4
src/views/private/components/modal-browse/index.ts
Normal file
4
src/views/private/components/modal-browse/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import ModalBrowse from './modal-browse.vue';
|
||||
|
||||
export { ModalBrowse };
|
||||
export default ModalBrowse;
|
||||
121
src/views/private/components/modal-browse/modal-browse.vue
Normal file
121
src/views/private/components/modal-browse/modal-browse.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<v-modal v-model="_active" :title="$t('select_item')" no-padding>
|
||||
<layout-tabular
|
||||
class="layout"
|
||||
:collection="collection"
|
||||
:selection="_selection"
|
||||
:filters="filters"
|
||||
@update:selection="onSelect"
|
||||
select-mode
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<v-button @click="cancel" secondary>{{ $t('cancel') }}</v-button>
|
||||
<v-button @click="save">{{ $t('save') }}</v-button>
|
||||
</template>
|
||||
</v-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, computed } from '@vue/composition-api';
|
||||
import { Filter } from '@/stores/collection-presets/types';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
selection: {
|
||||
type: Array as PropType<(number | string)[]>,
|
||||
default: () => [],
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
filters: {
|
||||
type: Array as PropType<Filter[]>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const { save, cancel } = useActions();
|
||||
const { _active } = useActiveState();
|
||||
const { _selection, onSelect } = useSelection();
|
||||
|
||||
return { save, cancel, _active, _selection, onSelect };
|
||||
|
||||
function useActiveState() {
|
||||
const localActive = ref(false);
|
||||
|
||||
const _active = computed({
|
||||
get() {
|
||||
return props.active === undefined ? localActive.value : props.active;
|
||||
},
|
||||
set(newActive: boolean) {
|
||||
localActive.value = newActive;
|
||||
emit('update:active', newActive);
|
||||
},
|
||||
});
|
||||
|
||||
return { _active };
|
||||
}
|
||||
|
||||
function useSelection() {
|
||||
const localSelection = ref<(string | number)[]>(null);
|
||||
|
||||
const _selection = computed({
|
||||
get() {
|
||||
if (localSelection.value === null) {
|
||||
return props.selection;
|
||||
}
|
||||
|
||||
return localSelection.value;
|
||||
},
|
||||
set(newSelection: (string | number)[]) {
|
||||
localSelection.value = newSelection;
|
||||
},
|
||||
});
|
||||
|
||||
return { _selection, onSelect };
|
||||
|
||||
function onSelect(newSelection: (string | number)[]) {
|
||||
if (newSelection.length === 0) {
|
||||
localSelection.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.multiple === true) {
|
||||
localSelection.value = newSelection;
|
||||
} else {
|
||||
localSelection.value = [newSelection[newSelection.length - 1]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useActions() {
|
||||
return { save, cancel };
|
||||
|
||||
function save() {
|
||||
emit('input', _selection.value);
|
||||
_active.value = false;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
_active.value = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout {
|
||||
--layout-offset-top: 0px;
|
||||
}
|
||||
</style>
|
||||
4
src/views/private/components/modal-detail/index.ts
Normal file
4
src/views/private/components/modal-detail/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import ModalDetail from './modal-detail.vue';
|
||||
|
||||
export { ModalDetail };
|
||||
export default ModalDetail;
|
||||
144
src/views/private/components/modal-detail/modal-detail.vue
Normal file
144
src/views/private/components/modal-detail/modal-detail.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<v-modal v-model="_active" :title="$t('editing_in', { collection })" persistent>
|
||||
<v-form
|
||||
:loading="loading"
|
||||
:initial-values="item"
|
||||
:collection="collection"
|
||||
:primary-key="primaryKey"
|
||||
v-model="_edits"
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<v-button @click="cancel" secondary>{{ $t('cancel') }}</v-button>
|
||||
<v-button @click="save">{{ $t('save') }}</v-button>
|
||||
</template>
|
||||
</v-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, PropType, watch } from '@vue/composition-api';
|
||||
import api from '@/api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
|
||||
export default defineComponent({
|
||||
model: {
|
||||
prop: 'edits',
|
||||
},
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
primaryKey: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
edits: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const { _active } = useActiveState();
|
||||
const { _edits, loading, error, item } = useItem();
|
||||
const { save, cancel } = useActions();
|
||||
|
||||
return { _active, _edits, loading, error, item, save, cancel };
|
||||
|
||||
function useActiveState() {
|
||||
const localActive = ref(false);
|
||||
|
||||
const _active = computed({
|
||||
get() {
|
||||
return props.active === undefined ? localActive.value : props.active;
|
||||
},
|
||||
set(newActive: boolean) {
|
||||
localActive.value = newActive;
|
||||
emit('update:active', newActive);
|
||||
},
|
||||
});
|
||||
|
||||
return { _active };
|
||||
}
|
||||
|
||||
function useItem() {
|
||||
const localEdits = ref<Record<string, any>>({});
|
||||
|
||||
const _edits = computed<Record<string, any>>({
|
||||
get() {
|
||||
if (props.edits !== undefined) {
|
||||
return {
|
||||
...props.edits,
|
||||
...localEdits.value,
|
||||
};
|
||||
}
|
||||
|
||||
return localEdits.value;
|
||||
},
|
||||
set(newEdits) {
|
||||
localEdits.value = newEdits;
|
||||
},
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const item = ref<Record<string, any>>(null);
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
(isActive) => {
|
||||
if (isActive === true) {
|
||||
if (props.primaryKey !== '+') fetchItem();
|
||||
} else {
|
||||
loading.value = false;
|
||||
error.value = null;
|
||||
item.value = null;
|
||||
localEdits.value = {};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return { _edits, loading, error, item, fetchItem };
|
||||
|
||||
async function fetchItem() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(
|
||||
`/${currentProjectKey}/items/${props.collection}/${props.primaryKey}`
|
||||
);
|
||||
|
||||
item.value = response.data.data;
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useActions() {
|
||||
return { save, cancel };
|
||||
|
||||
function save() {
|
||||
emit('input', _edits.value);
|
||||
_active.value = false;
|
||||
_edits.value = {};
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
_active.value = false;
|
||||
_edits.value = {};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<value-null v-if="value === null || value === undefined" />
|
||||
<span v-else-if="!displayInfo">{{ value }}</span>
|
||||
<span v-else-if="displayInfo === null">{{ value }}</span>
|
||||
<span v-else-if="typeof displayInfo.handler === 'function'">
|
||||
{{ display.handler(value, options) }}
|
||||
</span>
|
||||
@@ -15,7 +15,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import displays from '@/displays';
|
||||
import ValueNull from '@/views/private/components/value-null';
|
||||
|
||||
@@ -44,7 +44,9 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const displayInfo = displays.find((display) => display.id === props.display) || null;
|
||||
const displayInfo = computed(
|
||||
() => displays.find((display) => display.id === props.display) || null
|
||||
);
|
||||
return { displayInfo };
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user