diff --git a/api/src/database/helpers/geometry.ts b/api/src/database/helpers/geometry.ts index 489fd3166c..5175e05c2c 100644 --- a/api/src/database/helpers/geometry.ts +++ b/api/src/database/helpers/geometry.ts @@ -60,7 +60,7 @@ class KnexSpatial { } _intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw { const geometry = this.fromGeoJSON(geojson); - return this.knex.raw('intersects(??, ?)', [key, geometry]); + return this.knex.raw('st_intersects(??, ?)', [key, geometry]); } intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw { return this.isTrue(this._intersects_bbox(key, geojson)); diff --git a/api/src/database/migrations/20211007A-update-presets.ts b/api/src/database/migrations/20211007A-update-presets.ts new file mode 100644 index 0000000000..2cd08a7d5b --- /dev/null +++ b/api/src/database/migrations/20211007A-update-presets.ts @@ -0,0 +1,141 @@ +import { Filter, LogicalFilterAND } from '@directus/shared/types'; +import { Knex } from 'knex'; +import { nanoid } from 'nanoid'; + +type OldFilter = { + key: string; + field: string; + value: any; + operator: string; +}; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('directus_presets', (table) => { + table.json('filter'); + }); + + const presets = await knex + .select<{ id: number; filters: string | OldFilter[]; layout_query: string | Record }[]>( + 'id', + 'filters', + 'layout_query' + ) + .from('directus_presets'); + + for (const preset of presets) { + if (preset.filters) { + const oldFilters: OldFilter[] = + (typeof preset.filters === 'string' ? JSON.parse(preset.filters) : preset.filters) ?? []; + + if (oldFilters.length === 0) continue; + + const newFilter: Filter = { + _and: [], + }; + + for (const oldFilter of oldFilters) { + if (oldFilter.key === 'hide-archived') continue; + + newFilter._and.push({ + [oldFilter.field]: { + ['_' + oldFilter.operator]: oldFilter.value, + }, + }); + } + + if (newFilter._and.length > 0) { + await knex('directus_presets') + .update({ filter: JSON.stringify(newFilter) }) + .where('id', '=', preset.id); + } + } + + if (preset.layout_query) { + const layoutQuery: Record = + typeof preset.layout_query === 'string' ? JSON.parse(preset.layout_query) : preset.layout_query; + + for (const [layout, query] of Object.entries(layoutQuery)) { + if (query.sort) { + query.sort = [query.sort]; + } + + layoutQuery[layout] = query; + } + + await knex('directus_presets') + .update({ layout_query: JSON.stringify(layoutQuery) }) + .where('id', '=', preset.id); + } + } + + await knex.schema.alterTable('directus_presets', (table) => { + table.dropColumn('filters'); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('directus_presets', (table) => { + table.json('filters'); + }); + + const presets = await knex + .select<{ id: number; filter: string | OldFilter[]; layout_query: string | Record }[]>( + 'id', + 'filter', + 'layout_query' + ) + .from('directus_presets'); + + for (const preset of presets) { + if (preset.filter) { + const newFilter: LogicalFilterAND = + (typeof preset.filter === 'string' ? JSON.parse(preset.filter) : preset.filter) ?? {}; + + if (Object.keys(newFilter).length === 0) continue; + + const oldFilters: OldFilter[] = []; + + for (const filter of newFilter._and ?? []) { + const field = Object.keys(filter)?.[0]; + const operator = Object.keys(Object.values(filter)?.[0] ?? {})?.[0]; + const value = Object.values(Object.values(filter)?.[0] ?? {})?.[0]; + + if (!field || !operator || !value) continue; + + oldFilters.push({ + key: nanoid(), + field, + operator: operator.substring(1), + value, + }); + } + + if (oldFilters.length > 0) { + await knex('directus_presets') + .update({ filters: JSON.stringify(oldFilters) }) + .where('id', '=', preset.id); + } + } + + if (preset.layout_query) { + const layoutQuery: Record = + typeof preset.layout_query === 'string' ? JSON.parse(preset.layout_query) : preset.layout_query; + + for (const [layout, query] of Object.entries(layoutQuery)) { + if (query.sort && Array.isArray(query.sort)) { + query.sort = query.sort?.[0] ?? null; + } + + layoutQuery[layout] = query; + } + + await knex('directus_presets') + .update({ layout_query: JSON.stringify(layoutQuery) }) + .where('id', '=', preset.id); + } + } + + await knex.schema.alterTable('directus_presets', (table) => { + table.dropColumn('filter'); + }); +} diff --git a/api/src/database/run-ast.ts b/api/src/database/run-ast.ts index 1b6fe41fe1..0b3f6e697f 100644 --- a/api/src/database/run-ast.ts +++ b/api/src/database/run-ast.ts @@ -1,13 +1,14 @@ import { Knex } from 'knex'; import { clone, cloneDeep, pick, uniq } from 'lodash'; import { PayloadService } from '../services/payload'; -import { Item, Query, SchemaOverview } from '../types'; +import { Item, SchemaOverview } from '../types'; import { AST, FieldNode, NestedCollectionNode, M2ONode } from '../types/ast'; import { applyFunctionToColumnName } from '../utils/apply-function-to-column-name'; import applyQuery from '../utils/apply-query'; import { getColumn } from '../utils/get-column'; import { stripFunction } from '../utils/strip-function'; import { toArray } from '@directus/shared/utils'; +import { Query } from '@directus/shared/types'; import getDatabase from './index'; import { isNativeGeometry } from '../utils/geometry'; import { getGeometryHelper } from '../database/helpers/geometry'; @@ -289,7 +290,7 @@ function applyParentFilters( ...nestedNode.query[relatedCollection], filter: { _and: [ - nestedNode.query[relatedCollection].filter, + nestedNode.query[relatedCollection].filter ?? {}, { [nestedNode.relatedKey[relatedCollection]]: { _in: uniq(keysPerCollection[relatedCollection]), @@ -343,7 +344,14 @@ function mergeWithParentItems( }) .sort((a, b) => { // This is pre-filled in get-ast-from-query - const { column, order } = nestedNode.query.sort![0]!; + const sortField = nestedNode.query.sort![0]!; + let column = sortField; + let order: 'asc' | 'desc' = 'asc'; + + if (sortField.startsWith('-')) { + column = sortField.substring(1); + order = 'desc'; + } if (a[column] === b[column]) return 0; if (a[column] === null) return 1; diff --git a/api/src/database/system-data/app-access-permissions/index.ts b/api/src/database/system-data/app-access-permissions/index.ts index 498aa0ea3d..1533da5532 100644 --- a/api/src/database/system-data/app-access-permissions/index.ts +++ b/api/src/database/system-data/app-access-permissions/index.ts @@ -1,5 +1,5 @@ import { merge } from 'lodash'; -import { Permission } from '../../../types'; +import { Permission } from '@directus/shared/types'; import { requireYAML } from '../../../utils/require-yaml'; const defaults: Partial = { diff --git a/api/src/database/system-data/fields/presets.yaml b/api/src/database/system-data/fields/presets.yaml index c454de2974..06007abae8 100644 --- a/api/src/database/system-data/fields/presets.yaml +++ b/api/src/database/system-data/fields/presets.yaml @@ -1,7 +1,7 @@ table: directus_presets fields: - - field: filters + - field: filter hidden: true special: json diff --git a/api/src/services/authorization.ts b/api/src/services/authorization.ts index 8e19e4c99d..2884aac776 100644 --- a/api/src/services/authorization.ts +++ b/api/src/services/authorization.ts @@ -11,13 +11,10 @@ import { FieldNode, Item, NestedCollectionNode, - Permission, - PermissionsAction, PrimaryKey, - Query, SchemaOverview, - Aggregate, } from '../types'; +import { Query, Aggregate, Permission, PermissionsAction } from '@directus/shared/types'; import { ItemsService } from './items'; import { PayloadService } from './payload'; @@ -105,7 +102,11 @@ export class AuthorizationService { } } - function checkFields(collection: string, children: (NestedCollectionNode | FieldNode)[], aggregate?: Aggregate) { + function checkFields( + collection: string, + children: (NestedCollectionNode | FieldNode)[], + aggregate?: Aggregate | null + ) { // We check the availability of the permissions in the step before this is run const permissions = permissionsForCollections.find((permission) => permission.collection === collection)!; const allowedFields = permissions.fields || []; @@ -185,7 +186,7 @@ export class AuthorizationService { query.filter._and.push(parsedPermissions); } - if (query.filter._and.length === 0) delete query.filter._and; + if (query.filter._and.length === 0) delete query.filter; } } } @@ -258,7 +259,7 @@ export class AuthorizationService { } if (requiredColumns.length > 0) { - permission.validation = hasValidationRules ? { _and: [permission.validation] } : { _and: [] }; + permission.validation = hasValidationRules ? { _and: [permission.validation!] } : { _and: [] }; for (const field of requiredColumns) { if (action === 'create' && field.defaultValue === null) { diff --git a/api/src/services/graphql.ts b/api/src/services/graphql.ts index 4e4e131961..28dfaec593 100644 --- a/api/src/services/graphql.ts +++ b/api/src/services/graphql.ts @@ -52,8 +52,8 @@ import env from '../env'; import { BaseException } from '@directus/shared/exceptions'; import { ForbiddenException, GraphQLValidationException, InvalidPayloadException } from '../exceptions'; import { getExtensionManager } from '../extensions'; -import { Accountability } from '@directus/shared/types'; -import { AbstractServiceOptions, Action, Aggregate, GraphQLParams, Item, Query, SchemaOverview } from '../types'; +import { Accountability, Query, Aggregate } from '@directus/shared/types'; +import { AbstractServiceOptions, Action, GraphQLParams, Item, SchemaOverview } from '../types'; import { getGraphQLType } from '../utils/get-graphql-type'; import { reduceSchema } from '../utils/reduce-schema'; import { sanitizeQuery } from '../utils/sanitize-query'; @@ -1413,7 +1413,7 @@ export class GraphQLService { return uniq(fields); }; - const replaceFuncs = (filter?: Filter): undefined | Filter => { + const replaceFuncs = (filter?: Filter | null): null | undefined | Filter => { if (!filter) return filter; return replaceFuncDeep(filter); diff --git a/api/src/services/items.ts b/api/src/services/items.ts index 8811994b3a..11398819bd 100644 --- a/api/src/services/items.ts +++ b/api/src/services/items.ts @@ -9,17 +9,8 @@ import env from '../env'; import { ForbiddenException, InvalidPayloadException } from '../exceptions'; import { translateDatabaseError } from '../exceptions/database/translate'; import logger from '../logger'; -import { Accountability } from '@directus/shared/types'; -import { - AbstractService, - AbstractServiceOptions, - Action, - Item as AnyItem, - PermissionsAction, - PrimaryKey, - Query, - SchemaOverview, -} from '../types'; +import { Accountability, Query, PermissionsAction } from '@directus/shared/types'; +import { AbstractService, AbstractServiceOptions, Action, Item as AnyItem, PrimaryKey, SchemaOverview } from '../types'; import getASTFromQuery from '../utils/get-ast-from-query'; import { toArray } from '@directus/shared/utils'; import { AuthorizationService } from './authorization'; diff --git a/api/src/services/meta.ts b/api/src/services/meta.ts index d88a099246..298527189e 100644 --- a/api/src/services/meta.ts +++ b/api/src/services/meta.ts @@ -2,8 +2,7 @@ import { Knex } from 'knex'; import getDatabase from '../database'; import { ForbiddenException } from '../exceptions'; import { AbstractServiceOptions, SchemaOverview } from '../types'; -import { Accountability } from '@directus/shared/types'; -import { Query } from '../types/query'; +import { Accountability, Query } from '@directus/shared/types'; import { applyFilter, applySearch } from '../utils/apply-query'; import { parseFilter } from '@directus/shared/utils'; @@ -18,11 +17,11 @@ export class MetaService { this.schema = options.schema; } - async getMetaForQuery(collection: string, query: Query): Promise | undefined> { + async getMetaForQuery(collection: string, query: any): Promise | undefined> { if (!query || !query.meta) return; const results = await Promise.all( - query.meta.map((metaVal) => { + query.meta.map((metaVal: string) => { if (metaVal === 'total_count') return this.totalCount(collection); if (metaVal === 'filter_count') return this.filterCount(collection, query); }) diff --git a/api/src/services/payload.ts b/api/src/services/payload.ts index ae0e59b4be..cda1d0b27d 100644 --- a/api/src/services/payload.ts +++ b/api/src/services/payload.ts @@ -5,8 +5,8 @@ import { clone, cloneDeep, isObject, isPlainObject, omit, isNil } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import getDatabase from '../database'; import { ForbiddenException, InvalidPayloadException } from '../exceptions'; -import { AbstractServiceOptions, Item, PrimaryKey, Query, SchemaOverview, Alterations } from '../types'; -import { Accountability } from '@directus/shared/types'; +import { AbstractServiceOptions, Item, PrimaryKey, SchemaOverview, Alterations } from '../types'; +import { Accountability, Query } from '@directus/shared/types'; import { toArray } from '@directus/shared/utils'; import { ItemsService } from './items'; import { unflatten } from 'flat'; diff --git a/api/src/services/permissions.ts b/api/src/services/permissions.ts index 8ed4733b22..0dcf76c924 100644 --- a/api/src/services/permissions.ts +++ b/api/src/services/permissions.ts @@ -1,7 +1,8 @@ import { appAccessMinimalPermissions } from '../database/system-data/app-access-permissions'; import logger from '../logger'; import { ItemsService, QueryOptions } from '../services/items'; -import { AbstractServiceOptions, Item, PermissionsAction, PrimaryKey, Query } from '../types'; +import { AbstractServiceOptions, Item, PrimaryKey } from '../types'; +import { Query, PermissionsAction } from '@directus/shared/types'; import { filterItems } from '../utils/filter-items'; export class PermissionsService extends ItemsService { diff --git a/api/src/services/relations.ts b/api/src/services/relations.ts index 636e5336da..b9b988d7a0 100644 --- a/api/src/services/relations.ts +++ b/api/src/services/relations.ts @@ -1,7 +1,8 @@ import { Knex } from 'knex'; import { systemRelationRows } from '../database/system-data/relations'; import { ForbiddenException, InvalidPayloadException } from '../exceptions'; -import { AbstractServiceOptions, SchemaOverview, Query, Relation, RelationMeta } from '../types'; +import { AbstractServiceOptions, SchemaOverview, Relation, RelationMeta } from '../types'; +import { Query } from '@directus/shared/types'; import { Accountability } from '@directus/shared/types'; import { toArray } from '@directus/shared/utils'; import { ItemsService, QueryOptions } from './items'; diff --git a/api/src/services/roles.ts b/api/src/services/roles.ts index eff48f77b4..c28f716c5d 100644 --- a/api/src/services/roles.ts +++ b/api/src/services/roles.ts @@ -1,5 +1,6 @@ import { ForbiddenException, UnprocessableEntityException } from '../exceptions'; -import { AbstractServiceOptions, PrimaryKey, Query, Alterations, Item } from '../types'; +import { AbstractServiceOptions, PrimaryKey, Alterations, Item } from '../types'; +import { Query } from '@directus/shared/types'; import { ItemsService, MutationOptions } from './items'; import { PermissionsService } from './permissions'; import { PresetsService } from './presets'; diff --git a/api/src/services/specifications.ts b/api/src/services/specifications.ts index d0c449f1e6..0830015626 100644 --- a/api/src/services/specifications.ts +++ b/api/src/services/specifications.ts @@ -15,8 +15,8 @@ import { import { version } from '../../package.json'; import getDatabase from '../database'; import env from '../env'; -import { AbstractServiceOptions, Collection, Permission, Relation, SchemaOverview } from '../types'; -import { Accountability, Field, Type } from '@directus/shared/types'; +import { AbstractServiceOptions, Collection, Relation, SchemaOverview } from '../types'; +import { Accountability, Field, Type, Permission } from '@directus/shared/types'; import { getRelationType } from '../utils/get-relation-type'; import { CollectionsService } from './collections'; import { FieldsService } from './fields'; diff --git a/api/src/services/users.ts b/api/src/services/users.ts index 19d8efe328..82d2c184a2 100644 --- a/api/src/services/users.ts +++ b/api/src/services/users.ts @@ -7,7 +7,8 @@ import { FailedValidationException } from '@directus/shared/exceptions'; import { ForbiddenException, InvalidPayloadException, UnprocessableEntityException } from '../exceptions'; import { RecordNotUniqueException } from '../exceptions/database/record-not-unique'; import logger from '../logger'; -import { AbstractServiceOptions, Item, PrimaryKey, Query, SchemaOverview } from '../types'; +import { AbstractServiceOptions, Item, PrimaryKey, SchemaOverview } from '../types'; +import { Query } from '@directus/shared/types'; import { Accountability } from '@directus/shared/types'; import isUrlAllowed from '../utils/is-url-allowed'; import { toArray } from '@directus/shared/utils'; diff --git a/api/src/types/ast.ts b/api/src/types/ast.ts index 75b32e44dc..f52f972a84 100644 --- a/api/src/types/ast.ts +++ b/api/src/types/ast.ts @@ -1,4 +1,4 @@ -import { Query } from './query'; +import { Query } from '@directus/shared/types'; import { Relation } from './relation'; export type M2ONode = { diff --git a/api/src/types/express.d.ts b/api/src/types/express.d.ts index 1c65aa2e4e..f0c6b7b511 100644 --- a/api/src/types/express.d.ts +++ b/api/src/types/express.d.ts @@ -3,7 +3,7 @@ */ import { Accountability } from '@directus/shared/types'; -import { Query } from './query'; +import { Query } from '@directus/shared/types'; import { SchemaOverview } from './schema'; export {}; diff --git a/api/src/types/index.ts b/api/src/types/index.ts index 9895ca47b0..109a370687 100644 --- a/api/src/types/index.ts +++ b/api/src/types/index.ts @@ -9,8 +9,6 @@ export * from './graphql'; export * from './items'; export * from './meta'; export * from './migration'; -export * from './permissions'; -export * from './query'; export * from './relation'; export * from './revision'; export * from './schema'; diff --git a/api/src/types/permissions.ts b/api/src/types/permissions.ts deleted file mode 100644 index 76dddb6f41..0000000000 --- a/api/src/types/permissions.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Filter } from './query'; - -export type PermissionsAction = 'create' | 'read' | 'update' | 'delete' | 'comment' | 'explain'; - -export type Permission = { - id?: number; - role: string | null; - collection: string; - action: PermissionsAction; - permissions: Record; - validation: Filter | null; - presets: Record | null; - fields: string[] | null; - system?: true; -}; diff --git a/api/src/types/query.ts b/api/src/types/query.ts deleted file mode 100644 index b046afd5c7..0000000000 --- a/api/src/types/query.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Meta } from './meta'; - -export type Query = { - fields?: string[]; - sort?: Sort[]; - filter?: Filter; - limit?: number; - offset?: number; - page?: number; - meta?: Meta[]; - search?: string; - export?: 'json' | 'csv' | 'xml'; - group?: string[]; - aggregate?: Aggregate; - deep?: Record; - alias?: Record; -}; - -export type Sort = { - column: string; - order: 'asc' | 'desc'; -}; - -export type Filter = { - [keyOrOperator: string]: Filter | any; -}; - -/** - * Aggregate operation. Contains column name, and the field alias it should be returned as - */ -export type Aggregate = { - avg?: string[]; - avgDistinct?: string[]; - count?: string[]; - countDistinct?: string[]; - sum?: string[]; - sumDistinct?: string[]; - min?: string[]; - max?: string[]; -}; diff --git a/api/src/types/schema.ts b/api/src/types/schema.ts index c0c5eddf2b..614794f128 100644 --- a/api/src/types/schema.ts +++ b/api/src/types/schema.ts @@ -1,5 +1,4 @@ -import { Type } from '@directus/shared/types'; -import { Permission } from './permissions'; +import { Type, Permission } from '@directus/shared/types'; import { Relation } from './relation'; export type FieldOverview = { diff --git a/api/src/types/services.ts b/api/src/types/services.ts index 5794f75e8c..06179f7377 100644 --- a/api/src/types/services.ts +++ b/api/src/types/services.ts @@ -2,8 +2,7 @@ import { Knex } from 'knex'; import { SchemaOverview } from '../types'; import { Accountability } from '@directus/shared/types'; import { Item, PrimaryKey } from './items'; -import { PermissionsAction } from './permissions'; -import { Query } from './query'; +import { Query, PermissionsAction } from '@directus/shared/types'; export type AbstractServiceOptions = { knex?: Knex; diff --git a/api/src/utils/apply-query.ts b/api/src/utils/apply-query.ts index afbbfb2391..e10a1b0554 100644 --- a/api/src/utils/apply-query.ts +++ b/api/src/utils/apply-query.ts @@ -3,7 +3,8 @@ import { clone, get, isPlainObject, set } from 'lodash'; import { customAlphabet } from 'nanoid'; import validate from 'uuid-validate'; import { InvalidQueryException } from '../exceptions'; -import { Aggregate, Filter, Query, Relation, SchemaOverview } from '../types'; +import { Relation, SchemaOverview } from '../types'; +import { Aggregate, Filter, Query } from '@directus/shared/types'; import { applyFunctionToColumnName } from './apply-function-to-column-name'; import { getColumn } from './get-column'; import { getRelationType } from './get-relation-type'; @@ -24,10 +25,20 @@ export default function applyQuery( ): void { if (query.sort) { dbQuery.orderBy( - query.sort.map((sort) => ({ - ...sort, - column: getColumn(knex, collection, sort.column, false) as any, - })) + query.sort.map((sortField) => { + let column = sortField; + let order: 'asc' | 'desc' = 'asc'; + + if (sortField.startsWith('-')) { + column = column.substring(1); + order = 'desc'; + } + + return { + order, + column: getColumn(knex, collection, column, false) as any, + }; + }) ); } diff --git a/api/src/utils/filter-items.ts b/api/src/utils/filter-items.ts index 17538f16cb..475d597b39 100644 --- a/api/src/utils/filter-items.ts +++ b/api/src/utils/filter-items.ts @@ -1,4 +1,4 @@ -import { Query } from '../types'; +import { Query } from '@directus/shared/types'; import generateJoi from './generate-joi'; /* diff --git a/api/src/utils/generate-joi.ts b/api/src/utils/generate-joi.ts index 44ab075b2c..758b064b51 100644 --- a/api/src/utils/generate-joi.ts +++ b/api/src/utils/generate-joi.ts @@ -1,6 +1,6 @@ import BaseJoi, { AnySchema } from 'joi'; -import { Filter } from '../types'; import { escapeRegExp } from 'lodash'; +import { Filter } from '@directus/shared/types'; const Joi: typeof BaseJoi = BaseJoi.extend({ type: 'string', diff --git a/api/src/utils/get-ast-from-query.ts b/api/src/utils/get-ast-from-query.ts index 91be1e5d3f..b9dd7a7d1b 100644 --- a/api/src/utils/get-ast-from-query.ts +++ b/api/src/utils/get-ast-from-query.ts @@ -5,7 +5,8 @@ import { Knex } from 'knex'; import { cloneDeep, mapKeys, omitBy, uniq } from 'lodash'; import { Accountability } from '@directus/shared/types'; -import { AST, FieldNode, NestedCollectionNode, PermissionsAction, Query, SchemaOverview } from '../types'; +import { AST, FieldNode, NestedCollectionNode, SchemaOverview } from '../types'; +import { Query, PermissionsAction } from '@directus/shared/types'; import { getRelationType } from '../utils/get-relation-type'; type GetASTOptions = { @@ -87,7 +88,7 @@ export default async function getASTFromQuery( sortField = query.group[0]; } - query.sort = [{ column: sortField, order: 'asc' }]; + query.sort = [sortField]; } // When no group by is supplied, but an aggregate function is used, only a single row will be @@ -235,9 +236,7 @@ export default async function getASTFromQuery( }; if (relationType === 'o2m' && !child!.query.sort) { - child!.query.sort = [ - { column: relation.meta?.sort_field || schema.collections[relation.collection].primary, order: 'asc' }, - ]; + child!.query.sort = [relation.meta?.sort_field || schema.collections[relation.collection].primary]; } } diff --git a/api/src/utils/get-schema.ts b/api/src/utils/get-schema.ts index 85ce75dfae..aa1ee03f7c 100644 --- a/api/src/utils/get-schema.ts +++ b/api/src/utils/get-schema.ts @@ -6,8 +6,8 @@ import { systemCollectionRows } from '../database/system-data/collections'; import { systemFieldRows } from '../database/system-data/fields'; import logger from '../logger'; import { RelationsService } from '../services'; -import { Permission, SchemaOverview } from '../types'; -import { Accountability } from '@directus/shared/types'; +import { SchemaOverview } from '../types'; +import { Accountability, Permission } from '@directus/shared/types'; import { toArray } from '@directus/shared/utils'; import getDefaultValue from './get-default-value'; import getLocalType from './get-local-type'; diff --git a/api/src/utils/merge-permissions.ts b/api/src/utils/merge-permissions.ts index 5b5f029898..48c82b6cc5 100644 --- a/api/src/utils/merge-permissions.ts +++ b/api/src/utils/merge-permissions.ts @@ -1,5 +1,5 @@ import { flatten, merge, omit } from 'lodash'; -import { Permission } from '../types'; +import { Permission, LogicalFilterOR } from '@directus/shared/types'; export function mergePermissions(...permissions: Permission[][]): Permission[] { const allPermissions = flatten(permissions); @@ -29,7 +29,7 @@ function mergePerm(currentPerm: Permission, newPerm: Permission) { if (newPerm.permissions) { if (currentPerm.permissions && Object.keys(currentPerm.permissions)[0] === '_or') { permissions = { - _or: [...currentPerm.permissions._or, newPerm.permissions], + _or: [...(currentPerm.permissions as LogicalFilterOR)._or, newPerm.permissions], }; } else if (currentPerm.permissions) { permissions = { @@ -45,7 +45,7 @@ function mergePerm(currentPerm: Permission, newPerm: Permission) { if (newPerm.validation) { if (currentPerm.validation && Object.keys(currentPerm.validation)[0] === '_or') { validation = { - _or: [...currentPerm.validation._or, newPerm.validation], + _or: [...(currentPerm.validation as LogicalFilterOR)._or, newPerm.validation], }; } else if (currentPerm.validation) { validation = { diff --git a/api/src/utils/reduce-schema.ts b/api/src/utils/reduce-schema.ts index 6f53b08cb5..fd7a05b953 100644 --- a/api/src/utils/reduce-schema.ts +++ b/api/src/utils/reduce-schema.ts @@ -1,5 +1,6 @@ import { uniq } from 'lodash'; -import { PermissionsAction, SchemaOverview } from '../types'; +import { SchemaOverview } from '../types'; +import { PermissionsAction } from '@directus/shared/types'; /** * Reduces the schema based on the included permissions. The resulting object is the schema structure, but with only diff --git a/api/src/utils/sanitize-query.ts b/api/src/utils/sanitize-query.ts index c9c31b87a2..7b6c3ee1cf 100644 --- a/api/src/utils/sanitize-query.ts +++ b/api/src/utils/sanitize-query.ts @@ -1,6 +1,7 @@ import { flatten, get, merge, set } from 'lodash'; import logger from '../logger'; -import { Aggregate, Filter, Meta, Query, Sort } from '../types'; +import { Meta } from '../types'; +import { Query, Aggregate, Filter } from '@directus/shared/types'; import { Accountability } from '@directus/shared/types'; import { parseFilter, deepMap } from '@directus/shared/utils'; @@ -44,7 +45,7 @@ export function sanitizeQuery(rawQuery: Record, accountability?: Ac } if (rawQuery.meta) { - query.meta = sanitizeMeta(rawQuery.meta); + (query as any).meta = sanitizeMeta(rawQuery.meta); } if (rawQuery.search && typeof rawQuery.search === 'string') { @@ -90,11 +91,7 @@ function sanitizeSort(rawSort: any) { if (typeof rawSort === 'string') fields = rawSort.split(','); else if (Array.isArray(rawSort)) fields = rawSort as string[]; - return fields.map((field) => { - const order = field.startsWith('-') ? 'desc' : 'asc'; - const column = field.startsWith('-') ? field.substring(1) : field; - return { column, order } as Sort; - }); + return fields; } function sanitizeAggregate(rawAggregate: any): Aggregate { diff --git a/api/src/utils/validate-query.ts b/api/src/utils/validate-query.ts index 5e33b456be..92f911b117 100644 --- a/api/src/utils/validate-query.ts +++ b/api/src/utils/validate-query.ts @@ -1,18 +1,13 @@ import Joi from 'joi'; import { isPlainObject } from 'lodash'; import { InvalidQueryException } from '../exceptions'; -import { Query } from '../types'; +import { Query } from '@directus/shared/types'; import { stringify } from 'wellknown'; const querySchema = Joi.object({ fields: Joi.array().items(Joi.string()), group: Joi.array().items(Joi.string()), - sort: Joi.array().items( - Joi.object({ - column: Joi.string(), - order: Joi.string().valid('asc', 'desc'), - }) - ), + sort: Joi.array().items(Joi.string()), filter: Joi.object({}).unknown(), limit: Joi.number(), offset: Joi.number(), diff --git a/app/src/api.ts b/app/src/api.ts index 5c380b1a01..1784982822 100644 --- a/app/src/api.ts +++ b/app/src/api.ts @@ -50,6 +50,8 @@ export const onResponse = (response: AxiosResponse | Response): AxiosResponse | export const onError = async (error: RequestError): Promise => { const requestsStore = useRequestsStore(); + + // Note: Cancelled requests don't respond with the config const id = (error.response?.config as RequestConfig)?.id; if (id) requestsStore.endRequest(id); diff --git a/app/src/components/register.ts b/app/src/components/register.ts index 472a981c3e..ac5d5e0f0a 100644 --- a/app/src/components/register.ts +++ b/app/src/components/register.ts @@ -1,5 +1,4 @@ import ExportSidebarDetail from '@/views/private/components/export-sidebar-detail'; -import FilterSidebarDetail from '@/views/private/components/filter-sidebar-detail'; import RenderDisplay from '@/views/private/components/render-display'; import RenderTemplate from '@/views/private/components/render-template'; import SidebarDetail from '@/views/private/components/sidebar-detail/'; @@ -113,7 +112,6 @@ export function registerComponents(app: App): void { app.component('RenderDisplay', RenderDisplay); app.component('RenderTemplate', RenderTemplate); - app.component('FilterSidebarDetail', FilterSidebarDetail); app.component('ExportSidebarDetail', ExportSidebarDetail); app.component('SidebarDetail', SidebarDetail); app.component('UserPopover', UserPopover); diff --git a/app/src/components/transition/expand/transition-expand-methods.ts b/app/src/components/transition/expand/transition-expand-methods.ts index 97048af1ba..2922283383 100644 --- a/app/src/components/transition/expand/transition-expand-methods.ts +++ b/app/src/components/transition/expand/transition-expand-methods.ts @@ -11,12 +11,21 @@ interface HTMLExpandElement extends HTMLElement { }; } -export default function (expandedParentClass = '', xAxis = false): Record { +export default function ( + expandedParentClass = '', + xAxis = false, + emit: ( + event: 'beforeEnter' | 'enter' | 'afterEnter' | 'enterCancelled' | 'leave' | 'afterLeave' | 'leaveCancelled', + ...args: any[] + ) => void +): Record { const sizeProperty = xAxis ? 'width' : ('height' as 'width' | 'height'); const offsetProperty = `offset${capitalizeFirst(sizeProperty)}` as 'offsetHeight' | 'offsetWidth'; return { beforeEnter(el: HTMLExpandElement) { + emit('beforeEnter'); + el._parent = el.parentNode as (Node & ParentNode & HTMLElement) | null; el._initialStyle = { transition: el.style.transition, @@ -27,6 +36,8 @@ export default function (expandedParentClass = '', xAxis = false): Record (el.style[sizeProperty] = '0')); }, - afterLeave, - leaveCancelled: afterLeave, - }; + afterLeave(el: HTMLExpandElement) { + emit('afterLeave'); - function afterLeave(el: HTMLExpandElement) { - if (expandedParentClass && el._parent) { - el._parent.classList.remove(expandedParentClass); - } - resetStyles(el); - } + if (expandedParentClass && el._parent) { + el._parent.classList.remove(expandedParentClass); + } + + resetStyles(el); + }, + leaveCancelled(el: HTMLExpandElement) { + emit('leaveCancelled'); + + if (expandedParentClass && el._parent) { + el._parent.classList.remove(expandedParentClass); + } + + resetStyles(el); + }, + }; function resetStyles(el: HTMLExpandElement) { if (!el._initialStyle) return; diff --git a/app/src/components/transition/expand/transition-expand.vue b/app/src/components/transition/expand/transition-expand.vue index a05eaf5ee5..df55f30c3a 100644 --- a/app/src/components/transition/expand/transition-expand.vue +++ b/app/src/components/transition/expand/transition-expand.vue @@ -19,8 +19,9 @@ export default defineComponent({ default: '', }, }, - setup(props) { - const methods = ExpandMethods(props.expandedParentClass, props.xAxis); + emits: ['beforeEnter', 'enter', 'afterEnter', 'enterCancelled', 'leave', 'afterLeave', 'leaveCancelled'], + setup(props, { emit }) { + const methods = ExpandMethods(props.expandedParentClass, props.xAxis, emit); return { methods }; }, }); diff --git a/app/src/components/v-badge/v-badge.vue b/app/src/components/v-badge/v-badge.vue index 4c0b4f544b..7810544a5b 100644 --- a/app/src/components/v-badge/v-badge.vue +++ b/app/src/components/v-badge/v-badge.vue @@ -46,8 +46,8 @@ export default defineComponent({ }); - - diff --git a/app/src/interfaces/files/files.vue b/app/src/interfaces/files/files.vue index 480288a32d..152ff12eb1 100644 --- a/app/src/interfaces/files/files.vue +++ b/app/src/interfaces/files/files.vue @@ -83,7 +83,6 @@ v-model:active="selectModalActive" :collection="relationCollection.collection" :selection="[]" - :filters="selectionFilters" multiple @input="stageSelection" /> @@ -210,7 +209,7 @@ export default defineComponent({ const { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit, relatedPrimaryKey, editModalActive } = useEdit(value, relationInfo, emitter); - const { stageSelection, selectModalActive, selectionFilters } = useSelection(value, items, relationInfo, emitter); + const { stageSelection, selectModalActive } = useSelection(items, relationInfo, emitter); const { sort, sortItems, sortedItems } = useSort(relationInfo, fields, items, emitter); const { createAllowed, selectAllowed } = usePermissions(junctionCollection, relationCollection); @@ -238,7 +237,6 @@ export default defineComponent({ stageSelection, selectModalActive, deleteItem, - selectionFilters, items, relationInfo, relatedPrimaryKey, diff --git a/app/src/interfaces/list-o2m-tree-view/list-o2m-tree-view.vue b/app/src/interfaces/list-o2m-tree-view/list-o2m-tree-view.vue index 48f538d570..f42fd88968 100644 --- a/app/src/interfaces/list-o2m-tree-view/list-o2m-tree-view.vue +++ b/app/src/interfaces/list-o2m-tree-view/list-o2m-tree-view.vue @@ -40,7 +40,6 @@ v-model:active="selectDrawer" :collection="collection" :selection="[]" - :filters="selectionFilters" multiple @input="stageSelection" /> @@ -56,7 +55,7 @@ import api from '@/api'; import { getFieldsFromTemplate } from '@directus/shared/utils'; import hideDragImage from '@/utils/hide-drag-image'; import NestedDraggable from './nested-draggable.vue'; -import { Filter, Relation } from '@directus/shared/types'; +import { Relation } from '@directus/shared/types'; import DrawerCollection from '@/views/private/components/drawer-collection'; import DrawerItem from '@/views/private/components/drawer-item'; @@ -107,7 +106,7 @@ export default defineComponent({ const { info, primaryKeyField } = useCollection(relation.value.related_collection!); const { loading, error, stagedValues, fetchValues, emitValue } = useValues(); - const { stageSelection, selectDrawer, selectionFilters } = useSelection(); + const { stageSelection, selectDrawer } = useSelection(); const { addNewActive, addNew } = useAddNew(); const template = computed(() => { @@ -253,30 +252,7 @@ export default defineComponent({ } }); - const selectionFilters = computed(() => { - const pkField = primaryKeyField.value?.field; - - if (selectedPrimaryKeys.value.length === 0) return []; - - return [ - { - key: 'selection', - field: pkField, - operator: 'nin', - value: selectedPrimaryKeys.value.join(','), - locked: true, - }, - { - key: 'parent', - field: relation.value.field, - operator: 'null', - value: true, - locked: true, - }, - ] as Filter[]; - }); - - return { stageSelection, selectDrawer, selectionFilters }; + return { stageSelection, selectDrawer }; async function stageSelection(newSelection: (number | string)[]) { loading.value = true; diff --git a/app/src/layouts/calendar/index.ts b/app/src/layouts/calendar/index.ts index c167388124..1b040b0585 100644 --- a/app/src/layouts/calendar/index.ts +++ b/app/src/layouts/calendar/index.ts @@ -8,7 +8,7 @@ import { getFieldsFromTemplate } from '@directus/shared/utils'; import getFullcalendarLocale from '@/utils/get-fullcalendar-locale'; import { renderPlainStringTemplate } from '@/utils/render-string-template'; import { unexpectedError } from '@/utils/unexpected-error'; -import { Field, Item } from '@directus/shared/types'; +import { Field, Item, Filter } from '@directus/shared/types'; import { defineLayout } from '@directus/shared/utils'; import { Calendar, CalendarOptions as FullCalendarOptions, EventInput } from '@fullcalendar/core'; import dayGridPlugin from '@fullcalendar/daygrid'; @@ -20,7 +20,6 @@ import { useI18n } from 'vue-i18n'; import CalendarActions from './actions.vue'; import CalendarLayout from './calendar.vue'; import CalendarOptions from './options.vue'; -import CalendarSidebar from './sidebar.vue'; import { useSync } from '@directus/shared/composables'; import { LayoutOptions } from './types'; @@ -31,7 +30,8 @@ export default defineLayout({ component: CalendarLayout, slots: { options: CalendarOptions, - sidebar: CalendarSidebar, + // eslint-disable-next-line @typescript-eslint/no-empty-function + sidebar: () => {}, actions: CalendarActions, }, setup(props, { emit }) { @@ -42,8 +42,8 @@ export default defineLayout({ const appStore = useAppStore(); const layoutOptions = useSync(props, 'layoutOptions', emit); - const filters = useSync(props, 'filters', emit); - const searchQuery = useSync(props, 'searchQuery', emit); + const filter = useSync(props, 'filter', emit); + const search = useSync(props, 'search', emit); const { selection, collection } = toRefs(props); @@ -55,26 +55,24 @@ export default defineLayout({ }) ); - const filtersWithCalendarView = computed(() => { - if (!calendar.value || !startDateField.value) return filters.value; + const filterWithCalendarView = computed(() => { + if (!calendar.value || !startDateField.value) return filter.value; - return [ - ...filters.value, - { - key: 'start_date', - field: startDateField.value, - operator: 'gte', - value: formatISO(calendar.value.view.currentStart), - hidden: true, - }, - { - key: 'end_date', - field: startDateField.value, - operator: 'lte', - value: formatISO(calendar.value.view.currentEnd), - hidden: true, - }, - ]; + return { + _and: [ + filter.value, + { + [startDateField.value]: { + _gte: formatISO(calendar.value.view.currentStart), + }, + }, + { + [startDateField.value]: { + _lte: formatISO(calendar.value.view.currentEnd), + }, + }, + ], + } as Filter; }); const template = computed({ @@ -136,7 +134,7 @@ export default defineLayout({ const { items, loading, error, totalPages, itemCount, totalCount, changeManualSort, getItems } = useItems( collection, { - sort: computed(() => primaryKeyField.value?.field || ''), + sort: computed(() => [primaryKeyField.value?.field || '']), page: ref(1), limit: ref(-1), fields: computed(() => { @@ -148,8 +146,8 @@ export default defineLayout({ if (endDateField.value) fields.push(endDateField.value); return fields; }), - filters: filtersWithCalendarView, - searchQuery: searchQuery, + filter: filterWithCalendarView, + search: search, }, false ); @@ -269,7 +267,7 @@ export default defineLayout({ totalCount, changeManualSort, getItems, - filtersWithCalendarView, + filterWithCalendarView, template, dateFields, startDateField, diff --git a/app/src/layouts/calendar/sidebar.vue b/app/src/layouts/calendar/sidebar.vue deleted file mode 100644 index dc19afd884..0000000000 --- a/app/src/layouts/calendar/sidebar.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - diff --git a/app/src/layouts/cards/cards.vue b/app/src/layouts/cards/cards.vue index 5e723dee1f..7e0fae88e0 100644 --- a/app/src/layouts/cards/cards.vue +++ b/app/src/layouts/cards/cards.vue @@ -10,13 +10,8 @@ />
- - - +
@@ -87,7 +82,7 @@ import CardsHeader from './components/header.vue'; import useElementSize from '@/composables/use-element-size'; import { Field, Item } from '@directus/shared/types'; import { useSync } from '@directus/shared/composables'; -import { Collection } from '@directus/shared/types'; +import { Collection, Filter } from '@directus/shared/types'; export default defineComponent({ components: { Card, CardsHeader }, @@ -178,7 +173,7 @@ export default defineComponent({ required: true, }, sort: { - type: String, + type: Array as PropType, required: true, }, info: { @@ -193,10 +188,6 @@ export default defineComponent({ type: Number, required: true, }, - activeFilterCount: { - type: Number, - required: true, - }, selectAll: { type: Function as PropType<() => void>, required: true, @@ -205,6 +196,14 @@ export default defineComponent({ type: Function as PropType<() => Promise>, required: true, }, + filter: { + type: Object as PropType, + default: null, + }, + search: { + type: String, + default: null, + }, }, emits: ['update:selection', 'update:limit', 'update:size', 'update:sort', 'update:width'], setup(props, { emit }) { diff --git a/app/src/layouts/cards/components/header.vue b/app/src/layouts/cards/components/header.vue index 6c2125a374..a823a8cd24 100644 --- a/app/src/layouts/cards/components/header.vue +++ b/app/src/layouts/cards/components/header.vue @@ -1,9 +1,9 @@ @@ -84,7 +84,7 @@ import { useI18n } from 'vue-i18n'; import { ComponentPublicInstance, defineComponent, PropType, ref } from 'vue'; import { useSync } from '@directus/shared/composables'; import useShortcut from '@/composables/use-shortcut'; -import { Field, Item, Collection } from '@directus/shared/types'; +import { Field, Item, Collection, Filter } from '@directus/shared/types'; import { HeaderRaw } from '@/components/v-table/types'; export default defineComponent({ @@ -174,10 +174,6 @@ export default defineComponent({ type: Function as PropType<(data: any) => Promise>, required: true, }, - activeFilterCount: { - type: Number, - required: true, - }, resetPresetAndRefresh: { type: Function as PropType<() => Promise>, required: true, @@ -186,6 +182,14 @@ export default defineComponent({ type: Function as PropType<() => void>, required: true, }, + filterUser: { + type: Object as PropType, + default: null, + }, + search: { + type: String, + default: null, + }, }, emits: ['update:selection', 'update:tableHeaders', 'update:limit'], setup(props, { emit }) { diff --git a/app/src/layouts/tabular/types.ts b/app/src/layouts/tabular/types.ts index efeb30c7fd..a33e26333c 100644 --- a/app/src/layouts/tabular/types.ts +++ b/app/src/layouts/tabular/types.ts @@ -8,7 +8,7 @@ export type LayoutOptions = { export type LayoutQuery = { fields?: string[]; - sort?: string; + sort?: string[]; page?: number; limit?: number; }; diff --git a/app/src/modules/activity/components/navigation.vue b/app/src/modules/activity/components/navigation.vue index eaab2ef7c9..21c7a219a9 100644 --- a/app/src/modules/activity/components/navigation.vue +++ b/app/src/modules/activity/components/navigation.vue @@ -1,6 +1,6 @@ - + + + + + @@ -50,13 +64,14 @@ import { defineComponent, computed, ref } from 'vue'; import ActivityNavigation from '../components/navigation.vue'; import usePreset from '@/composables/use-preset'; import { useLayout } from '@/composables/use-layout'; -import FilterSidebarDetail from '@/views/private/components/filter-sidebar-detail'; import LayoutSidebarDetail from '@/views/private/components/layout-sidebar-detail'; import SearchInput from '@/views/private/components/search-input'; +import { Filter } from '@directus/shared/types'; +import { mergeFilters } from '@directus/shared/utils'; export default defineComponent({ name: 'ActivityCollection', - components: { ActivityNavigation, FilterSidebarDetail, LayoutSidebarDetail, SearchInput }, + components: { ActivityNavigation, LayoutSidebarDetail, SearchInput }, props: { primaryKey: { type: String, @@ -66,12 +81,25 @@ export default defineComponent({ setup() { const { t } = useI18n(); - const { layout, layoutOptions, layoutQuery, filters, searchQuery } = usePreset(ref('directus_activity')); + const { layout, layoutOptions, layoutQuery, filter, search } = usePreset(ref('directus_activity')); const { breadcrumb } = useBreadcrumb(); const { layoutWrapper } = useLayout(layout); - return { t, breadcrumb, layout, layoutWrapper, layoutOptions, layoutQuery, searchQuery, filters }; + const roleFilter = ref(null); + + return { + t, + breadcrumb, + layout, + layoutWrapper, + layoutOptions, + layoutQuery, + search, + filter, + roleFilter, + mergeFilters, + }; function useBreadcrumb() { const breadcrumb = computed(() => { diff --git a/app/src/modules/collections/routes/collection.vue b/app/src/modules/collections/routes/collection.vue index 7069f4c9d3..abb04e4a2b 100644 --- a/app/src/modules/collections/routes/collection.vue +++ b/app/src/modules/collections/routes/collection.vue @@ -6,8 +6,10 @@ v-model:selection="selection" v-model:layout-options="layoutOptions" v-model:layout-query="layoutQuery" - v-model:filters="filters" - v-model:search-query="searchQuery" + :filter-user="filter" + :filter-system="archiveFilter" + :filter="mergeFilters(filter, archiveFilter)" + :search="search" :collection="collection" :reset-preset="resetPreset" > @@ -88,7 +90,7 @@