Add functions support to the app + add count function (#12488)

* Rename date functions to fn, add json_array_length for pg

* Add json count to mssql

* Add json array count support to other vendors

* Add UI for selecting API functions

* Make it not break

* Render functions in filter preview better

* Include functions in field altering

* Add schema access to database helper

* Allow filtering against o2m/m2m/m2a count

* Add data function execution helpers in utils

* Fix type issue

* Inject function results in validate step

* Render field keys with function names translated

* Allow selecting nested/functions in field validation step

* Make sure number comparisons are treated as numbers

* Add check if instanceof date when casting to a Number

* Prevent selecting foreign keys for junction sort (#12463)

* [SDK] Add further request options to `items` functions (#12503)

* add possibility to set further options to the request

* fix options type

* add typings to interface

* add test if headers are passed thourght

* create reusable options param

* set higher priority to options param

* Small stylistic cleanup

Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com>
Co-authored-by: ian <licitdev@gmail.com>
Co-authored-by: Jürg Hunziker <juerg.hunziker@gmail.com>
This commit is contained in:
Rijk van Zanten
2022-03-31 16:56:26 -04:00
committed by GitHub
parent abcbc7ffcb
commit 90f5b0a471
40 changed files with 876 additions and 342 deletions

View File

@@ -16,19 +16,19 @@
<div v-if="filterInfo[index].isField" block class="node field">
<div class="header" :class="{ inline }">
<v-icon name="drag_indicator" class="drag-handle" small></v-icon>
<v-menu placement="bottom-start" show-arrow :disabled="!!field === false">
<v-menu placement="bottom-start" show-arrow>
<template #activator="{ toggle }">
<button
class="name"
:disabled="!!field === false"
:class="{ disabled: !!field === false }"
@click="toggle"
>
<button class="name" @click="toggle">
<span>{{ getFieldPreview(element) }}</span>
</button>
</template>
<v-field-list :collection="collection" @select-field="updateField(index, $event)" />
<v-field-list
:collection="collection"
:field="field"
include-functions
@select-field="updateField(index, $event)"
/>
</v-menu>
<v-select
inline
@@ -96,18 +96,25 @@
</template>
<script lang="ts" setup>
import { useFieldTree } from '@/composables/use-field-tree';
import { computed, toRefs } from 'vue';
import InputGroup from './input-group.vue';
import Draggable from 'vuedraggable';
import { useFieldsStore } from '@/stores';
import { useI18n } from 'vue-i18n';
import { getFilterOperatorsForType } from '@directus/shared/utils';
import { get } from 'lodash';
import { FieldFilter, Filter, FieldFilterOperator, LogicalFilterAND, LogicalFilterOR } from '@directus/shared/types';
import { extractFieldFromFunction } from '@/utils/extract-field-from-function';
import { useSync } from '@directus/shared/composables';
import { fieldToFilter, getField, getNodeName, getComparator } from './utils';
import { toArray } from '@directus/shared/utils';
import {
FieldFilter,
FieldFilterOperator,
Filter,
LogicalFilterAND,
LogicalFilterOR,
Type,
} from '@directus/shared/types';
import { getFilterOperatorsForType, getOutputTypeForFunction, toArray } from '@directus/shared/utils';
import { get } from 'lodash';
import { computed, toRefs } from 'vue';
import { useI18n } from 'vue-i18n';
import Draggable from 'vuedraggable';
import InputGroup from './input-group.vue';
import { fieldToFilter, getComparator, getField, getNodeName } from './utils';
import { getFunctionsForType } from '@directus/shared/utils';
type FilterInfo =
| {
@@ -145,7 +152,6 @@ const emit = defineEmits(['remove-node', 'update:filter', 'change']);
const { collection } = toRefs(props);
const filterSync = useSync(props, 'filter', emit);
const { treeList: fieldOptions, loadFieldRelations } = useFieldTree(collection);
const fieldsStore = useFieldsStore();
const { t } = useI18n();
@@ -181,9 +187,27 @@ function getFieldPreview(node: Record<string, any>) {
const fieldParts = fieldKey.split('.');
const fieldNames = fieldParts.map((fieldKey, index) => {
const hasFunction = fieldKey.includes('(') && fieldKey.includes(')');
let key = fieldKey;
let functionName;
if (hasFunction) {
const { field, fn } = extractFieldFromFunction(fieldKey);
functionName = fn;
key = field;
}
const pathPrefix = fieldParts.slice(0, index);
const field = fieldsStore.getField(props.collection, [...pathPrefix, fieldKey].join('.'));
return field?.name ?? fieldKey;
const field = fieldsStore.getField(props.collection, [...pathPrefix, key].join('.'));
const name = field?.name ?? key;
if (hasFunction) {
return t(`functions.${functionName}`) + ` (${name})`;
}
return name;
});
return fieldNames.join(' -> ');
@@ -294,10 +318,17 @@ function replaceNode(index: number, newFilter: Filter) {
}
function getCompareOptions(name: string) {
const fieldInfo = fieldsStore.getField(props.collection, name);
if (fieldInfo === null) return [];
let type: Type;
return getFilterOperatorsForType(fieldInfo.type, { includeValidation: true }).map((type) => ({
if (name.includes('(') && name.includes(')')) {
const functionName = name.split('(')[0];
type = getOutputTypeForFunction(functionName);
} else {
const fieldInfo = fieldsStore.getField(props.collection, name);
type = fieldInfo?.type || 'unknown';
}
return getFilterOperatorsForType(type, { includeValidation: true }).map((type) => ({
text: t(`operators.${type}`),
value: `_${type}`,
}));

View File

@@ -38,7 +38,7 @@
</button>
</template>
<v-field-list :collection="collection" @select-field="addNode($event)">
<v-field-list :collection="collection" include-functions @select-field="addNode($event)">
<template #prepend>
<v-list-item clickable @click="addNode('$group')">
<v-list-item-content>
@@ -54,14 +54,14 @@
</template>
<script lang="ts" setup>
import { get, set, isEmpty, cloneDeep } from 'lodash';
import { useFieldsStore } from '@/stores';
import { Filter, Type } from '@directus/shared/types';
import { getFilterOperatorsForType, getOutputTypeForFunction } from '@directus/shared/utils';
import { cloneDeep, get, isEmpty, set } from 'lodash';
import { computed, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { Filter } from '@directus/shared/types';
import Nodes from './nodes.vue';
import { getNodeName } from './utils';
import { getFilterOperatorsForType } from '@directus/shared/utils';
import { useFieldsStore } from '@/stores';
interface Props {
value?: Record<string, any>;
@@ -129,8 +129,16 @@ function addNode(key: string) {
if (key === '$group') {
innerValue.value = innerValue.value.concat({ _and: [] });
} else {
const field = fieldsStore.getField(collection.value, key)!;
const operator = getFilterOperatorsForType(field.type)[0];
let type: Type;
if (key.includes('(') && key.includes(')')) {
const functionName = key.split('(')[0];
type = getOutputTypeForFunction(functionName);
} else {
const field = fieldsStore.getField(collection.value, key)!;
type = field?.type || 'unknown';
}
const operator = getFilterOperatorsForType(type)[0];
const node = set({}, key, { ['_' + operator]: null });
innerValue.value = innerValue.value.concat(node);
}