mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Merge branch 'main' of github.com:directus/directus into robl/cms-385
This commit is contained in:
5
.changeset/brave-bushes-judge.md
Normal file
5
.changeset/brave-bushes-judge.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/system-data': patch
|
||||
---
|
||||
|
||||
Hide accepted terms field in settings
|
||||
5
.changeset/cyan-guests-admire.md
Normal file
5
.changeset/cyan-guests-admire.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': minor
|
||||
---
|
||||
|
||||
Added the code tool to the WYSIWYG text editor by @Abdallah-Awwad & @robluton
|
||||
5
.changeset/easy-pandas-sin.md
Normal file
5
.changeset/easy-pandas-sin.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/api': patch
|
||||
---
|
||||
|
||||
Fixed parsing functions in aliases
|
||||
5
.changeset/eight-symbols-grow.md
Normal file
5
.changeset/eight-symbols-grow.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Ensured that relational interfaces could reset their saved edits in versions
|
||||
5
.changeset/fluffy-geese-wave.md
Normal file
5
.changeset/fluffy-geese-wave.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Fixed a bug that caused “Save as Copy” to mutate edits before saving
|
||||
5
.changeset/itchy-poems-ring.md
Normal file
5
.changeset/itchy-poems-ring.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/api': patch
|
||||
---
|
||||
|
||||
Removed duplicate code in fields readAll
|
||||
5
.changeset/loose-parts-laugh.md
Normal file
5
.changeset/loose-parts-laugh.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': minor
|
||||
---
|
||||
|
||||
Improved the readability of the primary button in dark mode
|
||||
5
.changeset/puny-boxes-relate.md
Normal file
5
.changeset/puny-boxes-relate.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': minor
|
||||
---
|
||||
|
||||
Ensured that custom validation rules are executed in overlays
|
||||
5
.changeset/puny-cities-bet.md
Normal file
5
.changeset/puny-cities-bet.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': minor
|
||||
---
|
||||
|
||||
Improved custom validation message handling
|
||||
5
.changeset/rare-doors-notice.md
Normal file
5
.changeset/rare-doors-notice.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Added logout flow when user removes own account.
|
||||
5
.changeset/sad-moose-grow.md
Normal file
5
.changeset/sad-moose-grow.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Added redirect to profile page when user registers and not required to verify by email.
|
||||
5
.changeset/salty-pans-tap.md
Normal file
5
.changeset/salty-pans-tap.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Fixed a bug that prevented popups from working in the WYSIWYG interface when opened in a drawer
|
||||
5
.changeset/shy-needles-create.md
Normal file
5
.changeset/shy-needles-create.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Fixed a bug that was preventing overlay forms with junction fields from correctly applying filters in M2O fields
|
||||
21
.changeset/slick-seas-learn.md
Normal file
21
.changeset/slick-seas-learn.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
'@directus/api': major
|
||||
---
|
||||
|
||||
Exclude database-only tables from snapshots
|
||||
|
||||
::: notice
|
||||
Snapshots now exclude tables not tracked in `directus_collections` (database-only tables).
|
||||
|
||||
| Source Version | Target Version | Behavior | Impact |
|
||||
| -------------- | -------------- | ---------------------------------------------------------- | -------------------------------------------- |
|
||||
| < 11.10.0 | ≥ 11.10.0 | Database-only tables from source will be created on target | ⚠️ Tables added |
|
||||
| ≥ 11.10.0 | < 11.10.0 | Database-only tables will be dropped from target | 🚨 Data loss risk |
|
||||
| ≥ 11.10.0 | ≥ 11.10.0 | Database-only tables are ignored in snapshots | ✅ No changes |
|
||||
| < 11.10.0 | < 11.10.0 | Database-only tables may be created or dropped | ⚠️ Depends on the diff between source/target |
|
||||
|
||||
Please review your snapshot workflows to ensure these changes will not result in unexpected behaviour.
|
||||
:::
|
||||
|
||||
|
||||
|
||||
5
.changeset/spicy-singers-nail.md
Normal file
5
.changeset/spicy-singers-nail.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Ensured app access permission rules are applied consistently, regardless of the selection context
|
||||
5
.changeset/spotty-facts-stand.md
Normal file
5
.changeset/spotty-facts-stand.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Fixed a bug that was preventing translations from displaying in the calendar layout
|
||||
5
.changeset/ten-ideas-feel.md
Normal file
5
.changeset/ten-ideas-feel.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Fixed a bug that caused the upload modal to appear behind the drawer
|
||||
5
.changeset/ten-wombats-think.md
Normal file
5
.changeset/ten-wombats-think.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Fixed a bug that prevented the horizontal rule from appearing in the WYSIWYG editor
|
||||
5
.changeset/twelve-pugs-play.md
Normal file
5
.changeset/twelve-pugs-play.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Added code to update the file list ui when importing a file via url
|
||||
@@ -15,8 +15,9 @@ export const snapshotBeforeCreateCollection: Snapshot = {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: null,
|
||||
versioning: false,
|
||||
translations: {},
|
||||
system: false,
|
||||
},
|
||||
schema: {
|
||||
comment: null,
|
||||
@@ -44,7 +45,7 @@ export const snapshotBeforeCreateCollection: Snapshot = {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: {},
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -90,8 +91,9 @@ export const snapshotCreateCollection: Snapshot = {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: null,
|
||||
versioning: false,
|
||||
translations: {},
|
||||
system: false,
|
||||
},
|
||||
schema: {
|
||||
comment: null,
|
||||
@@ -110,8 +112,9 @@ export const snapshotCreateCollection: Snapshot = {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: null,
|
||||
versioning: false,
|
||||
translations: {},
|
||||
system: false,
|
||||
},
|
||||
schema: {
|
||||
comment: null,
|
||||
@@ -130,8 +133,9 @@ export const snapshotCreateCollection: Snapshot = {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: null,
|
||||
versioning: false,
|
||||
translations: {},
|
||||
system: false,
|
||||
},
|
||||
schema: {
|
||||
comment: null,
|
||||
@@ -159,7 +163,7 @@ export const snapshotCreateCollection: Snapshot = {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: {},
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -204,7 +208,7 @@ export const snapshotCreateCollection: Snapshot = {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: {},
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -249,7 +253,7 @@ export const snapshotCreateCollection: Snapshot = {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: {},
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -296,7 +300,8 @@ export const snapshotCreateCollectionNotNested: Snapshot = {
|
||||
note: null,
|
||||
singleton: false,
|
||||
versioning: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
},
|
||||
schema: {
|
||||
comment: null,
|
||||
@@ -316,7 +321,8 @@ export const snapshotCreateCollectionNotNested: Snapshot = {
|
||||
note: null,
|
||||
singleton: false,
|
||||
versioning: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
},
|
||||
schema: {
|
||||
comment: null,
|
||||
@@ -344,7 +350,7 @@ export const snapshotCreateCollectionNotNested: Snapshot = {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: {},
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -389,7 +395,7 @@ export const snapshotCreateCollectionNotNested: Snapshot = {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: {},
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -436,7 +442,8 @@ export const snapshotBeforeDeleteCollection: Snapshot = {
|
||||
note: null,
|
||||
singleton: false,
|
||||
versioning: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
},
|
||||
schema: {
|
||||
comment: null,
|
||||
@@ -456,7 +463,8 @@ export const snapshotBeforeDeleteCollection: Snapshot = {
|
||||
note: null,
|
||||
singleton: false,
|
||||
versioning: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
},
|
||||
schema: {
|
||||
comment: null,
|
||||
@@ -476,7 +484,8 @@ export const snapshotBeforeDeleteCollection: Snapshot = {
|
||||
note: null,
|
||||
singleton: false,
|
||||
versioning: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
},
|
||||
schema: {
|
||||
comment: null,
|
||||
@@ -504,7 +513,7 @@ export const snapshotBeforeDeleteCollection: Snapshot = {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: [],
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -549,7 +558,7 @@ export const snapshotBeforeDeleteCollection: Snapshot = {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: ['translations'],
|
||||
translations: [],
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -575,7 +584,7 @@ export const snapshotBeforeDeleteCollection: Snapshot = {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: [],
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -620,7 +629,7 @@ export const snapshotBeforeDeleteCollection: Snapshot = {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: [],
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -665,7 +674,7 @@ export const snapshotBeforeDeleteCollection: Snapshot = {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: [],
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -710,7 +719,7 @@ export const snapshotBeforeDeleteCollection: Snapshot = {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: [],
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -778,7 +787,7 @@ export const snapshotBeforeDeleteCollection: Snapshot = {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: [],
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -823,7 +832,7 @@ export const snapshotBeforeDeleteCollection: Snapshot = {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: [],
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
|
||||
@@ -2,7 +2,6 @@ import { SchemaBuilder } from '@directus/schema-builder';
|
||||
|
||||
import { expect, test, vi } from 'vitest';
|
||||
import { Client_SQLite3 } from '../../run-ast/lib/apply-query/mock.js';
|
||||
import { convertWildcards } from './convert-wildcards.js';
|
||||
import { fetchAllowedFields } from '../../../permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js';
|
||||
import type { Accountability } from '@directus/types';
|
||||
import knex from 'knex';
|
||||
@@ -48,12 +47,14 @@ test('parse fields with id and title', async () => {
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'id',
|
||||
name: 'id',
|
||||
type: 'field',
|
||||
whenCase: [],
|
||||
},
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'title',
|
||||
name: 'title',
|
||||
type: 'field',
|
||||
@@ -87,6 +88,7 @@ test('parse fields with m2o relation', async () => {
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'id',
|
||||
name: 'id',
|
||||
type: 'field',
|
||||
@@ -96,6 +98,7 @@ test('parse fields with m2o relation', async () => {
|
||||
cases: [],
|
||||
children: [
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'name',
|
||||
name: 'name',
|
||||
type: 'field',
|
||||
@@ -124,6 +127,7 @@ test('parse fields with o2m relation', async () => {
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'id',
|
||||
name: 'id',
|
||||
type: 'field',
|
||||
@@ -133,6 +137,7 @@ test('parse fields with o2m relation', async () => {
|
||||
cases: [],
|
||||
children: [
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'id',
|
||||
name: 'id',
|
||||
type: 'field',
|
||||
@@ -163,6 +168,7 @@ test('parse fields with m2m relation', async () => {
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'id',
|
||||
name: 'id',
|
||||
type: 'field',
|
||||
@@ -175,6 +181,7 @@ test('parse fields with m2m relation', async () => {
|
||||
cases: [],
|
||||
children: [
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'id',
|
||||
name: 'id',
|
||||
type: 'field',
|
||||
@@ -215,18 +222,21 @@ test('parse fields with *.*.*', async () => {
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'id',
|
||||
name: 'id',
|
||||
type: 'field',
|
||||
whenCase: [],
|
||||
},
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'title',
|
||||
name: 'title',
|
||||
type: 'field',
|
||||
whenCase: [],
|
||||
},
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'date',
|
||||
name: 'date',
|
||||
type: 'field',
|
||||
@@ -236,12 +246,14 @@ test('parse fields with *.*.*', async () => {
|
||||
cases: [],
|
||||
children: [
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'id',
|
||||
name: 'id',
|
||||
type: 'field',
|
||||
whenCase: [],
|
||||
},
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'name',
|
||||
name: 'name',
|
||||
type: 'field',
|
||||
@@ -261,6 +273,7 @@ test('parse fields with *.*.*', async () => {
|
||||
cases: [],
|
||||
children: [
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'id',
|
||||
name: 'id',
|
||||
type: 'field',
|
||||
@@ -270,24 +283,28 @@ test('parse fields with *.*.*', async () => {
|
||||
cases: [],
|
||||
children: [
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'id',
|
||||
name: 'id',
|
||||
type: 'field',
|
||||
whenCase: [],
|
||||
},
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'title',
|
||||
name: 'title',
|
||||
type: 'field',
|
||||
whenCase: [],
|
||||
},
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'date',
|
||||
name: 'date',
|
||||
type: 'field',
|
||||
whenCase: [],
|
||||
},
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'author',
|
||||
name: 'author',
|
||||
type: 'field',
|
||||
@@ -347,6 +364,7 @@ test('parse fields with *.*.*', async () => {
|
||||
cases: [],
|
||||
children: [
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'id',
|
||||
name: 'id',
|
||||
type: 'field',
|
||||
@@ -356,24 +374,28 @@ test('parse fields with *.*.*', async () => {
|
||||
cases: [],
|
||||
children: [
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'id',
|
||||
name: 'id',
|
||||
type: 'field',
|
||||
whenCase: [],
|
||||
},
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'title',
|
||||
name: 'title',
|
||||
type: 'field',
|
||||
whenCase: [],
|
||||
},
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'date',
|
||||
name: 'date',
|
||||
type: 'field',
|
||||
whenCase: [],
|
||||
},
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'author',
|
||||
name: 'author',
|
||||
type: 'field',
|
||||
@@ -421,6 +443,7 @@ test('parse fields with *.*.*', async () => {
|
||||
cases: [],
|
||||
children: [
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'id',
|
||||
name: 'id',
|
||||
type: 'field',
|
||||
@@ -464,12 +487,14 @@ test('parse fields with links.*.* and backlinks disabled', async () => {
|
||||
cases: [],
|
||||
children: [
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'id',
|
||||
name: 'id',
|
||||
type: 'field',
|
||||
whenCase: [],
|
||||
},
|
||||
{
|
||||
alias: false,
|
||||
fieldKey: 'article_id',
|
||||
name: 'article_id',
|
||||
type: 'field',
|
||||
|
||||
@@ -60,11 +60,13 @@ export async function parseFields(
|
||||
const relationalStructure: Record<string, string[] | CollectionScope> = Object.create(null);
|
||||
|
||||
for (const fieldKey of fields) {
|
||||
let alias = false;
|
||||
let name = fieldKey;
|
||||
|
||||
if (options.query.alias) {
|
||||
// check for field alias (is one of the key)
|
||||
if (name in options.query.alias) {
|
||||
alias = true;
|
||||
name = options.query.alias[fieldKey]!;
|
||||
}
|
||||
}
|
||||
@@ -151,7 +153,7 @@ export async function parseFields(
|
||||
continue;
|
||||
}
|
||||
|
||||
children.push({ type: 'field', name, fieldKey, whenCase: [] });
|
||||
children.push({ type: 'field', name, fieldKey, whenCase: [], alias });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ export function applyParentFilters(
|
||||
name: nestedNode.relation.field,
|
||||
fieldKey: nestedNode.relation.field,
|
||||
whenCase: [],
|
||||
alias: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,6 +39,7 @@ export function applyParentFilters(
|
||||
name: nestedNode.relation.meta.sort_field,
|
||||
fieldKey: nestedNode.relation.meta.sort_field,
|
||||
whenCase: [],
|
||||
alias: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,5 +2,6 @@ import type { FieldNode, FunctionFieldNode, M2ONode, O2MNode } from '../../../ty
|
||||
import { applyFunctionToColumnName } from './apply-function-to-column-name.js';
|
||||
|
||||
export function getNodeAlias(node: FieldNode | FunctionFieldNode | M2ONode | O2MNode) {
|
||||
if ('alias' in node && node.alias === true) return node.fieldKey;
|
||||
return applyFunctionToColumnName(node.fieldKey);
|
||||
}
|
||||
|
||||
@@ -58,10 +58,15 @@ export function removeTemporaryFields(
|
||||
}
|
||||
} else {
|
||||
const fields: string[] = [];
|
||||
const aliasFields: string[] = [];
|
||||
const nestedCollectionNodes: NestedCollectionNode[] = [];
|
||||
|
||||
for (const child of ast.children) {
|
||||
fields.push(child.fieldKey);
|
||||
if ('alias' in child && child.alias === true) {
|
||||
aliasFields.push(child.fieldKey);
|
||||
} else {
|
||||
fields.push(child.fieldKey);
|
||||
}
|
||||
|
||||
if (child.type !== 'field' && child.type !== 'functionField') {
|
||||
nestedCollectionNodes.push(child);
|
||||
@@ -98,7 +103,7 @@ export function removeTemporaryFields(
|
||||
|
||||
const fieldsWithFunctionsApplied = fields.map((field) => applyFunctionToColumnName(field));
|
||||
|
||||
item = fields.length > 0 ? pick(rawItem, fieldsWithFunctionsApplied) : rawItem[primaryKeyField];
|
||||
item = fields.length > 0 ? pick(rawItem, fieldsWithFunctionsApplied, aliasFields) : rawItem[primaryKeyField];
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ test('Throws InvalidPayloadError on missing body', async () => {
|
||||
await validateBatch('read')(mockRequest as Request, mockResponse as Response, nextFunction);
|
||||
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(InvalidPayloadError);
|
||||
expect(vi.mocked(nextFunction)?.mock?.calls?.[0]?.[0]).toBeInstanceOf(InvalidPayloadError);
|
||||
});
|
||||
|
||||
test(`Short circuits on Array body in update/delete use`, async () => {
|
||||
@@ -79,7 +79,7 @@ test(`Doesn't allow both query and keys in a batch delete`, async () => {
|
||||
await validateBatch('delete')(mockRequest as Request, mockResponse as Response, nextFunction);
|
||||
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(InvalidPayloadError);
|
||||
expect(vi.mocked(nextFunction)?.mock?.calls?.[0]?.[0]).toBeInstanceOf(InvalidPayloadError);
|
||||
});
|
||||
|
||||
test(`Requires 'data' on batch update`, async () => {
|
||||
@@ -93,7 +93,7 @@ test(`Requires 'data' on batch update`, async () => {
|
||||
await validateBatch('update')(mockRequest as Request, mockResponse as Response, nextFunction);
|
||||
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(InvalidPayloadError);
|
||||
expect(vi.mocked(nextFunction)?.mock?.calls?.[0]?.[0]).toBeInstanceOf(InvalidPayloadError);
|
||||
});
|
||||
|
||||
test(`Calls next when all is well`, async () => {
|
||||
@@ -107,5 +107,5 @@ test(`Calls next when all is well`, async () => {
|
||||
await validateBatch('update')(mockRequest as Request, mockResponse as Response, nextFunction);
|
||||
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(nextFunction).mock.calls[0][0]).toBeUndefined();
|
||||
expect(vi.mocked(nextFunction)?.mock?.calls?.[0]?.[0]).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ test('Returns highest result if user is passed', async () => {
|
||||
vi.mocked(fetchGlobalAccessForRoles).mockResolvedValue(mockRolesAccess);
|
||||
vi.mocked(fetchGlobalAccessForUser).mockResolvedValue(mockUserAccess);
|
||||
|
||||
const res = await fetchGlobalAccess({ user: 'user', roles: [] }, knex);
|
||||
const res = await fetchGlobalAccess({ user: 'user', roles: [], ip: '' }, knex);
|
||||
|
||||
expect(res).toEqual({ app: true, admin: true });
|
||||
});
|
||||
@@ -42,7 +42,7 @@ test('Combines result of role and user', async () => {
|
||||
vi.mocked(fetchGlobalAccessForRoles).mockResolvedValue(mockRolesAccess);
|
||||
vi.mocked(fetchGlobalAccessForUser).mockResolvedValue(mockUserAccess);
|
||||
|
||||
const res = await fetchGlobalAccess({ user: 'user', roles: [] }, knex);
|
||||
const res = await fetchGlobalAccess({ user: 'user', roles: [], ip: '' }, knex);
|
||||
|
||||
expect(res).toEqual({ app: true, admin: true });
|
||||
});
|
||||
|
||||
@@ -28,7 +28,9 @@ export async function validateItemAccess(options: ValidateItemAccessOptions, con
|
||||
name: options.collection,
|
||||
query: { limit: options.primaryKeys.length },
|
||||
// Act as if every field was a "normal" field
|
||||
children: options.fields?.map((field) => ({ type: 'field', name: field, fieldKey: field, whenCase: [] })) ?? [],
|
||||
children:
|
||||
options.fields?.map((field) => ({ type: 'field', name: field, fieldKey: field, whenCase: [], alias: false })) ??
|
||||
[],
|
||||
cases: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import type { AccessRow } from '../modules/process-ast/types.js';
|
||||
import type { AccessRow } from '../lib/fetch-policies.js';
|
||||
import { filterPoliciesByIp } from './filter-policies-by-ip.js';
|
||||
|
||||
test('Keeps policies that do not have a ip access rule set configured when IP is null', () => {
|
||||
const policies: AccessRow[] = [
|
||||
{
|
||||
role: null,
|
||||
policy: {
|
||||
id: 'test-policy-1',
|
||||
ip_access: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
role: null,
|
||||
policy: {
|
||||
id: 'test-policy-1',
|
||||
ip_access: ['127.0.0.1'],
|
||||
@@ -22,6 +24,7 @@ test('Keeps policies that do not have a ip access rule set configured when IP is
|
||||
|
||||
expect(output).toEqual([
|
||||
{
|
||||
role: null,
|
||||
policy: {
|
||||
id: 'test-policy-1',
|
||||
ip_access: null,
|
||||
@@ -33,12 +36,14 @@ test('Keeps policies that do not have a ip access rule set configured when IP is
|
||||
test('Keeps policies that match the IP cidr block', () => {
|
||||
const policies: AccessRow[] = [
|
||||
{
|
||||
role: null,
|
||||
policy: {
|
||||
id: 'test-policy-1',
|
||||
ip_access: ['192.168.1.0/22'],
|
||||
},
|
||||
},
|
||||
{
|
||||
role: null,
|
||||
policy: {
|
||||
id: 'test-policy-1',
|
||||
ip_access: ['127.0.0.1'],
|
||||
@@ -50,6 +55,7 @@ test('Keeps policies that match the IP cidr block', () => {
|
||||
|
||||
expect(output).toEqual([
|
||||
{
|
||||
role: null,
|
||||
policy: {
|
||||
id: 'test-policy-1',
|
||||
ip_access: ['192.168.1.0/22'],
|
||||
|
||||
@@ -25,7 +25,7 @@ afterEach(() => {
|
||||
test('Blocks request if host is missing', () => {
|
||||
const options = {};
|
||||
|
||||
expect(() => mockAgent.createConnection(options, () => {})).toThrowError(
|
||||
expect(() => mockAgent.createConnection?.(options, () => {})).toThrowError(
|
||||
`Request cannot be verified due to missing host`,
|
||||
);
|
||||
});
|
||||
@@ -33,7 +33,7 @@ test('Blocks request if host is missing', () => {
|
||||
test('Does not call IP check on createConnection if host is not an IP', () => {
|
||||
const options = { host: 'directus.io' };
|
||||
|
||||
mockAgent.createConnection(options, () => {});
|
||||
mockAgent.createConnection?.(options, () => {});
|
||||
|
||||
expect(isDeniedIp).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -41,7 +41,7 @@ test('Does not call IP check on createConnection if host is not an IP', () => {
|
||||
test('Calls IP check on createConnection if host is IP', async () => {
|
||||
const options = { host: '127.0.0.1' };
|
||||
|
||||
mockAgent.createConnection(options, () => {});
|
||||
mockAgent.createConnection?.(options, () => {});
|
||||
|
||||
expect(isDeniedIp).toHaveBeenCalled();
|
||||
});
|
||||
@@ -51,7 +51,7 @@ test('Blocks on createConnection if IP is denied', async () => {
|
||||
|
||||
const options = { host: '127.0.0.1' };
|
||||
|
||||
expect(() => mockAgent.createConnection(options, () => {})).toThrowError(
|
||||
expect(() => mockAgent.createConnection?.(options, () => {})).toThrowError(
|
||||
`Requested domain "${options.host}" resolves to a denied IP address`,
|
||||
);
|
||||
});
|
||||
@@ -61,7 +61,7 @@ test('Blocks on resolve if IP is denied', async () => {
|
||||
|
||||
const options = { host: 'baddomain' };
|
||||
|
||||
mockAgent.createConnection(options, () => {});
|
||||
mockAgent.createConnection?.(options, () => {});
|
||||
|
||||
mockSocket.emit('lookup', null, '127.0.0.1');
|
||||
|
||||
@@ -75,7 +75,7 @@ test('Does not block on resolve if IP is allowed', async () => {
|
||||
|
||||
const options = { host: 'directus.io' };
|
||||
|
||||
mockAgent.createConnection(options, () => {});
|
||||
mockAgent.createConnection?.(options, () => {});
|
||||
|
||||
mockSocket.emit('lookup', null, '127.0.0.1');
|
||||
|
||||
@@ -88,7 +88,7 @@ test('Checks each resolved IP', async () => {
|
||||
|
||||
const options = { host: 'baddomain' };
|
||||
|
||||
mockAgent.createConnection(options, () => {});
|
||||
mockAgent.createConnection?.(options, () => {});
|
||||
|
||||
mockSocket.emit('lookup', null, '192.158.1.38');
|
||||
mockSocket.emit('lookup', null, '127.0.0.1');
|
||||
|
||||
@@ -199,7 +199,7 @@ export class FieldsService {
|
||||
|
||||
const knownCollections = Object.keys(this.schema.collections);
|
||||
|
||||
const result = [...columnsWithSystem, ...aliasFieldsAsField].filter((field) =>
|
||||
let result = [...columnsWithSystem, ...aliasFieldsAsField].filter((field) =>
|
||||
knownCollections.includes(field.collection),
|
||||
);
|
||||
|
||||
@@ -239,7 +239,7 @@ export class FieldsService {
|
||||
throw new ForbiddenError();
|
||||
}
|
||||
|
||||
return result.filter((field) => {
|
||||
result = result.filter((field) => {
|
||||
if (field.collection in allowedFieldsInCollection === false) return false;
|
||||
const allowedFields = allowedFieldsInCollection[field.collection]!;
|
||||
if (allowedFields.has('*')) return true;
|
||||
@@ -249,12 +249,6 @@ export class FieldsService {
|
||||
|
||||
// Update specific database type overrides
|
||||
for (const field of result) {
|
||||
if (field.meta?.special?.includes('cast-timestamp')) {
|
||||
field.type = 'timestamp';
|
||||
} else if (field.meta?.special?.includes('cast-datetime')) {
|
||||
field.type = 'dateTime';
|
||||
}
|
||||
|
||||
field.type = this.helpers.schema.processFieldType(field);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEnv } from '@directus/env';
|
||||
import { getSharpInstance } from './get-sharp-instance';
|
||||
import { getSharpInstance } from './get-sharp-instance.js';
|
||||
|
||||
import { beforeAll, expect, test, vi } from 'vitest';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { GraphQLError } from 'graphql';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import processError from './process-error.js';
|
||||
import { createError } from '@directus/errors';
|
||||
import type { Accountability } from '@directus/types';
|
||||
|
||||
describe('GraphQL processError util', () => {
|
||||
const sampleError = new GraphQLError('An error message', { path: ['test_collection'] });
|
||||
@@ -19,11 +20,15 @@ describe('GraphQL processError util', () => {
|
||||
});
|
||||
|
||||
test('returns redacted error when authenticated but not an admin', () => {
|
||||
expect(processError({ role: 'd674e22b-f405-48ba-9958-9a7bd16a1aa9' }, sampleError)).toEqual(redactedError);
|
||||
expect(processError({ role: 'd674e22b-f405-48ba-9958-9a7bd16a1aa9' } as Accountability, sampleError)).toEqual(
|
||||
redactedError,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns original error when authenticated and is an admin', () => {
|
||||
expect(processError({ role: 'd674e22b-f405-48ba-9958-9a7bd16a1aa9', admin: true }, sampleError)).toEqual({
|
||||
expect(
|
||||
processError({ role: 'd674e22b-f405-48ba-9958-9a7bd16a1aa9', admin: true } as Accountability, sampleError),
|
||||
).toEqual({
|
||||
message: 'An error message',
|
||||
path: ['test_collection'],
|
||||
extensions: {
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Helpers } from '../database/helpers/index.js';
|
||||
import { getHelpers } from '../database/helpers/index.js';
|
||||
import { PayloadService } from './index.js';
|
||||
import { SchemaBuilder } from '@directus/schema-builder';
|
||||
import type { Item } from '@directus/types';
|
||||
import type { Item, Accountability } from '@directus/types';
|
||||
|
||||
vi.mock('../../src/database/index', () => ({
|
||||
getDatabaseClient: vi.fn().mockReturnValue('postgres'),
|
||||
@@ -18,6 +18,7 @@ describe('Integration Tests', () => {
|
||||
let tracker: Tracker;
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.stubEnv('TZ', 'UTC');
|
||||
db = vi.mocked(knex.default({ client: MockClient }));
|
||||
tracker = createTracker(db);
|
||||
});
|
||||
@@ -46,7 +47,7 @@ describe('Integration Tests', () => {
|
||||
value: 123,
|
||||
action: 'read',
|
||||
payload: {},
|
||||
accountability: { role: null },
|
||||
accountability: { role: null } as Accountability,
|
||||
specials: [],
|
||||
helpers,
|
||||
});
|
||||
@@ -59,7 +60,7 @@ describe('Integration Tests', () => {
|
||||
value: '',
|
||||
action: 'read',
|
||||
payload: {},
|
||||
accountability: { role: null },
|
||||
accountability: { role: null } as Accountability,
|
||||
specials: [],
|
||||
helpers,
|
||||
});
|
||||
@@ -72,7 +73,7 @@ describe('Integration Tests', () => {
|
||||
value: ['test', 'directus'],
|
||||
action: 'read',
|
||||
payload: {},
|
||||
accountability: { role: null },
|
||||
accountability: { role: null } as Accountability,
|
||||
specials: [],
|
||||
helpers,
|
||||
});
|
||||
@@ -85,7 +86,7 @@ describe('Integration Tests', () => {
|
||||
value: 'test,directus',
|
||||
action: 'read',
|
||||
payload: {},
|
||||
accountability: { role: null },
|
||||
accountability: { role: null } as Accountability,
|
||||
specials: [],
|
||||
helpers,
|
||||
});
|
||||
@@ -98,7 +99,7 @@ describe('Integration Tests', () => {
|
||||
value: ['test', 'directus'],
|
||||
action: 'create',
|
||||
payload: {},
|
||||
accountability: { role: null },
|
||||
accountability: { role: null } as Accountability,
|
||||
specials: [],
|
||||
helpers,
|
||||
});
|
||||
@@ -111,7 +112,7 @@ describe('Integration Tests', () => {
|
||||
value: 'test,directus',
|
||||
action: 'create',
|
||||
payload: {},
|
||||
accountability: { role: null },
|
||||
accountability: { role: null } as Accountability,
|
||||
specials: [],
|
||||
helpers,
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import knex from 'knex';
|
||||
import { createTracker, MockClient, Tracker } from 'knex-mock-client';
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { ForbiddenError } from '@directus/errors';
|
||||
import type { Accountability } from '@directus/types';
|
||||
import type { Collection } from '../types/collection.js';
|
||||
import type { Snapshot, SnapshotDiffWithHash } from '../types/snapshot.js';
|
||||
import { applyDiff } from '../utils/apply-diff.js';
|
||||
@@ -54,7 +55,9 @@ const testCollectionDiff = {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
versioning: false,
|
||||
},
|
||||
schema: { name: 'test' },
|
||||
},
|
||||
@@ -77,7 +80,7 @@ describe('Services / Schema', () => {
|
||||
it('should throw ForbiddenError for non-admin user', async () => {
|
||||
vi.mocked(getSnapshot).mockResolvedValueOnce(testSnapshot);
|
||||
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'test', admin: false } });
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'test', admin: false } as Accountability });
|
||||
|
||||
expect(service.snapshot()).rejects.toThrowError(ForbiddenError);
|
||||
});
|
||||
@@ -85,7 +88,7 @@ describe('Services / Schema', () => {
|
||||
it('should return snapshot for admin user', async () => {
|
||||
vi.mocked(getSnapshot).mockResolvedValueOnce(testSnapshot);
|
||||
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'admin', admin: true } });
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'admin', admin: true } as Accountability });
|
||||
|
||||
expect(service.snapshot()).resolves.toEqual(testSnapshot);
|
||||
});
|
||||
@@ -104,7 +107,7 @@ describe('Services / Schema', () => {
|
||||
it('should throw ForbiddenError for non-admin user', async () => {
|
||||
vi.mocked(getSnapshot).mockResolvedValueOnce(testSnapshot);
|
||||
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'test', admin: false } });
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'test', admin: false } as Accountability });
|
||||
|
||||
expect(service.apply(snapshotDiffWithHash)).rejects.toThrowError(ForbiddenError);
|
||||
expect(vi.mocked(applyDiff)).not.toHaveBeenCalledOnce();
|
||||
@@ -113,7 +116,7 @@ describe('Services / Schema', () => {
|
||||
it('should apply for admin user', async () => {
|
||||
vi.mocked(getSnapshot).mockResolvedValueOnce(testSnapshot);
|
||||
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'admin', admin: true } });
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'admin', admin: true } as Accountability });
|
||||
|
||||
await service.apply(snapshotDiffWithHash);
|
||||
|
||||
@@ -138,7 +141,9 @@ describe('Services / Schema', () => {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
versioning: false,
|
||||
},
|
||||
schema: {
|
||||
name: 'test',
|
||||
@@ -150,7 +155,7 @@ describe('Services / Schema', () => {
|
||||
} satisfies Snapshot;
|
||||
|
||||
it('should throw ForbiddenError for non-admin user', async () => {
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'test', admin: false } });
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'test', admin: false } as Accountability });
|
||||
|
||||
expect(service.diff(snapshotToApply, { currentSnapshot: testSnapshot, force: true })).rejects.toThrowError(
|
||||
ForbiddenError,
|
||||
@@ -158,7 +163,7 @@ describe('Services / Schema', () => {
|
||||
});
|
||||
|
||||
it('should return diff for admin user', async () => {
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'admin', admin: true } });
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'admin', admin: true } as Accountability });
|
||||
|
||||
expect(service.diff(snapshotToApply, { currentSnapshot: testSnapshot, force: true })).resolves.toEqual({
|
||||
collections: [testCollectionDiff],
|
||||
@@ -168,7 +173,7 @@ describe('Services / Schema', () => {
|
||||
});
|
||||
|
||||
it('should return null for empty diff', async () => {
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'admin', admin: true } });
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'admin', admin: true } as Accountability });
|
||||
|
||||
expect(service.diff(testSnapshot, { currentSnapshot: testSnapshot, force: true })).resolves.toBeNull();
|
||||
});
|
||||
@@ -176,7 +181,7 @@ describe('Services / Schema', () => {
|
||||
|
||||
describe('getHashedSnapshot', () => {
|
||||
it('should return snapshot for admin user', async () => {
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'admin', admin: true } });
|
||||
const service = new SchemaService({ knex: db, accountability: { role: 'admin', admin: true } as Accountability });
|
||||
|
||||
expect(service.getHashedSnapshot(testSnapshot)).toEqual(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -5,6 +5,8 @@ import { createTracker, MockClient, Tracker } from 'knex-mock-client';
|
||||
import type { MockedFunction } from 'vitest';
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { SpecificationService } from './index.js';
|
||||
import type { Accountability } from '@directus/types';
|
||||
import type { RequestBodyObject } from 'openapi3-ts/oas30';
|
||||
|
||||
class Client_PG extends MockClient {}
|
||||
|
||||
@@ -48,7 +50,7 @@ describe('Integration Tests', () => {
|
||||
const service = new SpecificationService({
|
||||
knex: db,
|
||||
schema,
|
||||
accountability: { role: 'admin', admin: true },
|
||||
accountability: { role: 'admin', admin: true } as Accountability,
|
||||
});
|
||||
|
||||
const spec = await service.oas.generate();
|
||||
@@ -259,12 +261,13 @@ describe('Integration Tests', () => {
|
||||
const service = new SpecificationService({
|
||||
knex: db,
|
||||
schema: schema2,
|
||||
accountability: { role: 'admin', admin: true },
|
||||
accountability: { role: 'admin', admin: true } as Accountability,
|
||||
});
|
||||
|
||||
const spec = await service.oas.generate();
|
||||
const requestBody = spec.paths['/items/test_table']?.post?.requestBody as RequestBodyObject;
|
||||
|
||||
const targetSchema = spec.paths['/items/test_table']?.post?.requestBody?.content['application/json'].schema;
|
||||
const targetSchema = requestBody?.content?.['application/json']?.schema;
|
||||
|
||||
expect(targetSchema).toHaveProperty('oneOf');
|
||||
expect(targetSchema).not.toHaveProperty('type');
|
||||
|
||||
@@ -65,13 +65,13 @@ describe('Integration Tests', () => {
|
||||
|
||||
describe('createOne', () => {
|
||||
it('should error because of deprecation', async () => {
|
||||
return expect(service.createOne({})).rejects.toEqual(errorDeprecation);
|
||||
return expect(service.createOne()).rejects.toEqual(errorDeprecation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMany', () => {
|
||||
it('should error because of deprecation', async () => {
|
||||
return expect(service.createMany([{}])).rejects.toEqual(errorDeprecation);
|
||||
return expect(service.createMany()).rejects.toEqual(errorDeprecation);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
describe('updateMany', () => {
|
||||
it('should error because of deprecation', async () => {
|
||||
return expect(service.updateMany([1], {})).rejects.toEqual(errorDeprecation);
|
||||
return expect(service.updateMany()).rejects.toEqual(errorDeprecation);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -57,7 +57,11 @@ describe('WebSocketService', () => {
|
||||
});
|
||||
|
||||
test('broadcast with role filter', () => {
|
||||
const clients = [mockClient({ user: 'test', role: 'test' }), mockClient({ user: 'test2', role: 'test2' })];
|
||||
const clients = [
|
||||
mockClient({ user: 'test', role: 'test' } as Accountability),
|
||||
mockClient({ user: 'test2', role: 'test2' } as Accountability),
|
||||
];
|
||||
|
||||
const message = 'test 123';
|
||||
|
||||
vi.mocked(getWebSocketController).mockReturnValue({ clients: new Set(clients) } as unknown as WebSocketController);
|
||||
@@ -70,7 +74,11 @@ describe('WebSocketService', () => {
|
||||
});
|
||||
|
||||
test('broadcast with user filter', () => {
|
||||
const clients = [mockClient({ user: 'test', role: 'test' }), mockClient({ user: 'test2', role: 'test2' })];
|
||||
const clients = [
|
||||
mockClient({ user: 'test', role: 'test' } as Accountability),
|
||||
mockClient({ user: 'test2', role: 'test2' } as Accountability),
|
||||
];
|
||||
|
||||
const message = 'test 123';
|
||||
|
||||
vi.mocked(getWebSocketController).mockReturnValue({ clients: new Set(clients) } as unknown as WebSocketController);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getExtensionCount, type ExtensionCount } from './get-extension-count.js
|
||||
vi.mock('../../utils/get-schema.js');
|
||||
vi.mock('../../services/extensions.js');
|
||||
|
||||
let mockDb: Knex;
|
||||
const mockDb: Knex = {} as Knex;
|
||||
|
||||
function generateParentBundleExtension(enabled: boolean) {
|
||||
return {
|
||||
|
||||
@@ -78,6 +78,8 @@ export type FieldNode = {
|
||||
type: 'field';
|
||||
name: string;
|
||||
fieldKey: string;
|
||||
/** If the field was created through alias query parameters */
|
||||
alias: boolean;
|
||||
|
||||
/**
|
||||
* Which permission cases have to be met on the current item for this field to return a value
|
||||
|
||||
@@ -50,7 +50,8 @@ describe('applySnapshot', () => {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
versioning: false,
|
||||
},
|
||||
schema: { name: 'test_table_2' },
|
||||
@@ -73,7 +74,7 @@ describe('applySnapshot', () => {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: {},
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -132,8 +133,9 @@ describe('applySnapshot', () => {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
versioning: false,
|
||||
system: false,
|
||||
},
|
||||
schema: { name: 'test_table_2' },
|
||||
fields: [
|
||||
@@ -155,7 +157,7 @@ describe('applySnapshot', () => {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: {},
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -203,7 +205,7 @@ describe('applySnapshot', () => {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: {},
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -237,8 +239,9 @@ describe('applySnapshot', () => {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
versioning: false,
|
||||
system: false,
|
||||
},
|
||||
schema: { name: 'test_table_3' },
|
||||
};
|
||||
@@ -286,7 +289,9 @@ describe('applySnapshot', () => {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
versioning: false,
|
||||
},
|
||||
schema: { name: 'test_uuid_table' },
|
||||
},
|
||||
@@ -295,7 +300,9 @@ describe('applySnapshot', () => {
|
||||
{
|
||||
collection: 'test_uuid_table',
|
||||
field: 'id',
|
||||
name: 'id',
|
||||
meta: {
|
||||
id: 1,
|
||||
collection: 'test_uuid_table',
|
||||
conditions: null,
|
||||
display: null,
|
||||
@@ -310,7 +317,7 @@ describe('applySnapshot', () => {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: {},
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
@@ -351,14 +358,18 @@ describe('applySnapshot', () => {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
versioning: false,
|
||||
system: false,
|
||||
},
|
||||
schema: { name: 'test_uuid_table' },
|
||||
fields: [
|
||||
{
|
||||
collection: 'test_uuid_table',
|
||||
field: 'id',
|
||||
name: 'id',
|
||||
meta: {
|
||||
id: 1,
|
||||
collection: 'test_uuid_table',
|
||||
conditions: null,
|
||||
display: null,
|
||||
@@ -373,7 +384,7 @@ describe('applySnapshot', () => {
|
||||
required: false,
|
||||
sort: null,
|
||||
special: null,
|
||||
translations: {},
|
||||
translations: null,
|
||||
validation: null,
|
||||
validation_message: null,
|
||||
width: 'full',
|
||||
|
||||
@@ -3,8 +3,8 @@ import '../types/express.d.ts';
|
||||
import asyncHandler from './async-handler.js';
|
||||
import { expect, vi, test } from 'vitest';
|
||||
|
||||
let mockRequest: Partial<Request & { token?: string }>;
|
||||
let mockResponse: Partial<Response>;
|
||||
const mockRequest: Partial<Request & { token?: string }> = {};
|
||||
const mockResponse: Partial<Response> = {};
|
||||
const nextFunction = vi.fn();
|
||||
|
||||
test('Wraps async middleware in Promise resolve that will catch rejects and pass them to the nextFn', async () => {
|
||||
|
||||
@@ -2,10 +2,24 @@ import type { Request } from 'express';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { getCacheControlHeader } from './get-cache-headers.js';
|
||||
import { useEnv } from '@directus/env';
|
||||
import type { Accountability } from '@directus/types';
|
||||
|
||||
vi.mock('@directus/env');
|
||||
|
||||
const scenarios = [
|
||||
type Scenario = {
|
||||
name: string;
|
||||
input: {
|
||||
env: Record<string, any>;
|
||||
headers: Record<string, string>;
|
||||
accountability: Accountability | null;
|
||||
ttl?: number;
|
||||
globalCacheSettings: boolean;
|
||||
personalized: boolean;
|
||||
};
|
||||
output: string;
|
||||
};
|
||||
|
||||
const scenarios: Scenario[] = [
|
||||
// Test the cache-control header
|
||||
{
|
||||
name: 'when cache-Control header includes no-store',
|
||||
@@ -133,7 +147,7 @@ const scenarios = [
|
||||
headers: {},
|
||||
accountability: {
|
||||
role: '7efc7413-7ffe-4e6f-a0ac-687bbf9f8076',
|
||||
},
|
||||
} as Accountability,
|
||||
ttl: 5678910,
|
||||
globalCacheSettings: false,
|
||||
personalized: true,
|
||||
@@ -145,7 +159,7 @@ const scenarios = [
|
||||
input: {
|
||||
env: {},
|
||||
headers: {},
|
||||
accountability: {},
|
||||
accountability: {} as Accountability,
|
||||
ttl: 5678910,
|
||||
globalCacheSettings: false,
|
||||
personalized: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SchemaOverview } from '@directus/types';
|
||||
import type { Field, Relation, SchemaOverview } from '@directus/types';
|
||||
import { version } from 'directus/version';
|
||||
import type { Knex } from 'knex';
|
||||
import { fromPairs, isArray, isPlainObject, mapValues, omit, sortBy, toPairs } from 'lodash-es';
|
||||
@@ -25,9 +25,9 @@ export async function getSnapshot(options?: { database?: Knex; schema?: SchemaOv
|
||||
relationsService.readAll(),
|
||||
]);
|
||||
|
||||
const collectionsFiltered = collectionsRaw.filter((item: any) => excludeSystem(item));
|
||||
const fieldsFiltered = fieldsRaw.filter((item: any) => excludeSystem(item));
|
||||
const relationsFiltered = relationsRaw.filter((item: any) => excludeSystem(item));
|
||||
const collectionsFiltered = collectionsRaw.filter((item: any) => excludeSystem(item) && excludeUntracked(item));
|
||||
const fieldsFiltered = fieldsRaw.filter((item: any) => excludeSystem(item) && excludeUntracked(item));
|
||||
const relationsFiltered = relationsRaw.filter((item: any) => excludeSystem(item) && excludeUntracked(item));
|
||||
|
||||
const collectionsSorted = sortBy(mapValues(collectionsFiltered, sortDeep), ['collection']);
|
||||
|
||||
@@ -49,11 +49,16 @@ export async function getSnapshot(options?: { database?: Knex; schema?: SchemaOv
|
||||
};
|
||||
}
|
||||
|
||||
function excludeSystem(item: { meta?: { system?: boolean } }) {
|
||||
function excludeSystem(item: Collection | Field | Relation) {
|
||||
if (item?.meta?.system === true) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function excludeUntracked(item: Collection | Field | Relation) {
|
||||
if (item?.meta === null) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function omitID(item: Record<string, any>) {
|
||||
return omit(item, 'meta.id');
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ describe('sanitizeCollection', () => {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
versioning: false,
|
||||
},
|
||||
schema: { comment: null, name: 'test', schema: 'public' },
|
||||
},
|
||||
@@ -33,7 +35,9 @@ describe('sanitizeCollection', () => {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
versioning: false,
|
||||
},
|
||||
schema: { collation: 'latin1_swedish_ci', name: 'test', engine: 'InnoDB' },
|
||||
},
|
||||
@@ -49,7 +53,9 @@ describe('sanitizeCollection', () => {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
versioning: false,
|
||||
},
|
||||
schema: { name: 'test', owner: 'postgres' },
|
||||
},
|
||||
@@ -65,7 +71,9 @@ describe('sanitizeCollection', () => {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
versioning: false,
|
||||
},
|
||||
schema: { name: 'test', sql: 'CREATE TABLE `test` (`id` integer not null primary key autoincrement)' },
|
||||
},
|
||||
@@ -81,7 +89,9 @@ describe('sanitizeCollection', () => {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
versioning: false,
|
||||
},
|
||||
schema: { name: 'test', catalog: 'test-db' },
|
||||
},
|
||||
@@ -99,7 +109,9 @@ describe('sanitizeCollection', () => {
|
||||
item_duplication_fields: null,
|
||||
note: null,
|
||||
singleton: false,
|
||||
translations: {},
|
||||
translations: null,
|
||||
system: false,
|
||||
versioning: false,
|
||||
},
|
||||
schema: { name: 'test' },
|
||||
});
|
||||
@@ -142,6 +154,7 @@ describe('sanitizeField', () => {
|
||||
foreign_key_table: null,
|
||||
generation_expression: null,
|
||||
has_auto_increment: true,
|
||||
is_indexed: false,
|
||||
is_generated: false,
|
||||
is_nullable: false,
|
||||
is_primary_key: true,
|
||||
@@ -190,6 +203,7 @@ describe('sanitizeField', () => {
|
||||
foreign_key_table: null,
|
||||
generation_expression: null,
|
||||
has_auto_increment: true,
|
||||
is_indexed: false,
|
||||
is_generated: false,
|
||||
is_nullable: false,
|
||||
is_primary_key: true,
|
||||
@@ -198,7 +212,6 @@ describe('sanitizeField', () => {
|
||||
name: 'id',
|
||||
numeric_precision: 32,
|
||||
numeric_scale: 0,
|
||||
|
||||
table: 'test',
|
||||
},
|
||||
type: 'integer',
|
||||
@@ -238,6 +251,7 @@ describe('sanitizeField', () => {
|
||||
foreign_key_table: null,
|
||||
generation_expression: null,
|
||||
has_auto_increment: true,
|
||||
is_indexed: false,
|
||||
is_generated: false,
|
||||
is_nullable: false,
|
||||
is_primary_key: true,
|
||||
|
||||
@@ -27,6 +27,9 @@ const inputFile = {
|
||||
modified_on: '',
|
||||
focal_point_x: null,
|
||||
focal_point_y: null,
|
||||
created_on: '',
|
||||
tus_data: null,
|
||||
tus_id: null,
|
||||
} satisfies File;
|
||||
|
||||
describe('resolvePreset', () => {
|
||||
|
||||
@@ -270,6 +270,12 @@ async function onClick(event: MouseEvent) {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
body.dark .button {
|
||||
--v-button-color: var(--theme--foreground);
|
||||
--v-button-color-hover: var(--theme--foreground);
|
||||
--v-button-color-active: var(--theme--foreground);
|
||||
}
|
||||
|
||||
.button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
@@ -151,7 +151,7 @@ function useOverlayFocusTrap() {
|
||||
z-index: 600;
|
||||
|
||||
&.keep-behind {
|
||||
z-index: 490;
|
||||
z-index: 500;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,15 @@ const validationPrefix = computed(() => {
|
||||
return null;
|
||||
});
|
||||
|
||||
const showCustomValidationMessage = computed(() => {
|
||||
if (!props.validationError) return false;
|
||||
|
||||
const customValidationMessage = !!props.field.meta?.validation_message;
|
||||
const hasCustomValidation = !!props.field.meta?.validation;
|
||||
|
||||
return customValidationMessage && (!hasCustomValidation || props.validationError.code === 'FAILED_VALIDATION');
|
||||
});
|
||||
|
||||
function emitValue(value: any) {
|
||||
if (
|
||||
(isEqual(value, props.initialValue) || (props.initialValue === undefined && isEqual(value, defaultValue.value))) &&
|
||||
@@ -215,7 +224,7 @@ function useComputedValues() {
|
||||
<small v-if="field.meta && field.meta.note" v-md="{ value: field.meta.note, target: '_blank' }" class="type-note" />
|
||||
|
||||
<small v-if="validationError" class="validation-error selectable">
|
||||
<template v-if="field.meta?.validation_message">
|
||||
<template v-if="showCustomValidationMessage">
|
||||
{{ field.meta?.validation_message }}
|
||||
<v-icon v-tooltip="validationMessage" small right name="help" />
|
||||
</template>
|
||||
|
||||
@@ -389,6 +389,7 @@ function useRawEditor() {
|
||||
:badge="badge"
|
||||
:raw-editor-enabled="rawEditorEnabled"
|
||||
:direction="direction"
|
||||
:version
|
||||
v-bind="fieldsMap[fieldName]!.meta?.options || {}"
|
||||
@apply="apply"
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,13 @@ import { ValidationError, Field } from '@directus/types';
|
||||
import { formatFieldFunction } from '@/utils/format-field-function';
|
||||
import { extractFieldFromFunction } from '@/utils/extract-field-from-function';
|
||||
|
||||
type ValidationErrorWithDetails = ValidationError & {
|
||||
fieldName: string;
|
||||
groupName: string;
|
||||
hasCustomValidation: boolean;
|
||||
customValidationMessage: string | null;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
validationErrors: ValidationError[];
|
||||
fields: Field[];
|
||||
@@ -14,9 +21,7 @@ defineEmits(['scroll-to-field']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const validationErrorsWithNames = computed<
|
||||
(ValidationError & { fieldName: string; groupName: string; customValidationMessage: string | null })[]
|
||||
>(() => {
|
||||
const validationErrorsWithDetails = computed<ValidationErrorWithDetails[]>(() => {
|
||||
return props.validationErrors.map(
|
||||
(validationError: ValidationError & { nestedNames?: Record<string, string>; validation_message?: string }) => {
|
||||
const { field: _fieldKey, fn: functionName } = extractFieldFromFunction(validationError.field);
|
||||
@@ -30,6 +35,7 @@ const validationErrorsWithNames = computed<
|
||||
field: fieldKey,
|
||||
fieldName,
|
||||
groupName: group?.name ?? validationError.group,
|
||||
hasCustomValidation: !!field?.meta?.validation,
|
||||
customValidationMessage: validationError.validation_message ?? field?.meta?.validation_message,
|
||||
};
|
||||
|
||||
@@ -45,8 +51,22 @@ const validationErrorsWithNames = computed<
|
||||
return `${separator}${nestedFieldKeys.map((name) => nestedNames?.[name] ?? name).join(separator)}`;
|
||||
}
|
||||
},
|
||||
) as (ValidationError & { fieldName: string; groupName: string; customValidationMessage: string | null })[];
|
||||
) as ValidationErrorWithDetails[];
|
||||
});
|
||||
|
||||
function getDefaultValidationMessage(validationError: ValidationError) {
|
||||
const isNotUnique = validationError.code === 'RECORD_NOT_UNIQUE';
|
||||
if (isNotUnique) return t('validationError.unique', validationError);
|
||||
|
||||
return t(`validationError.${validationError.type}`, validationError);
|
||||
}
|
||||
|
||||
function showCustomValidationMessage(validationError: ValidationErrorWithDetails) {
|
||||
return (
|
||||
validationError.customValidationMessage &&
|
||||
(!validationError.hasCustomValidation || validationError.code === 'FAILED_VALIDATION')
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -54,7 +74,7 @@ const validationErrorsWithNames = computed<
|
||||
<div>
|
||||
<p>{{ t('validation_errors_notice') }}</p>
|
||||
<ul class="validation-errors-list">
|
||||
<li v-for="(validationError, index) of validationErrorsWithNames" :key="index" class="validation-error">
|
||||
<li v-for="(validationError, index) of validationErrorsWithDetails" :key="index" class="validation-error">
|
||||
<strong class="field" @click="$emit('scroll-to-field', validationError.group || validationError.field)">
|
||||
<template v-if="validationError.field && validationError.hidden && validationError.group">
|
||||
{{
|
||||
@@ -69,27 +89,13 @@ const validationErrorsWithNames = computed<
|
||||
<template v-else-if="validationError.field">{{ validationError.fieldName }}</template>
|
||||
</strong>
|
||||
<strong>{{ ': ' }}</strong>
|
||||
<template v-if="validationError.customValidationMessage">
|
||||
|
||||
<template v-if="showCustomValidationMessage(validationError)">
|
||||
{{ validationError.customValidationMessage }}
|
||||
<v-icon
|
||||
v-tooltip="
|
||||
validationError.code === 'RECORD_NOT_UNIQUE'
|
||||
? t('validationError.unique', validationError)
|
||||
: t(`validationError.${validationError.type}`, validationError)
|
||||
"
|
||||
small
|
||||
right
|
||||
name="help"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="validationError.code === 'RECORD_NOT_UNIQUE'">
|
||||
{{ t('validationError.unique', validationError) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t(`validationError.${validationError.type}`, validationError) }}
|
||||
</template>
|
||||
<v-icon v-tooltip="getDefaultValidationMessage(validationError)" small right name="help" />
|
||||
</template>
|
||||
|
||||
<template v-else>{{ getDefaultValidationMessage(validationError) }}</template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -241,6 +241,8 @@ function useSelection() {
|
||||
function useURLImport() {
|
||||
const url = ref('');
|
||||
const loading = ref(false);
|
||||
const filesStore = useFilesStore();
|
||||
const newUpload = filesStore.upload();
|
||||
|
||||
const isValidURL = computed(() => {
|
||||
try {
|
||||
@@ -257,6 +259,7 @@ function useURLImport() {
|
||||
if (!isValidURL.value || loading.value) return;
|
||||
|
||||
loading.value = true;
|
||||
newUpload.start(1);
|
||||
|
||||
const data = {
|
||||
...props.preset,
|
||||
@@ -270,6 +273,9 @@ function useURLImport() {
|
||||
data,
|
||||
});
|
||||
|
||||
newUpload.progress.value = 100;
|
||||
newUpload.done.value = 1;
|
||||
|
||||
emitter.emit(Events.upload);
|
||||
|
||||
if (props.multiple) {
|
||||
@@ -284,6 +290,7 @@ function useURLImport() {
|
||||
unexpectedError(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
newUpload.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Alterations, Field, Item, PrimaryKey, Query, Relation } from '@directus
|
||||
import { getEndpoint, isObject } from '@directus/utils';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { jsonToGraphQLQuery } from 'json-to-graphql-query';
|
||||
import { mergeWith } from 'lodash';
|
||||
import { mergeWith, cloneDeep } from 'lodash';
|
||||
import { ComputedRef, MaybeRef, Ref, computed, isRef, ref, unref, watch } from 'vue';
|
||||
import { UsablePermissions, usePermissions } from '../use-permissions';
|
||||
import { getGraphqlQueryFields } from './lib/get-graphql-query-fields';
|
||||
@@ -217,7 +217,7 @@ export function useItem<T extends Item>(
|
||||
|
||||
const newItem: Item = {
|
||||
...(itemData || {}),
|
||||
...edits.value,
|
||||
...cloneDeep(edits.value),
|
||||
};
|
||||
|
||||
clearPrimaryKey(primaryKeyField.value, newItem);
|
||||
|
||||
@@ -108,7 +108,7 @@ const TestComponent = defineComponent({
|
||||
});
|
||||
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
return { value: valueRef, ...useRelationMultiple(valueRef, query, relation, id) };
|
||||
return { value: valueRef, ...useRelationMultiple(valueRef, query, relation, id, ref(null)) };
|
||||
},
|
||||
render: () => h('div'),
|
||||
});
|
||||
@@ -436,7 +436,7 @@ const TestComponentM2A = defineComponent({
|
||||
});
|
||||
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
return { value: valueRef, ...useRelationMultiple(valueRef, query, relation, id) };
|
||||
return { value: valueRef, ...useRelationMultiple(valueRef, query, relation, id, ref(null)) };
|
||||
},
|
||||
render: () => h('div'),
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { RelationM2M } from '@/composables/use-relation-m2m';
|
||||
import { RelationO2M } from '@/composables/use-relation-o2m';
|
||||
import { fetchAll } from '@/utils/fetch-all';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
import { Filter, Item } from '@directus/types';
|
||||
import { ContentVersion, Filter, Item } from '@directus/types';
|
||||
import { getEndpoint, toArray } from '@directus/utils';
|
||||
import { clamp, cloneDeep, get, isEqual, merge } from 'lodash';
|
||||
import { Ref, computed, ref, watch } from 'vue';
|
||||
@@ -32,10 +32,11 @@ export type ChangesItem = {
|
||||
};
|
||||
|
||||
export function useRelationMultiple(
|
||||
value: Ref<Record<string, any> | any[] | undefined>,
|
||||
value: Ref<Record<string, any> | any[] | undefined | null>,
|
||||
previewQuery: Ref<RelationQueryMultiple>,
|
||||
relation: Ref<RelationM2A | RelationM2M | RelationO2M | undefined>,
|
||||
itemId: Ref<string | number | null>,
|
||||
version: Ref<ContentVersion | null>,
|
||||
) {
|
||||
const loading = ref(false);
|
||||
const fetchedItems = ref<Record<string, any>[]>([]);
|
||||
@@ -43,6 +44,18 @@ export function useRelationMultiple(
|
||||
|
||||
const { cleanItem, getPage, isLocalItem, getItemEdits, isEmpty } = useUtil();
|
||||
|
||||
const targetPKField = computed(() => {
|
||||
if (!relation.value) return 'id';
|
||||
|
||||
return relation.value.type === 'o2m'
|
||||
? relation.value.relatedPrimaryKeyField.field
|
||||
: relation.value.junctionPrimaryKeyField.field;
|
||||
});
|
||||
|
||||
const fetchedItemsPKs = computed(() => {
|
||||
return fetchedItems.value.map((item) => item[targetPKField.value]);
|
||||
});
|
||||
|
||||
const _value = computed<ChangesItem>({
|
||||
get() {
|
||||
if (!value.value || Array.isArray(value.value)) {
|
||||
@@ -57,6 +70,13 @@ export function useRelationMultiple(
|
||||
},
|
||||
set(newValue) {
|
||||
if (newValue.create.length === 0 && newValue.update.length === 0 && newValue.delete.length === 0) {
|
||||
const isVersion = version.value !== null;
|
||||
|
||||
if (isVersion) {
|
||||
value.value = fetchedItemsPKs.value;
|
||||
return;
|
||||
}
|
||||
|
||||
value.value = undefined;
|
||||
return;
|
||||
}
|
||||
@@ -118,16 +138,11 @@ export function useRelationMultiple(
|
||||
const displayItems = computed(() => {
|
||||
if (!relation.value) return [];
|
||||
|
||||
const targetPKField =
|
||||
relation.value.type === 'o2m'
|
||||
? relation.value.relatedPrimaryKeyField.field
|
||||
: relation.value.junctionPrimaryKeyField.field;
|
||||
|
||||
const items: DisplayItem[] = fetchedItems.value.map((item: Record<string, any>) => {
|
||||
let edits;
|
||||
|
||||
for (const [index, value] of _value.value.update.entries()) {
|
||||
if (typeof value === 'object' && value[targetPKField] === item[targetPKField]) {
|
||||
if (typeof value === 'object' && value[targetPKField.value] === item[targetPKField.value]) {
|
||||
edits = { index, value };
|
||||
break;
|
||||
}
|
||||
@@ -153,7 +168,7 @@ export function useRelationMultiple(
|
||||
updatedItem.$edits = edits.index;
|
||||
}
|
||||
|
||||
const deleteIndex = _value.value.delete.findIndex((id) => id === item[targetPKField]);
|
||||
const deleteIndex = _value.value.delete.findIndex((id) => id === item[targetPKField.value]);
|
||||
|
||||
if (deleteIndex !== -1) {
|
||||
merge(updatedItem, { $type: 'deleted', $index: deleteIndex });
|
||||
@@ -166,7 +181,7 @@ export function useRelationMultiple(
|
||||
const fetchedItem = fetchedSelectItems.value.find((item) => {
|
||||
switch (relation.value?.type) {
|
||||
case 'o2m':
|
||||
return edit[targetPKField] === item[targetPKField];
|
||||
return edit[targetPKField.value] === item[targetPKField.value];
|
||||
case 'm2m':
|
||||
return (
|
||||
edit[relation.value.junctionField.field][relation.value.relatedPrimaryKeyField.field] ===
|
||||
@@ -262,21 +277,16 @@ export function useRelationMultiple(
|
||||
function remove(...items: DisplayItem[]) {
|
||||
if (!relation.value) return;
|
||||
|
||||
const pkField =
|
||||
relation.value.type === 'o2m'
|
||||
? relation.value.relatedPrimaryKeyField.field
|
||||
: relation.value.junctionPrimaryKeyField.field;
|
||||
|
||||
for (const item of items) {
|
||||
if (item.$type === undefined || item.$index === undefined) {
|
||||
target.value.delete.push(item[pkField]);
|
||||
target.value.delete.push(item[targetPKField.value]);
|
||||
} else if (item.$type === 'created') {
|
||||
target.value.create.splice(item.$index, 1);
|
||||
} else if (item.$type === 'updated') {
|
||||
if (isItemSelected(item)) {
|
||||
target.value.update.splice(item.$index, 1);
|
||||
} else {
|
||||
target.value.delete.push(item[pkField]);
|
||||
target.value.delete.push(item[targetPKField.value]);
|
||||
}
|
||||
} else if (item.$type === 'deleted') {
|
||||
target.value.delete.splice(item.$index, 1);
|
||||
@@ -428,21 +438,17 @@ export function useRelationMultiple(
|
||||
}
|
||||
|
||||
let targetCollection: string;
|
||||
let targetPKField: string;
|
||||
const reverseJunctionField = relation.value.reverseJunctionField.field;
|
||||
|
||||
switch (relation.value.type) {
|
||||
case 'm2a':
|
||||
targetCollection = relation.value.junctionCollection.collection;
|
||||
targetPKField = relation.value.junctionPrimaryKeyField.field;
|
||||
break;
|
||||
case 'm2m':
|
||||
targetCollection = relation.value.junctionCollection.collection;
|
||||
targetPKField = relation.value.junctionPrimaryKeyField.field;
|
||||
break;
|
||||
case 'o2m':
|
||||
targetCollection = relation.value.relatedCollection.collection;
|
||||
targetPKField = relation.value.relatedPrimaryKeyField.field;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -456,13 +462,13 @@ export function useRelationMultiple(
|
||||
params: {
|
||||
search: previewQuery.value.search,
|
||||
aggregate: {
|
||||
count: targetPKField,
|
||||
count: targetPKField.value,
|
||||
},
|
||||
filter,
|
||||
},
|
||||
});
|
||||
|
||||
existingItemCount.value = Number(response.data.data[0].count[targetPKField]);
|
||||
existingItemCount.value = Number(response.data.data[0].count[targetPKField.value]);
|
||||
}
|
||||
|
||||
function useSelected() {
|
||||
@@ -545,13 +551,12 @@ export function useRelationMultiple(
|
||||
if (relation.sortField) fields.add(relation.sortField);
|
||||
|
||||
const targetCollection = relation.relatedCollection.collection;
|
||||
const targetPKField = relation.relatedPrimaryKeyField.field;
|
||||
|
||||
fetchedSelectItems.value = await fetchAll(getEndpoint(targetCollection), {
|
||||
params: {
|
||||
fields: Array.from(fields),
|
||||
filter: {
|
||||
[targetPKField]: {
|
||||
[targetPKField.value]: {
|
||||
_in: selectedOnPage.value.map(getRelatedIDs),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getAssetUrl } from '@/utils/get-asset-url';
|
||||
import { parseFilter } from '@/utils/parse-filter';
|
||||
import DrawerFiles from '@/views/private/components/drawer-files.vue';
|
||||
import DrawerItem from '@/views/private/components/drawer-item.vue';
|
||||
import { Filter } from '@directus/types';
|
||||
import type { ContentVersion, Filter } from '@directus/types';
|
||||
import { deepMap, getFieldsFromTemplate } from '@directus/utils';
|
||||
import { clamp, get, isEmpty, isNil, set } from 'lodash';
|
||||
import { render } from 'micromustache';
|
||||
@@ -21,8 +21,9 @@ const props = withDefaults(
|
||||
primaryKey: string | number;
|
||||
collection: string;
|
||||
field: string;
|
||||
template?: string | null;
|
||||
disabled?: boolean;
|
||||
version: ContentVersion | null;
|
||||
template?: string | null;
|
||||
enableCreate?: boolean;
|
||||
enableSelect?: boolean;
|
||||
folder?: string;
|
||||
@@ -43,7 +44,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { collection, field, primaryKey, limit } = toRefs(props);
|
||||
const { collection, field, primaryKey, limit, version } = toRefs(props);
|
||||
const { relationInfo } = useRelationM2M(collection, field);
|
||||
|
||||
const value = computed({
|
||||
@@ -108,7 +109,7 @@ const {
|
||||
isItemSelected,
|
||||
isLocalItem,
|
||||
getItemEdits,
|
||||
} = useRelationMultiple(value, query, relationInfo, primaryKey);
|
||||
} = useRelationMultiple(value, query, relationInfo, primaryKey, version);
|
||||
|
||||
const { createAllowed, updateAllowed, selectAllowed, deleteAllowed } = useRelationPermissionsM2M(relationInfo);
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ code {
|
||||
font-weight: 500;
|
||||
padding: 2px 4px;
|
||||
font-family: ${cssVar('--theme--fonts--monospace--font-family')}, monospace;
|
||||
background-color: ${cssVar('--theme--background-accent')};
|
||||
background-color: ${cssVar('--theme--background-normal')};
|
||||
border-radius: ${cssVar('--theme--border-radius')};
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
@@ -119,7 +119,7 @@ pre {
|
||||
font-weight: 500;
|
||||
padding: 1em;
|
||||
font-family: ${cssVar('--theme--fonts--monospace--font-family')}, monospace;
|
||||
background-color: ${cssVar('--theme--background-accent')};
|
||||
background-color: ${cssVar('--theme--background-normal')};
|
||||
border-radius: ${cssVar('--theme--border-radius')};
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineInterface } from '@directus/extensions';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import toolbarDefault from './toolbar-default';
|
||||
import PreviewSVG from './preview.svg?raw';
|
||||
|
||||
const InterfaceWYSIWYG = defineAsyncComponent(() => import('./input-rich-text-html.vue'));
|
||||
@@ -20,24 +21,7 @@ export default defineInterface({
|
||||
name: '$t:interfaces.input-rich-text-html.toolbar',
|
||||
type: 'json',
|
||||
schema: {
|
||||
default_value: [
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'numlist',
|
||||
'bullist',
|
||||
'removeformat',
|
||||
'blockquote',
|
||||
'customLink',
|
||||
'customImage',
|
||||
'customMedia',
|
||||
'hr',
|
||||
'code',
|
||||
'fullscreen',
|
||||
],
|
||||
default_value: toolbarDefault,
|
||||
},
|
||||
meta: {
|
||||
width: 'half',
|
||||
@@ -108,6 +92,10 @@ export default defineInterface({
|
||||
value: 'h6',
|
||||
text: '$t:wysiwyg_options.h6',
|
||||
},
|
||||
{
|
||||
value: 'customPre',
|
||||
text: '$t:wysiwyg_options.pre',
|
||||
},
|
||||
{
|
||||
value: 'alignleft',
|
||||
text: '$t:wysiwyg_options.alignleft',
|
||||
@@ -180,6 +168,10 @@ export default defineInterface({
|
||||
value: 'blockquote',
|
||||
text: '$t:wysiwyg_options.blockquote',
|
||||
},
|
||||
{
|
||||
value: 'customInlineCode',
|
||||
text: '$t:wysiwyg_options.codeblock',
|
||||
},
|
||||
{
|
||||
value: 'customLink',
|
||||
text: '$t:wysiwyg_options.link',
|
||||
@@ -204,10 +196,6 @@ export default defineInterface({
|
||||
value: 'hr',
|
||||
text: '$t:wysiwyg_options.hr',
|
||||
},
|
||||
{
|
||||
value: 'code',
|
||||
text: '$t:wysiwyg_options.source_code',
|
||||
},
|
||||
{
|
||||
value: 'fullscreen',
|
||||
text: '$t:wysiwyg_options.fullscreen',
|
||||
@@ -220,6 +208,10 @@ export default defineInterface({
|
||||
value: 'ltr rtl',
|
||||
text: '$t:wysiwyg_options.directionality',
|
||||
},
|
||||
{
|
||||
value: 'code',
|
||||
text: '$t:wysiwyg_options.source_code',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,10 +8,13 @@ import { cloneDeep, isEqual } from 'lodash';
|
||||
import { ComponentPublicInstance, computed, onMounted, ref, toRefs, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import getEditorStyles from './get-editor-styles';
|
||||
import toolbarDefault from './toolbar-default';
|
||||
import useImage from './useImage';
|
||||
import useLink from './useLink';
|
||||
import useMedia from './useMedia';
|
||||
import useSourceCode from './useSourceCode';
|
||||
import usePre from './usePre';
|
||||
import useInlineCode from './useInlineCode';
|
||||
import tinymce from 'tinymce/tinymce';
|
||||
|
||||
import 'tinymce/skins/ui/oxide/skin.css';
|
||||
@@ -58,23 +61,7 @@ const props = withDefaults(
|
||||
direction?: string;
|
||||
}>(),
|
||||
{
|
||||
toolbar: () => [
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'numlist',
|
||||
'bullist',
|
||||
'removeformat',
|
||||
'blockquote',
|
||||
'customLink',
|
||||
'customImage',
|
||||
'customMedia',
|
||||
'code',
|
||||
'fullscreen',
|
||||
],
|
||||
toolbar: () => toolbarDefault,
|
||||
font: 'sans-serif',
|
||||
customFormats: () => [],
|
||||
},
|
||||
@@ -127,6 +114,9 @@ const { linkButton, linkDrawerOpen, closeLinkDrawer, saveLink, linkSelection, is
|
||||
|
||||
const { codeDrawerOpen, code, closeCodeDrawer, saveCode, sourceCodeButton } = useSourceCode(editorRef);
|
||||
|
||||
const { preButton } = usePre(editorRef);
|
||||
const { inlineCodeButton } = useInlineCode(editorRef);
|
||||
|
||||
const internalValue = computed({
|
||||
get() {
|
||||
return props.value || '';
|
||||
@@ -180,7 +170,9 @@ const editorOptions = computed(() => {
|
||||
.replace(/^link$/g, 'customLink')
|
||||
.replace(/^media$/g, 'customMedia')
|
||||
.replace(/^code$/g, 'customCode')
|
||||
.replace(/^image$/g, 'customImage'),
|
||||
.replace(/^image$/g, 'customImage')
|
||||
.replace(/^pre$/g, 'customPre')
|
||||
.replace(/^inlinecode$/g, 'customInlineCode'),
|
||||
)
|
||||
.join(' ');
|
||||
|
||||
@@ -223,6 +215,7 @@ const editorOptions = computed(() => {
|
||||
paste_data_images: false,
|
||||
setup,
|
||||
language: i18n.global.locale.value,
|
||||
ui_mode: 'split',
|
||||
...(props.tinymceOverrides && cloneDeep(props.tinymceOverrides)),
|
||||
};
|
||||
});
|
||||
@@ -268,9 +261,12 @@ function setup(editor: any) {
|
||||
|
||||
const linkShortcut = 'meta+k';
|
||||
|
||||
editor.ui.registry.addToggleButton('customPre', preButton);
|
||||
editor.ui.registry.addToggleButton('customImage', imageButton);
|
||||
editor.ui.registry.addToggleButton('customMedia', mediaButton);
|
||||
editor.ui.registry.addToggleButton('customLink', { ...linkButton, shortcut: linkShortcut });
|
||||
|
||||
editor.ui.registry.addToggleButton('customInlineCode', inlineCodeButton);
|
||||
editor.ui.registry.addButton('customCode', sourceCodeButton);
|
||||
|
||||
editor.on('init', function () {
|
||||
|
||||
18
app/src/interfaces/input-rich-text-html/toolbar-default.ts
Normal file
18
app/src/interfaces/input-rich-text-html/toolbar-default.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export default [
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'numlist',
|
||||
'bullist',
|
||||
'removeformat',
|
||||
'blockquote',
|
||||
'customLink',
|
||||
'customImage',
|
||||
'customMedia',
|
||||
'hr',
|
||||
'code',
|
||||
'fullscreen',
|
||||
];
|
||||
63
app/src/interfaces/input-rich-text-html/useInlineCode.ts
Normal file
63
app/src/interfaces/input-rich-text-html/useInlineCode.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Ref } from 'vue';
|
||||
import { i18n } from '@/lang';
|
||||
|
||||
type InlineCodeButton = {
|
||||
icon: string;
|
||||
tooltip: string;
|
||||
onAction: () => void;
|
||||
onSetup: (api: { setActive: (isActive: boolean) => void }) => () => void;
|
||||
};
|
||||
|
||||
type UsableInlineCode = {
|
||||
inlineCodeButton: InlineCodeButton;
|
||||
};
|
||||
|
||||
function isInlineCode(node: Node): boolean {
|
||||
// check if node is a code node, or if it has a code node as a child
|
||||
return node.nodeName === 'CODE' || (node as Element).querySelector('code') !== null;
|
||||
}
|
||||
|
||||
export default function useInlineCode(editor: Ref<any>): UsableInlineCode {
|
||||
let keydownHandler: ((event: KeyboardEvent) => void) | null = null;
|
||||
|
||||
const inlineCodeButton: InlineCodeButton = {
|
||||
tooltip: i18n.global.t('wysiwyg_options.codeblock'),
|
||||
icon: 'code-sample',
|
||||
|
||||
onAction: () => {
|
||||
editor.value.execCommand('mceToggleFormat', false, 'code');
|
||||
},
|
||||
|
||||
onSetup: (api) => {
|
||||
const updateActiveState = () => {
|
||||
const isInlineCode = editor.value.formatter.match('code');
|
||||
|
||||
api.setActive(isInlineCode);
|
||||
};
|
||||
|
||||
updateActiveState();
|
||||
|
||||
const codeFormatChangedUnbind = editor.value.formatter.formatChanged('code', updateActiveState).unbind;
|
||||
|
||||
keydownHandler = (event: KeyboardEvent) => {
|
||||
const currentNode = editor.value.selection.getNode();
|
||||
if (!isInlineCode(currentNode)) return;
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
|
||||
editor.value.execCommand('removeformat');
|
||||
}
|
||||
};
|
||||
|
||||
editor.value.on('keydown', keydownHandler);
|
||||
|
||||
return () => {
|
||||
if (codeFormatChangedUnbind) codeFormatChangedUnbind();
|
||||
if (keydownHandler) editor.value.off('keydown', keydownHandler);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return { inlineCodeButton };
|
||||
}
|
||||
132
app/src/interfaces/input-rich-text-html/usePre.ts
Normal file
132
app/src/interfaces/input-rich-text-html/usePre.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { i18n } from '@/lang';
|
||||
import { Ref } from 'vue';
|
||||
|
||||
type PreButton = {
|
||||
text: string;
|
||||
tooltip: string;
|
||||
onAction: () => void;
|
||||
onSetup: (api: { setActive: (isActive: boolean) => void }) => () => void;
|
||||
};
|
||||
|
||||
type UsablePre = {
|
||||
preButton: PreButton;
|
||||
};
|
||||
|
||||
function isPre(node: Node): boolean {
|
||||
return node.nodeName === 'PRE';
|
||||
}
|
||||
|
||||
export default function usePre(editor: Ref<any>): UsablePre {
|
||||
let keydownHandler: ((event: KeyboardEvent) => void) | null = null;
|
||||
|
||||
const preButton: PreButton = {
|
||||
tooltip: i18n.global.t('wysiwyg_options.pre'),
|
||||
text: 'Pre',
|
||||
|
||||
onAction: () => {
|
||||
// Remove all existing formatting before applying code formatting
|
||||
editor.value.execCommand('removeformat');
|
||||
editor.value.execCommand('mceToggleFormat', false, 'pre');
|
||||
},
|
||||
|
||||
onSetup: (api) => {
|
||||
const updateActiveState = () => {
|
||||
const isPre = editor.value.formatter.match('pre');
|
||||
api.setActive(isPre);
|
||||
};
|
||||
|
||||
updateActiveState();
|
||||
|
||||
const preFormatUnbind = editor.value.formatter.formatChanged('pre', updateActiveState).unbind;
|
||||
|
||||
keydownHandler = (event: KeyboardEvent) => {
|
||||
const currentNode = editor.value.selection.getNode();
|
||||
|
||||
if (!isPre(currentNode)) return;
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
|
||||
if (handleTripleEnterInPre(editor.value, currentNode)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === 'Backspace') {
|
||||
// if the current node is a pre node, and it is empty, remove it
|
||||
if (
|
||||
(isPre(currentNode) && currentNode.childNodes.length === 0) ||
|
||||
(currentNode.childNodes.length === 1 && currentNode.childNodes[0].nodeName === 'BR')
|
||||
) {
|
||||
editor.value.dom.remove(currentNode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
editor.value.on('keydown', keydownHandler);
|
||||
|
||||
return () => {
|
||||
if (preFormatUnbind) preFormatUnbind();
|
||||
if (keydownHandler) editor.value.off('keydown', keydownHandler);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return { preButton };
|
||||
}
|
||||
|
||||
function handleTripleEnterInPre(editorInstance: any, currentNode: Node): boolean {
|
||||
const preNode = editorInstance.dom.is(currentNode, 'pre')
|
||||
? currentNode
|
||||
: editorInstance.dom.getParent(currentNode, 'pre');
|
||||
|
||||
if (preNode && hasTrailingBrs(preNode, 4)) {
|
||||
removeTrailingBrs(preNode, 3);
|
||||
insertParagraphAfter(editorInstance, preNode);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasTrailingBrs(node: Node, count: number): boolean {
|
||||
const childNodes = Array.from(node.childNodes);
|
||||
let trailingBrCount = 0;
|
||||
|
||||
for (let i = childNodes.length - 1; i >= 0; i--) {
|
||||
const child = childNodes[i];
|
||||
if (!child) break;
|
||||
|
||||
if (child.nodeName === 'BR') {
|
||||
trailingBrCount++;
|
||||
} else if (child.nodeType === Node.TEXT_NODE && child.textContent === '') {
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return trailingBrCount >= count;
|
||||
}
|
||||
|
||||
function removeTrailingBrs(node: Node, maxRemove: number) {
|
||||
let removedCount = 0;
|
||||
|
||||
while (removedCount < maxRemove) {
|
||||
const lastChild = node.lastChild;
|
||||
|
||||
if (lastChild && lastChild.nodeName === 'BR') {
|
||||
node.removeChild(lastChild);
|
||||
removedCount++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function insertParagraphAfter(editorInstance: any, referenceNode: Node) {
|
||||
const newParagraph = editorInstance.dom.create('p', {}, '<br>');
|
||||
editorInstance.dom.insertAfter(newParagraph, referenceNode);
|
||||
editorInstance.selection.setCursorLocation(newParagraph, 0);
|
||||
editorInstance.nodeChanged();
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { hideDragImage } from '@/utils/hide-drag-image';
|
||||
import { renderStringTemplate } from '@/utils/render-string-template';
|
||||
import DrawerCollection from '@/views/private/components/drawer-collection.vue';
|
||||
import DrawerItem from '@/views/private/components/drawer-item.vue';
|
||||
import { Filter } from '@directus/types';
|
||||
import type { ContentVersion, Filter } from '@directus/types';
|
||||
import { getFieldsFromTemplate } from '@directus/utils';
|
||||
import { clamp, get, isEmpty, isNil, set } from 'lodash';
|
||||
import { computed, ref, toRefs, unref, watch } from 'vue';
|
||||
@@ -23,6 +23,7 @@ const props = withDefaults(
|
||||
collection: string;
|
||||
field: string;
|
||||
disabled?: boolean;
|
||||
version: ContentVersion | null;
|
||||
enableCreate?: boolean;
|
||||
enableSelect?: boolean;
|
||||
limit?: number;
|
||||
@@ -41,7 +42,7 @@ const props = withDefaults(
|
||||
|
||||
const emit = defineEmits(['input']);
|
||||
const { t, te } = useI18n();
|
||||
const { collection, field, primaryKey, limit } = toRefs(props);
|
||||
const { collection, field, primaryKey, limit, version } = toRefs(props);
|
||||
const { relationInfo } = useRelationM2A(collection, field);
|
||||
|
||||
const value = computed({
|
||||
@@ -121,7 +122,7 @@ const {
|
||||
isItemSelected,
|
||||
isLocalItem,
|
||||
getItemEdits,
|
||||
} = useRelationMultiple(value, query, relationInfo, primaryKey);
|
||||
} = useRelationMultiple(value, query, relationInfo, primaryKey, version);
|
||||
|
||||
function sortItems(items: DisplayItem[]) {
|
||||
const info = relationInfo.value;
|
||||
@@ -131,7 +132,7 @@ function sortItems(items: DisplayItem[]) {
|
||||
const sortedItems = items.map((item, index) => {
|
||||
const junctionId = item?.[info.junctionPrimaryKeyField.field];
|
||||
const collection = item?.[info.collectionField.field];
|
||||
const pkField = info.relationPrimaryKeyFields[collection].field;
|
||||
const pkField = info.relationPrimaryKeyFields[collection]!.field;
|
||||
const relatedId = item?.[info.junctionField.field]?.[pkField];
|
||||
|
||||
const changes: Record<string, any> = {
|
||||
@@ -187,7 +188,7 @@ function editItem(item: DisplayItem) {
|
||||
if (!relationInfo.value) return;
|
||||
|
||||
const relationPkField =
|
||||
relationInfo.value.relationPrimaryKeyFields[item[relationInfo.value.collectionField.field]].field;
|
||||
relationInfo.value.relationPrimaryKeyFields[item[relationInfo.value.collectionField.field]]!.field;
|
||||
|
||||
const junctionField = relationInfo.value.junctionField.field;
|
||||
const junctionPkField = relationInfo.value.junctionPrimaryKeyField.field;
|
||||
@@ -297,7 +298,7 @@ const customFilter = computed(() => {
|
||||
|
||||
const selectedPrimaryKeys = selected.value.reduce(
|
||||
(acc, item) => {
|
||||
const relatedPKField = info.relationPrimaryKeyFields[item[info.collectionField.field]].field;
|
||||
const relatedPKField = info.relationPrimaryKeyFields[item[info.collectionField.field]]!.field;
|
||||
if (item[info.collectionField.field] === selectingFrom.value) acc.push(item[junctionField][relatedPKField]);
|
||||
return acc;
|
||||
},
|
||||
@@ -306,7 +307,7 @@ const customFilter = computed(() => {
|
||||
|
||||
if (selectedPrimaryKeys.length > 0) {
|
||||
filter._and.push({
|
||||
[info.relationPrimaryKeyFields[selectingFrom.value].field]: {
|
||||
[info.relationPrimaryKeyFields[selectingFrom.value]!.field]: {
|
||||
_nin: selectedPrimaryKeys,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ import DrawerBatch from '@/views/private/components/drawer-batch.vue';
|
||||
import DrawerCollection from '@/views/private/components/drawer-collection.vue';
|
||||
import DrawerItem from '@/views/private/components/drawer-item.vue';
|
||||
import SearchInput from '@/views/private/components/search-input.vue';
|
||||
import { Filter } from '@directus/types';
|
||||
import type { ContentVersion, Filter } from '@directus/types';
|
||||
import { deepMap, getFieldsFromTemplate } from '@directus/utils';
|
||||
import { clamp, get, isEmpty, isNil, merge, set } from 'lodash';
|
||||
import { render } from 'micromustache';
|
||||
@@ -29,6 +29,7 @@ const props = withDefaults(
|
||||
collection: string;
|
||||
field: string;
|
||||
width: string;
|
||||
version: ContentVersion | null;
|
||||
layout?: LAYOUTS;
|
||||
tableSpacing?: 'compact' | 'cozy' | 'comfortable';
|
||||
fields?: Array<string>;
|
||||
@@ -64,7 +65,7 @@ const props = withDefaults(
|
||||
|
||||
const emit = defineEmits(['input']);
|
||||
const { t, n } = useI18n();
|
||||
const { collection, field, primaryKey } = toRefs(props);
|
||||
const { collection, field, primaryKey, version } = toRefs(props);
|
||||
const { relationInfo } = useRelationM2M(collection, field);
|
||||
const fieldsStore = useFieldsStore();
|
||||
|
||||
@@ -177,7 +178,7 @@ const {
|
||||
isItemSelected,
|
||||
isLocalItem,
|
||||
getItemEdits,
|
||||
} = useRelationMultiple(value, query, relationInfo, primaryKey);
|
||||
} = useRelationMultiple(value, query, relationInfo, primaryKey, version);
|
||||
|
||||
const { createAllowed, updateAllowed, deleteAllowed, selectAllowed } = useRelationPermissionsM2M(relationInfo);
|
||||
|
||||
@@ -229,7 +230,7 @@ watch(
|
||||
return {
|
||||
text: field.name,
|
||||
value: key,
|
||||
width: contentWidth[key] < 10 ? contentWidth[key] * 16 + 10 : 160,
|
||||
width: contentWidth[key] !== undefined && contentWidth[key] < 10 ? contentWidth[key] * 16 + 10 : 160,
|
||||
sortable: !['json'].includes(field.type),
|
||||
};
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRelationO2M } from '@/composables/use-relation-o2m';
|
||||
import { addRelatedPrimaryKeyToFields } from '@/utils/add-related-primary-key-to-fields';
|
||||
import { adjustFieldsForDisplays } from '@/utils/adjust-fields-for-displays';
|
||||
import { parseFilter } from '@/utils/parse-filter';
|
||||
import { Filter } from '@directus/types';
|
||||
import type { ContentVersion, Filter } from '@directus/types';
|
||||
import { deepMap, getFieldsFromTemplate } from '@directus/utils';
|
||||
import { render } from 'micromustache';
|
||||
import { computed, inject, ref, toRefs } from 'vue';
|
||||
@@ -14,11 +14,12 @@ import NestedDraggable from './nested-draggable.vue';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value?: (number | string | Record<string, any>)[] | Record<string, any>;
|
||||
displayTemplate?: string;
|
||||
disabled?: boolean;
|
||||
collection: string;
|
||||
field: string;
|
||||
primaryKey: string | number;
|
||||
version: ContentVersion | null;
|
||||
displayTemplate?: string;
|
||||
enableCreate?: boolean;
|
||||
enableSelect?: boolean;
|
||||
filter?: Filter | null;
|
||||
@@ -135,6 +136,7 @@ const fields = computed(() => {
|
||||
:enable-select="enableSelect"
|
||||
:custom-filter="customFilter"
|
||||
:items-moved="itemsMoved"
|
||||
:version
|
||||
root
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { RelationO2M } from '@/composables/use-relation-o2m';
|
||||
import { hideDragImage } from '@/utils/hide-drag-image';
|
||||
import DrawerCollection from '@/views/private/components/drawer-collection.vue';
|
||||
import DrawerItem from '@/views/private/components/drawer-item.vue';
|
||||
import { Filter } from '@directus/types';
|
||||
import type { ContentVersion, Filter } from '@directus/types';
|
||||
import { moveInArray } from '@directus/utils';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { computed, ref, toRefs } from 'vue';
|
||||
@@ -41,11 +41,12 @@ type ChangeEvent =
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: ChangesItem;
|
||||
template: string;
|
||||
disabled?: boolean;
|
||||
collection: string;
|
||||
field: string;
|
||||
primaryKey: string | number;
|
||||
disabled?: boolean;
|
||||
version: ContentVersion | null;
|
||||
template: string;
|
||||
filter?: Filter | null;
|
||||
fields: string[];
|
||||
relationInfo: RelationO2M;
|
||||
@@ -76,7 +77,7 @@ const value = computed<ChangesItem | any[]>({
|
||||
},
|
||||
});
|
||||
|
||||
const { collection, field, primaryKey, relationInfo, root, fields, template, customFilter } = toRefs(props);
|
||||
const { collection, field, primaryKey, relationInfo, root, fields, template, customFilter, version } = toRefs(props);
|
||||
|
||||
const drag = ref(false);
|
||||
const open = ref<Record<string, boolean>>({});
|
||||
@@ -91,7 +92,7 @@ const query = computed<RelationQueryMultiple>(() => ({
|
||||
}));
|
||||
|
||||
const { displayItems, loading, create, update, remove, select, cleanItem, isLocalItem, getItemEdits } =
|
||||
useRelationMultiple(value, query, relationInfo, primaryKey);
|
||||
useRelationMultiple(value, query, relationInfo, primaryKey, version);
|
||||
|
||||
const selectDrawer = ref(false);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import DrawerBatch from '@/views/private/components/drawer-batch.vue';
|
||||
import DrawerCollection from '@/views/private/components/drawer-collection.vue';
|
||||
import DrawerItem from '@/views/private/components/drawer-item.vue';
|
||||
import SearchInput from '@/views/private/components/search-input.vue';
|
||||
import { Filter } from '@directus/types';
|
||||
import type { ContentVersion, Filter } from '@directus/types';
|
||||
import { deepMap, getFieldsFromTemplate } from '@directus/utils';
|
||||
import { clamp, get, isEmpty, isNil } from 'lodash';
|
||||
import { render } from 'micromustache';
|
||||
@@ -29,11 +29,12 @@ const props = withDefaults(
|
||||
collection: string;
|
||||
field: string;
|
||||
width: string;
|
||||
disabled?: boolean;
|
||||
version: ContentVersion | null;
|
||||
layout?: LAYOUTS;
|
||||
tableSpacing?: 'compact' | 'cozy' | 'comfortable';
|
||||
fields?: Array<string>;
|
||||
template?: string | null;
|
||||
disabled?: boolean;
|
||||
enableCreate?: boolean;
|
||||
enableSelect?: boolean;
|
||||
filter?: Filter | null;
|
||||
@@ -61,7 +62,7 @@ const props = withDefaults(
|
||||
|
||||
const emit = defineEmits(['input']);
|
||||
const { t, n } = useI18n();
|
||||
const { collection, field, primaryKey } = toRefs(props);
|
||||
const { collection, field, primaryKey, version } = toRefs(props);
|
||||
const { relationInfo } = useRelationO2M(collection, field);
|
||||
const fieldsStore = useFieldsStore();
|
||||
|
||||
@@ -148,7 +149,7 @@ const {
|
||||
isItemSelected,
|
||||
isLocalItem,
|
||||
getItemEdits,
|
||||
} = useRelationMultiple(value, query, relationInfo, primaryKey);
|
||||
} = useRelationMultiple(value, query, relationInfo, primaryKey, version);
|
||||
|
||||
const { createAllowed, deleteAllowed, updateAllowed } = useRelationPermissionsO2M(relationInfo);
|
||||
|
||||
@@ -200,7 +201,7 @@ watch(
|
||||
return {
|
||||
text: field.name,
|
||||
value: key,
|
||||
width: contentWidth[key] < 10 ? contentWidth[key] * 16 + 10 : 160,
|
||||
width: contentWidth[key] !== undefined && contentWidth[key] < 10 ? contentWidth[key] * 16 + 10 : 160,
|
||||
sortable: !['json'].includes(field.type),
|
||||
};
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useInjectNestedValidation } from '@/composables/use-nested-validation';
|
||||
import vTooltip from '@/directives/tooltip';
|
||||
import { useFieldsStore } from '@/stores/fields';
|
||||
import { fetchAll } from '@/utils/fetch-all';
|
||||
import type { ContentVersion } from '@directus/types';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
import { getEndpoint } from '@directus/utils';
|
||||
import { isNil } from 'lodash';
|
||||
@@ -28,6 +29,7 @@ const props = withDefaults(
|
||||
value: (number | string | Record<string, any>)[] | Record<string, any> | null;
|
||||
autofocus?: boolean;
|
||||
disabled?: boolean;
|
||||
version: ContentVersion | null;
|
||||
}>(),
|
||||
{
|
||||
languageField: null,
|
||||
@@ -50,7 +52,7 @@ const value = computed({
|
||||
},
|
||||
});
|
||||
|
||||
const { collection, field, primaryKey } = toRefs(props);
|
||||
const { collection, field, primaryKey, version } = toRefs(props);
|
||||
const { relationInfo } = useRelationM2M(collection, field);
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
@@ -107,7 +109,7 @@ const {
|
||||
loading: itemsLoading,
|
||||
fetchedItems,
|
||||
getItemEdits,
|
||||
} = useRelationMultiple(value, query, relationInfo, primaryKey);
|
||||
} = useRelationMultiple(value, query, relationInfo, primaryKey, version);
|
||||
|
||||
useNestedValidation();
|
||||
|
||||
|
||||
@@ -1123,6 +1123,7 @@ wysiwyg_options:
|
||||
h4: Heading 4
|
||||
h5: Heading 5
|
||||
h6: Heading 6
|
||||
pre: Preformatted
|
||||
fontselect: Select Font
|
||||
fontsizeselect: Select Font Size
|
||||
indent: Indent
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import api from '@/api';
|
||||
import { useLayoutClickHandler } from '@/composables/use-layout-click-handler';
|
||||
import { useServerStore } from '@/stores/server';
|
||||
import { adjustFieldsForDisplays } from '@/utils/adjust-fields-for-displays';
|
||||
import { formatItemsCountRelative } from '@/utils/format-items-count';
|
||||
import { getFullcalendarLocale } from '@/utils/get-fullcalendar-locale';
|
||||
import { renderDisplayStringTemplate } from '@/utils/render-string-template';
|
||||
@@ -108,7 +109,8 @@ export default defineLayout<LayoutOptions>({
|
||||
if (template.value) fields.push(...getFieldsFromTemplate(template.value));
|
||||
if (startDateField.value) fields.push(startDateField.value);
|
||||
if (endDateField.value) fields.push(endDateField.value);
|
||||
return fields;
|
||||
|
||||
return [...fields, ...adjustFieldsForDisplays(fields, collection.value!)];
|
||||
});
|
||||
|
||||
const limit = computed(() => (info.queryLimit?.max && info.queryLimit.max !== -1 ? info.queryLimit.max : 1000));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import api from '@/api';
|
||||
import { appRecommendedPermissions } from '@/app-permissions.js';
|
||||
import { appAccessMinimalPermissions } from '@directus/system-data';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
import type { Ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
@@ -33,7 +33,7 @@ export function useSave({ name, adminAccess, appAccess }: UseSaveOptions) {
|
||||
if (appAccess.value === true && adminAccess.value === false) {
|
||||
await api.post(
|
||||
'/permissions',
|
||||
appRecommendedPermissions.map((permission) => ({
|
||||
appAccessMinimalPermissions.map((permission) => ({
|
||||
...permission,
|
||||
policy: policyResponse.data.data.id,
|
||||
})),
|
||||
|
||||
@@ -16,10 +16,8 @@ import { useI18n } from 'vue-i18n';
|
||||
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';
|
||||
import UsersNavigation from '../components/navigation.vue';
|
||||
import useNavigation from '../composables/use-navigation';
|
||||
|
||||
type Item = {
|
||||
[field: string]: any;
|
||||
};
|
||||
import { logout } from '@/auth';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
|
||||
const props = defineProps<{ role?: string }>();
|
||||
|
||||
@@ -29,9 +27,10 @@ const { t } = useI18n();
|
||||
const { roles } = useNavigation(role);
|
||||
const userInviteModalActive = ref(false);
|
||||
const serverStore = useServerStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const layoutRef = ref();
|
||||
const selection = ref<Item[]>([]);
|
||||
const selection = ref<string[]>([]);
|
||||
|
||||
const { layout, layoutOptions, layoutQuery, filter, search, resetPreset } = usePreset(ref('directus_users'));
|
||||
const { addNewLink } = useLinks();
|
||||
@@ -108,6 +107,19 @@ function useBatch() {
|
||||
data: batchPrimaryKeys,
|
||||
});
|
||||
|
||||
// Check if the current user was among the deleted users
|
||||
const currentUserId = userStore.currentUser && 'id' in userStore.currentUser ? userStore.currentUser.id : null;
|
||||
|
||||
if (
|
||||
currentUserId &&
|
||||
batchPrimaryKeys.some((key) => {
|
||||
return key === currentUserId;
|
||||
})
|
||||
) {
|
||||
await logout();
|
||||
return;
|
||||
}
|
||||
|
||||
await refresh();
|
||||
|
||||
selection.value = [];
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import UsersNavigation from '../components/navigation.vue';
|
||||
import UserInfoSidebarDetail from '../components/user-info-sidebar-detail.vue';
|
||||
import { logout } from '@/auth';
|
||||
|
||||
const props = defineProps<{
|
||||
primaryKey: string;
|
||||
@@ -205,6 +206,15 @@ async function deleteAndQuit() {
|
||||
if (deleting.value) return;
|
||||
|
||||
try {
|
||||
const currentUserId = userStore.currentUser && 'id' in userStore.currentUser ? userStore.currentUser.id : null;
|
||||
|
||||
// If the deleted user is the current user, we want to log them out
|
||||
if (currentUserId && currentUserId === item.value?.id) {
|
||||
await remove();
|
||||
await logout();
|
||||
return;
|
||||
}
|
||||
|
||||
await remove();
|
||||
edits.value = {};
|
||||
router.replace(`/users`);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import api, { RequestError } from '@/api';
|
||||
import { login } from '@/auth';
|
||||
import { translateAPIError } from '@/lang';
|
||||
import { useServerStore } from '@/stores/server';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { ErrorCode } from '@directus/errors';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
type Credentials = {
|
||||
email: string;
|
||||
@@ -11,6 +15,9 @@ type Credentials = {
|
||||
};
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const serverStore = useServerStore();
|
||||
const userStore = useUserStore();
|
||||
const isLoading = ref(false);
|
||||
const email = ref<string | null>(null);
|
||||
const password = ref<string | null>(null);
|
||||
@@ -20,6 +27,8 @@ const emit = defineEmits<{
|
||||
wasSuccessful: [boolean];
|
||||
}>();
|
||||
|
||||
const requiresEmailVerification = computed(() => serverStore.info.project?.public_registration_verify_email);
|
||||
|
||||
const errorFormatted = computed(() => {
|
||||
// Show "Wrong username or password" for wrongly formatted emails as well
|
||||
if (error.value === ErrorCode.InvalidPayload) {
|
||||
@@ -52,7 +61,20 @@ async function onSubmit() {
|
||||
|
||||
await api.post('/users/register', credentials);
|
||||
|
||||
emit('wasSuccessful', true);
|
||||
// If email verification is not required, automatically log in the user
|
||||
if (!requiresEmailVerification.value) {
|
||||
await login({ credentials });
|
||||
const currentUser = userStore.currentUser;
|
||||
|
||||
if (currentUser && 'id' in currentUser) {
|
||||
router.push(`/users/${currentUser.id}`);
|
||||
} else {
|
||||
router.push('/login');
|
||||
}
|
||||
} else {
|
||||
// If email verification is required, show success message
|
||||
emit('wasSuccessful', true);
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.errors?.[0]?.extensions?.code || err;
|
||||
emit('wasSuccessful', false);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { Field, PrimaryKey } from '@directus/types';
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import ValidationErrors from '@/components/v-form/validation-errors.vue';
|
||||
import FilePreviewReplace from '@/views/private/components/file-preview-replace.vue';
|
||||
import type { Field, PrimaryKey } from '@directus/types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const {
|
||||
collection,
|
||||
@@ -13,6 +14,7 @@ const {
|
||||
initialValues,
|
||||
fields,
|
||||
junctionFieldLocation = 'bottom',
|
||||
relatedPrimaryKeyField,
|
||||
} = defineProps<{
|
||||
collection: string;
|
||||
primaryKey: PrimaryKey | null;
|
||||
@@ -26,15 +28,15 @@ const {
|
||||
junctionFieldLocation?: string;
|
||||
relatedCollectionFields: Field[];
|
||||
relatedPrimaryKey: PrimaryKey;
|
||||
relatedPrimaryKeyField: string | null;
|
||||
refresh: () => void;
|
||||
}>();
|
||||
|
||||
const internalEdits = defineModel<Record<string, any>>('internal-edits');
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { mainInitialValues, junctionInitialValues } = useInitialValues();
|
||||
const { file } = useFile();
|
||||
|
||||
const { scrollToField } = useValidationScrollToField();
|
||||
|
||||
const swapFormOrder = computed(() => junctionFieldLocation === 'top');
|
||||
@@ -48,15 +50,37 @@ function setRelationEdits(edits: any) {
|
||||
internalEdits.value[junctionField] = edits;
|
||||
}
|
||||
|
||||
function useInitialValues() {
|
||||
const mainInitialValues = computed(() => {
|
||||
if (!initialValues) return null;
|
||||
if (!junctionField || !initialValues[junctionField] || !relatedPrimaryKeyField) return initialValues;
|
||||
|
||||
const tempInitialValues = cloneDeep(initialValues);
|
||||
tempInitialValues[junctionField] = initialValues[junctionField][relatedPrimaryKeyField];
|
||||
|
||||
return tempInitialValues;
|
||||
});
|
||||
|
||||
const junctionInitialValues = computed(() => {
|
||||
if (!initialValues || !junctionField || !initialValues[junctionField]) return null;
|
||||
return initialValues[junctionField];
|
||||
});
|
||||
|
||||
return {
|
||||
mainInitialValues,
|
||||
junctionInitialValues,
|
||||
};
|
||||
}
|
||||
|
||||
function useFile() {
|
||||
const isDirectusFiles = computed(() => {
|
||||
return collection === 'directus_files' || relatedCollection === 'directus_files';
|
||||
});
|
||||
|
||||
const file = computed(() => {
|
||||
if (isDirectusFiles.value === false || !initialValues) return null;
|
||||
if (isDirectusFiles.value === false) return null;
|
||||
|
||||
const fileData = junctionField ? initialValues?.[junctionField] : initialValues;
|
||||
const fileData = junctionField ? junctionInitialValues.value : mainInitialValues.value;
|
||||
|
||||
return fileData || null;
|
||||
});
|
||||
@@ -107,7 +131,7 @@ function useValidationScrollToField() {
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
:show-no-visible-fields="false"
|
||||
:initial-values="initialValues?.[junctionField]"
|
||||
:initial-values="junctionInitialValues"
|
||||
:autofocus="!swapFormOrder"
|
||||
:show-divider="!swapFormOrder && hasVisibleFieldsJunction"
|
||||
:primary-key="relatedPrimaryKey"
|
||||
@@ -121,7 +145,7 @@ function useValidationScrollToField() {
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
:show-no-visible-fields="false"
|
||||
:initial-values="initialValues"
|
||||
:initial-values="mainInitialValues"
|
||||
:autofocus="swapFormOrder"
|
||||
:show-divider="swapFormOrder && hasVisibleFieldsRelated"
|
||||
:primary-key="primaryKey"
|
||||
|
||||
@@ -232,6 +232,7 @@ const overlayItemContentProps = computed(() => {
|
||||
junctionFieldLocation: props.junctionFieldLocation,
|
||||
relatedCollectionFields: relatedCollectionFields.value,
|
||||
relatedPrimaryKey: props.relatedPrimaryKey,
|
||||
relatedPrimaryKeyField: relatedPrimaryKeyField.value?.field ?? null,
|
||||
refresh,
|
||||
};
|
||||
});
|
||||
@@ -423,7 +424,7 @@ function useActions() {
|
||||
}
|
||||
|
||||
function validateForm({ defaultValues, existingValues, editsToValidate, fieldsToValidate }: Record<string, any>) {
|
||||
return validateItem(merge({}, defaultValues, existingValues, editsToValidate), fieldsToValidate, isNew.value);
|
||||
return validateItem(merge({}, defaultValues, existingValues, editsToValidate), fieldsToValidate, isNew.value, true);
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
||||
@@ -217,3 +217,5 @@
|
||||
- Mehdi-YC
|
||||
- MrGreenTea
|
||||
- smgrol
|
||||
- Abdallah-Awwad
|
||||
- DantonMariano
|
||||
|
||||
@@ -683,6 +683,7 @@ fields:
|
||||
- field: accepted_terms
|
||||
interface: boolean
|
||||
width: half
|
||||
hidden: true
|
||||
options:
|
||||
label: $t:accepted
|
||||
special:
|
||||
|
||||
Reference in New Issue
Block a user