Merge branch 'main' into aggregation

This commit is contained in:
rijkvanzanten
2021-06-18 16:37:39 -04:00
10 changed files with 644 additions and 29 deletions

View File

@@ -192,6 +192,6 @@ fields:
- field: item_duplication_fields
special:
- json
interface: code
interface: system-field-tree
options:
language: JSON
collectionField: collection

View File

@@ -15,6 +15,7 @@ import VBreadcrumb from './v-breadcrumb';
import VButton from './v-button/';
import VCard, { VCardActions, VCardSubtitle, VCardText, VCardTitle } from './v-card';
import VCheckbox from './v-checkbox/';
import VCheckboxTree from './v-checkbox-tree/';
import VChip from './v-chip/';
import VDetail from './v-detail';
import VDialog from './v-dialog';
@@ -60,6 +61,7 @@ export function registerComponents(app: App): void {
app.component('v-card-title', VCardTitle);
app.component('v-card', VCard);
app.component('v-checkbox', VCheckbox);
app.component('v-checkbox-tree', VCheckboxTree);
app.component('v-chip', VChip);
app.component('v-detail', VDetail);
app.component('v-dialog', VDialog);

View File

@@ -0,0 +1,4 @@
import VCheckboxTree from './v-checkbox-tree.vue';
export { VCheckboxTree };
export default VCheckboxTree;

View File

@@ -0,0 +1,418 @@
<template>
<v-list-group v-if="children" v-show="visibleChildrenValues.length > 0" :value="value">
<template #activator>
<v-checkbox
:indeterminate="groupIndeterminateState"
:checked="groupCheckedStateOverride"
:label="text"
:value="value"
v-model="treeValue"
/>
</template>
<v-checkbox-tree-checkbox
v-for="choice in children"
:key="choice[itemValue]"
:value-combining="valueCombining"
:checked="childrenCheckedStateOverride"
:hidden="visibleChildrenValues.includes(choice[itemValue]) === false"
:search="search"
:item-text="itemText"
:item-value="itemValue"
:item-children="itemChildren"
:text="choice[itemText]"
:value="choice[itemValue]"
:children="choice[itemChildren]"
v-model="treeValue"
/>
</v-list-group>
<v-list-item v-else-if="!children && !hidden">
<v-checkbox :checked="checked" :label="text" :value="value" v-model="treeValue" />
</v-list-item>
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from 'vue';
import { difference } from 'lodash';
type Delta = {
added?: (number | string)[];
removed?: (number | string)[];
};
export default defineComponent({
name: 'v-checkbox-tree-checkbox',
props: {
text: {
type: String,
required: true,
},
value: {
type: [String, Number],
required: true,
},
children: {
type: Array as PropType<Record<string, any>[]>,
default: null,
},
modelValue: {
type: Array as PropType<(string | number)[]>,
default: () => [],
},
valueCombining: {
type: String as PropType<'all' | 'branch' | 'leaf' | 'indeterminate' | 'exclusive'>,
required: true,
},
checked: {
type: Boolean,
default: null,
},
search: {
type: String,
default: null,
},
hidden: {
type: Boolean,
default: false,
},
itemText: {
type: String,
default: 'text',
},
itemValue: {
type: String,
default: 'value',
},
itemChildren: {
type: String,
default: 'children',
},
},
setup(props, { emit }) {
const visibleChildrenValues = computed(() => {
if (!props.search) return props.children?.map((child) => child[props.itemValue]);
return props.children
?.filter((child) => child[props.itemText].toLowerCase().includes(props.search.toLowerCase()))
?.map((child) => child[props.itemValue]);
});
const childrenValues = computed(() => props.children?.map((child) => child[props.itemValue]) || []);
const treeValue = computed({
get() {
return props.modelValue || [];
},
set(newValue: (string | number)[]) {
const added = difference(newValue, props.modelValue);
const removed = difference(props.modelValue, newValue);
if (props.children) {
switch (props.valueCombining) {
case 'all':
return emitAll(newValue, { added, removed });
case 'branch':
return emitBranch(newValue, { added, removed });
case 'leaf':
return emitLeaf(newValue, { added, removed });
case 'indeterminate':
return emitIndeterminate(newValue, { added, removed });
case 'exclusive':
return emitExclusive(newValue, { added, removed });
default:
return emitValue(newValue);
}
}
emitValue(newValue);
},
});
const groupCheckedStateOverride = computed(() => {
if (props.checked !== null) return props.checked;
if (props.valueCombining === 'all') return null;
if (props.valueCombining === 'leaf') {
const leafChildrenRecursive = getRecursiveChildrenValues('leaf');
return leafChildrenRecursive.every((childVal) => props.modelValue.includes(childVal));
}
return null;
});
const groupIndeterminateState = computed(() => {
const allChildrenValues = getRecursiveChildrenValues('all');
if (props.valueCombining === 'all' || props.valueCombining === 'branch') {
return (
allChildrenValues.some((childVal) => props.modelValue.includes(childVal)) &&
props.modelValue.includes(props.value) === false
);
}
if (props.valueCombining === 'indeterminate') {
return (
allChildrenValues.some((childVal) => props.modelValue.includes(childVal)) &&
allChildrenValues.every((childVal) => props.modelValue.includes(childVal)) === false
);
}
if (props.valueCombining === 'leaf') {
const leafChildrenRecursive = getRecursiveChildrenValues('leaf');
return (
leafChildrenRecursive.some((childVal) => props.modelValue.includes(childVal)) &&
leafChildrenRecursive.every((childVal) => props.modelValue.includes(childVal)) === false
);
}
if (props.valueCombining === 'exclusive') {
return allChildrenValues.some((childVal) => props.modelValue.includes(childVal));
}
return null;
});
const childrenCheckedStateOverride = computed(() => {
if (props.checked !== null) return props.checked;
if (props.valueCombining === 'all') return null;
if (props.valueCombining === 'branch') {
if (props.modelValue.includes(props.value)) return true;
}
return null;
});
return {
groupCheckedStateOverride,
childrenCheckedStateOverride,
treeValue,
groupIndeterminateState,
visibleChildrenValues,
};
function emitAll(rawValue: (string | number)[], { added, removed }: Delta) {
const childrenValuesRecursive = getRecursiveChildrenValues('all');
// When enabling the group level
if (added?.[0] === props.value) {
const newValue = [
...rawValue.filter((val) => val !== props.value && childrenValues.value.includes(val) === false),
...childrenValuesRecursive,
props.value,
];
return emitValue(newValue);
}
// When disabling the group level
if (removed?.[0] === props.value) {
const newValue = rawValue.filter(
(val) => val !== props.value && childrenValuesRecursive.includes(val) === false
);
return emitValue(newValue);
}
// When all children are clicked
if (childrenValues.value.every((childVal) => rawValue.includes(childVal))) {
const newValue = [
...rawValue.filter((val) => val !== props.value && childrenValuesRecursive.includes(val) === false),
...childrenValuesRecursive,
props.value,
];
return emitValue(newValue);
}
const newValue = rawValue.filter((val) => val !== props.value);
return emitValue(newValue);
}
function emitBranch(rawValue: (string | number)[], { added, removed }: Delta) {
const allChildrenRecursive = getRecursiveChildrenValues('all');
// When clicking on an individual item in the enabled group
if (
(props.modelValue.includes(props.value) || props.checked === true) &&
added &&
childrenValues.value.includes(added?.[0])
) {
const newValue = [
...rawValue.filter((val) => val !== props.value && val !== added?.[0]),
...childrenValues.value.filter((childVal) => childVal !== added?.[0]),
];
return emitValue(newValue);
}
// When a childgroup is modified
if (
props.modelValue.includes(props.value) &&
allChildrenRecursive.some((childVal) => rawValue.includes(childVal))
) {
const newValue = [
...rawValue.filter((val) => val !== props.value),
...(props.children || [])
.filter((child) => {
if (!child[props.itemChildren]) return true;
const childNestedValues = getRecursiveChildrenValues('all', child[props.itemChildren]);
return rawValue.some((rawVal) => childNestedValues.includes(rawVal)) === false;
})
.map((child) => child[props.itemValue]),
];
return emitValue(newValue);
}
// When enabling the group level
if (added?.includes(props.value)) {
const newValue = [
...rawValue.filter((val) => val !== props.value && allChildrenRecursive.includes(val) === false),
props.value,
];
return emitValue(newValue);
}
// When disabling the group level
if (removed?.includes(props.value)) {
const newValue = rawValue.filter((val) => val !== props.value && allChildrenRecursive.includes(val) === false);
return emitValue(newValue);
}
// When all children are clicked
if (childrenValues.value.every((childVal) => rawValue.includes(childVal))) {
const newValue = [
...rawValue.filter((val) => val !== props.value && allChildrenRecursive.includes(val) === false),
props.value,
];
return emitValue(newValue);
}
return emitValue(rawValue);
}
function emitLeaf(rawValue: (string | number)[], { added, removed }: Delta) {
const allChildrenRecursive = getRecursiveChildrenValues('all');
const leafChildrenRecursive = getRecursiveChildrenValues('leaf');
// When enabling the group level
if (added?.includes(props.value)) {
if (leafChildrenRecursive.every((childVal) => rawValue.includes(childVal))) {
const newValue = rawValue.filter(
(val) => val !== props.value && allChildrenRecursive.includes(val) === false
);
return emitValue(newValue);
} else {
const newValue = [
...rawValue.filter((val) => val !== props.value && allChildrenRecursive.includes(val) === false),
...leafChildrenRecursive,
];
return emitValue(newValue);
}
}
return emitValue(rawValue);
}
function emitIndeterminate(rawValue: (string | number)[], { added, removed }: Delta) {
const childrenValuesRecursive = getRecursiveChildrenValues('all');
// When enabling the group level
if (added?.[0] === props.value) {
const newValue = [
...rawValue.filter((val) => val !== props.value && childrenValues.value.includes(val) === false),
...childrenValuesRecursive,
props.value,
];
return emitValue(newValue);
}
// When disabling the group level
if (removed?.[0] === props.value) {
const newValue = rawValue.filter(
(val) => val !== props.value && childrenValuesRecursive.includes(val) === false
);
return emitValue(newValue);
}
// When a child value is clicked
if (childrenValues.value.some((childVal) => rawValue.includes(childVal))) {
const newValue = [...rawValue.filter((val) => val !== props.value), props.value];
return emitValue(newValue);
}
// When no children are clicked
if (childrenValues.value.every((childVal) => rawValue.includes(childVal) === false)) {
return emitValue(rawValue.filter((val) => val !== props.value));
}
return emitValue(rawValue);
}
function emitExclusive(rawValue: (string | number)[], { added, removed }: Delta) {
const childrenValuesRecursive = getRecursiveChildrenValues('all');
// When enabling the group level
if (added?.[0] === props.value) {
const newValue = [
...rawValue.filter((val) => val !== props.value && childrenValuesRecursive.includes(val) === false),
props.value,
];
return emitValue(newValue);
}
// When a child value is clicked
if (childrenValuesRecursive.some((childVal) => rawValue.includes(childVal))) {
const newValue = [...rawValue.filter((val) => val !== props.value)];
return emitValue(newValue);
}
return emitValue(rawValue);
}
function emitValue(newValue: (string | number)[]) {
emit('update:modelValue', newValue);
}
function getRecursiveChildrenValues(
mode: 'all' | 'branch' | 'leaf',
children: Record<string, any>[] = props.children
) {
const values: (string | number)[] = [];
getChildrenValuesRecursive(children);
return values;
function getChildrenValuesRecursive(children: Record<string, any>[]) {
if (!children) return;
for (const child of children) {
if (mode === 'all') {
values.push(child[props.itemValue]);
}
if (mode === 'branch' && child[props.itemChildren]) {
values.push(child[props.itemValue]);
}
if (mode === 'leaf' && !child[props.itemChildren]) {
values.push(child[props.itemValue]);
}
if (child[props.itemChildren]) {
getChildrenValuesRecursive(child[props.itemChildren]);
}
}
}
}
},
});
</script>

View File

@@ -0,0 +1,71 @@
<template>
<v-list :mandatory="false" v-model="openSelection">
<v-checkbox-tree-checkbox
v-for="choice in choices"
:key="choice[itemValue]"
:value-combining="valueCombining"
:search="search"
:item-text="itemText"
:item-value="itemValue"
:item-children="itemChildren"
:text="choice[itemText]"
:value="choice[itemValue]"
:children="choice[itemChildren]"
v-model="value"
/>
</v-list>
</template>
<script lang="ts">
import { computed, ref, defineComponent, PropType } from 'vue';
import VCheckboxTreeCheckbox from './v-checkbox-tree-checkbox.vue';
export default defineComponent({
name: 'v-checkbox-tree',
components: { VCheckboxTreeCheckbox },
props: {
choices: {
type: Array as PropType<Record<string, any>[]>,
default: () => [],
},
modelValue: {
type: Array as PropType<string[]>,
default: null,
},
valueCombining: {
type: String as PropType<'all' | 'branch' | 'leaf' | 'indeterminate' | 'exclusive'>,
default: 'all',
},
search: {
type: String,
default: null,
},
itemText: {
type: String,
default: 'text',
},
itemValue: {
type: String,
default: 'value',
},
itemChildren: {
type: String,
default: 'children',
},
},
setup(props, { emit }) {
const value = computed({
get() {
return props.modelValue || [];
},
set(newValue: string[]) {
emit('update:modelValue', newValue);
},
});
const openSelection = ref<(string | number)[]>([]);
return { value, openSelection };
},
});
</script>

View File

@@ -2,7 +2,7 @@
<component
:is="customValue ? 'div' : 'button'"
class="v-checkbox"
@click="toggleInput"
@click.stop="toggleInput"
type="button"
role="checkbox"
:aria-pressed="isChecked ? 'true' : 'false'"
@@ -10,10 +10,10 @@
:class="{ checked: isChecked, indeterminate, block }"
>
<div class="prepend" v-if="$slots.prepend"><slot name="prepend" /></div>
<v-icon class="checkbox" :name="icon" @click.stop="toggleInput" :disabled="disabled" />
<v-icon class="checkbox" :name="icon" :disabled="disabled" />
<span class="label type-text">
<slot v-if="customValue === false">{{ label }}</slot>
<input @click.stop class="custom-input" v-else v-model="internalValue" />
<input class="custom-input" v-else v-model="internalValue" />
</span>
<div class="append" v-if="$slots.append"><slot name="append" /></div>
</component>
@@ -66,11 +66,17 @@ export default defineComponent({
type: Boolean,
default: false,
},
checked: {
type: Boolean,
default: null,
},
},
setup(props, { emit }) {
const internalValue = useSync(props, 'value', emit);
const isChecked = computed<boolean>(() => {
if (props.checked !== null) return props.checked;
if (props.modelValue instanceof Array) {
return props.modelValue.includes(props.value);
}
@@ -93,7 +99,7 @@ export default defineComponent({
if (props.modelValue instanceof Array) {
const newValue = [...props.modelValue];
if (isChecked.value === false) {
if (props.modelValue.includes(props.value) === false) {
newValue.push(props.value);
} else {
newValue.splice(newValue.indexOf(props.value), 1);
@@ -101,7 +107,7 @@ export default defineComponent({
emit('update:modelValue', newValue);
} else {
emit('update:modelValue', !isChecked.value);
emit('update:modelValue', !props.modelValue);
}
}
},

View File

@@ -1,19 +1,21 @@
import { useFieldsStore, useRelationsStore } from '@/stores/';
import { useCollectionsStore, useFieldsStore, useRelationsStore } from '@/stores/';
import { Field, Relation } from '@/types';
import { getRelationType } from '@/utils/get-relation-type';
import { cloneDeep } from 'lodash';
import { cloneDeep, orderBy } from 'lodash';
import { computed, Ref, ComputedRef } from 'vue';
type FieldOption = { name: string; field: string; key: string; children?: FieldOption[] };
type FieldOption = { name: string; field: string; key: string; children?: FieldOption[]; group?: string };
export default function useFieldTree(
collection: Ref<string | null>,
/** Only allow m2o relations to be nested */
strict = false,
inject?: Ref<{ fields: Field[]; relations: Relation[] } | null>,
filter: (field: Field) => boolean = () => true
filter: (field: Field) => boolean = () => true,
depth = 3
): { tree: ComputedRef<FieldOption[]> } {
const fieldsStore = useFieldsStore();
const collectionsStore = useCollectionsStore();
const relationsStore = useRelationsStore();
const tree = computed(() => {
@@ -24,23 +26,27 @@ export default function useFieldTree(
return { tree };
function parseLevel(collection: string, parentPath: string | null, level = 0) {
const fieldsInLevel = [
...cloneDeep(fieldsStore.getFieldsForCollectionAlphabetical(collection)),
...(inject?.value?.fields.filter((field) => field.collection === collection) || []),
]
.filter((field: Field) => {
const shown =
field.meta?.special?.includes('alias') !== true && field.meta?.special?.includes('no-data') !== true;
return shown;
})
.filter(filter)
.map((field: Field) => ({
name: field.name,
field: field.field,
key: parentPath ? `${parentPath}.${field.field}` : field.field,
})) as FieldOption[];
const fieldsInLevel = orderBy(
[
...cloneDeep(fieldsStore.getFieldsForCollectionAlphabetical(collection)),
...(inject?.value?.fields.filter((field) => field.collection === collection) || []),
]
.filter((field: Field) => {
const shown =
field.meta?.special?.includes('alias') !== true && field.meta?.special?.includes('no-data') !== true;
return shown;
})
.filter(filter)
.map((field: Field) => ({
name: field.name,
field: field.field,
key: parentPath ? `${parentPath}.${field.field}` : field.field,
sort: field.meta?.sort,
})) as FieldOption[],
'sort'
);
if (level >= 3) return fieldsInLevel;
if (level >= depth) return fieldsInLevel;
for (const field of fieldsInLevel) {
const relations = [
@@ -65,10 +71,29 @@ export default function useFieldTree(
if (relationType === 'm2o') {
field.children = parseLevel(
relation.related_collection,
relation.related_collection!,
parentPath ? `${parentPath}.${field.field}` : field.field,
level + 1
);
} else if (relationType === 'm2a') {
field.children = [];
for (const relatedCollection of relation.meta!.one_allowed_collections!) {
const relatedCollectionName =
collectionsStore.collections.find((collection) => collection.collection === relatedCollection)?.name ||
relatedCollection;
field.children.push(
...parseLevel(
relatedCollection,
parentPath ? `${parentPath}.${field.field}:${relatedCollection}` : `${field.field}:${relatedCollection}`,
level + 1
).map((child) => ({
...child,
name: `${child.name} (${relatedCollectionName})`,
}))
);
}
} else if (strict === false) {
field.children = parseLevel(
relation.collection,

View File

@@ -158,8 +158,12 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
saving.value = true;
validationErrors.value = [];
const fields = collectionInfo.value?.meta?.item_duplication_fields || ['*'];
const itemData = await api.get(itemEndpoint.value, { params: { fields } });
const newItem: { [field: string]: any } = {
...(item.value || {}),
...(itemData.data.data || {}),
...edits.value,
};

View File

@@ -0,0 +1,12 @@
import { defineInterface } from '@/interfaces/define';
import InterfaceSystemFieldTree from './system-field-tree.vue';
export default defineInterface({
id: 'system-field-tree',
name: '$t:field',
icon: 'box',
component: InterfaceSystemFieldTree,
types: ['string'],
options: [],
system: true,
});

View File

@@ -0,0 +1,73 @@
<template>
<v-notice v-if="!collectionField && !collection" type="warning">
{{ t('collection_field_not_setup') }}
</v-notice>
<v-notice v-else-if="!chosenCollection" type="warning">
{{ t('select_a_collection') }}
</v-notice>
<div class="system-field-tree" v-else>
<v-checkbox-tree
@update:model-value="$emit('input', $event)"
:model-value="value"
:disabled="disabled"
:choices="tree"
item-text="name"
item-value="key"
value-combining="exclusive"
/>
</div>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, computed, inject, ref, PropType } from 'vue';
import { useFieldTree } from '@/composables/use-field-tree';
export default defineComponent({
emits: ['input'],
props: {
collectionField: {
type: String,
default: null,
},
collection: {
type: String,
default: null,
},
value: {
type: Array as PropType<string[]>,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: null,
},
allowNone: {
type: Boolean,
default: false,
},
},
setup(props) {
const { t } = useI18n();
const values = inject('values', ref<Record<string, any>>({}));
const chosenCollection = computed(() => values.value[props.collectionField] || props.collection);
const { tree } = useFieldTree(chosenCollection);
return { t, values, tree, chosenCollection };
},
});
</script>
<style scoped>
.system-field-tree {
border: var(--border-width) solid var(--border-normal);
border-radius: var(--border-radius);
}
</style>