Skip to content

Commit

Permalink
Release 5.9.1
Browse files Browse the repository at this point in the history
### Changelog:
* Feature(frontend): Allow selecting multiple values in `DeepFkField`.
* Fix(frontend): Form submission when `ListField` is used.
* Fix(frontend): Read only fields validation.
* Chore(frontend): Refactor `ArrayField` to prevent circular imports. 

Closes: vst/vst-utils#635+
Closes: vst/vst-utils#636+

See merge request vst/vst-utils!632
  • Loading branch information
onegreyonewhite committed Feb 20, 2024
2 parents fd89925 + 892d344 commit e7d0618
Show file tree
Hide file tree
Showing 36 changed files with 524 additions and 220 deletions.
3 changes: 3 additions & 0 deletions frontend_src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"include": [
"./**/*"
],
"compilerOptions": {
"outDir": "./dist",
"baseUrl": "./",
Expand Down
5 changes: 1 addition & 4 deletions frontend_src/unittests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,11 @@ export function useTestCtx() {
const user = userEvent.setup({ document: doc });
const screen = within(doc.body);

return { app: getApp(), screen, user, wrapper: currentWrapper };
return { app: getApp(), screen, user, wrapper: currentWrapper, waitFor };
}

export function waitFor<T>(callback: () => T, options?: Parameters<typeof _waitFor>[1]) {
const container = options?.container || __currentApp?.rootVm?.$el;
if (!container) {
throw new Error('App must be mounted first or custom must be provided');
}
return _waitFor(callback, {
...options,
// @ts-expect-error $el is ok here
Expand Down
4 changes: 2 additions & 2 deletions frontend_src/vstutils/fetch-values.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { i18n } from '@/vstutils/translation';
import { AggregatedQueriesExecutor } from '@/vstutils/AggregatedQueriesExecutor';
import { RequestTypes, createPropertyProxy, getApp } from '@/vstutils/utils';
import { ArrayField } from '@/vstutils/fields/array';
import { FKField } from '@/vstutils/fields/fk/fk/FKField';
import { ArrayField } from '@/vstutils/fields/array/ArrayField';
import { DynamicField } from '@/vstutils/fields/dynamic';
import { FKField } from '@/vstutils/fields/fk/fk';

import type { BaseView } from '@/vstutils/views';
import type { Model, ModelConstructor } from '@/vstutils/models';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test, describe, expect } from '@jest/globals';
import StringField from '../text/StringField';
import { FKField } from '../fk/fk';
import { FKField } from '../fk/fk/FKField';

describe('fields types', () => {
test('simple string field', () => {
Expand Down
26 changes: 2 additions & 24 deletions frontend_src/vstutils/fields/array/ArrayField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,10 @@ import type {
FieldXOptions,
} from '@/vstutils/fields/base';
import { BaseField } from '@/vstutils/fields/base';
import { ChoicesField, OrderingChoicesField } from '@/vstutils/fields/choices';
import { FKField } from '@/vstutils/fields/fk/fk';
import { NestedObjectField } from '@/vstutils/fields/nested-object';
import { integer, NumberField } from '@/vstutils/fields/numbers';
import { StringField } from '@/vstutils/fields/text';
import { onAppBeforeInit } from '@/vstutils/signals';
import type { InnerData, RepresentData } from '@/vstutils/utils';

import { ChoicesArrayFieldMixin } from './custom/choices';
import { FKArrayFieldMixin } from './custom/fk';
import { NestedObjectArrayFieldMixin } from './custom/nested-object';
import { IntegerArrayFieldMixin, NumberArrayFieldMixin } from './custom/number';
import { StringArrayFieldMixin } from './custom/string';
import { ArrayFieldMixin } from './mixins';
import type { InnerData, RepresentData } from '@/vstutils/utils';

export interface ArrayFieldXOptions extends FieldXOptions {
'x-collectionFormat'?: ParameterCollectionFormat;
Expand All @@ -39,16 +29,6 @@ export class ArrayField<TRealField extends Field = Field> extends BaseField<
['pipes', '|'],
]);

static CUSTOM_COMPONENTS = new Map<new (options: any) => Field, unknown>([
[ChoicesField as unknown as new (options: any) => Field, ChoicesArrayFieldMixin],
[OrderingChoicesField as unknown as new (options: any) => Field, ChoicesArrayFieldMixin],
[FKField, FKArrayFieldMixin],
[StringField, StringArrayFieldMixin],
[NumberField, NumberArrayFieldMixin],
[integer.IntegerField, IntegerArrayFieldMixin],
[NestedObjectField, NestedObjectArrayFieldMixin],
]);

collectionFormat?: ParameterCollectionFormat;
separator?: string;
minItems: number;
Expand Down Expand Up @@ -88,9 +68,7 @@ export class ArrayField<TRealField extends Field = Field> extends BaseField<
this.itemField = this.app.fieldsResolver.resolveField(this.options.items!, this.name) as TRealField;
this.itemField.model = this.model;

const customComponent = ArrayField.CUSTOM_COMPONENTS.get(
this.itemField.constructor as new (options: any) => Field,
);
const customComponent = this.itemField.getArrayComponent();
if (customComponent) {
this.component = customComponent;
}
Expand Down
6 changes: 3 additions & 3 deletions frontend_src/vstutils/fields/array/ArrayFieldEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<select ref="select" class="form-control" multiple style="width: 100%" />

<div v-if="showItemField" class="item-field-wrapper" style="padding: 0.5rem">
<button class="btn btn-danger" @click="closeItemField">
<button type="button" class="btn btn-danger" @click="closeItemField">
<i class="fas fa-ban" />
</button>
<component
Expand All @@ -15,7 +15,7 @@
hide-title
@set-value="setNewValue"
/>
<button class="btn btn-primary" @click="addValue">
<button type="button" class="btn btn-primary" @click="addValue">
<i class="fas fa-plus" />
</button>
</div>
Expand Down Expand Up @@ -66,7 +66,7 @@
if (!item.vm) {
item.vm = new Vue({
parent: this,
mixins: [this.field.itemField.component],
mixins: [this.field.itemField.getComponent()],
propsData: {
field: this.field.itemField,
data: { [this.field.itemField.name]: item.value },
Expand Down
10 changes: 0 additions & 10 deletions frontend_src/vstutils/fields/array/custom/choices/index.ts

This file was deleted.

13 changes: 8 additions & 5 deletions frontend_src/vstutils/fields/base/BaseField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import type { Model, ModelConstructor } from '../../models';
import BaseFieldMixin from './BaseFieldMixin.vue';
import { i18n } from '../../translation';
import type { IApp } from '@/vstutils/app';
import type { Component, ComponentOptions } from 'vue';
import type Vue from 'vue';
import type { ComponentOptionsMixin } from 'vue/types/v3-component-options';
import type { Component } from 'vue';
import { BaseFieldLabel } from '@/vstutils/fields/base';

const componentsCache = new WeakMap<FieldConstructor, Component>();
Expand Down Expand Up @@ -93,6 +91,7 @@ export interface Field<

getComponent(): Component;
getLabelComponent(): Component;
getArrayComponent(): Component | undefined;

toInner(data: RepresentData): Inner | null | undefined;
toRepresent(data: InnerData): Represent | null | undefined;
Expand Down Expand Up @@ -120,7 +119,7 @@ export interface Field<
getContainerCssClasses(data: RepresentData): string[] | undefined;
}

export type FieldMixin = ComponentOptionsMixin | ComponentOptions<Vue> | typeof Vue;
export type FieldMixin = Component;

export class BaseField<Inner, Represent, XOptions extends DefaultXOptions = DefaultXOptions>
implements Field<Inner, Represent, XOptions>
Expand All @@ -129,7 +128,7 @@ export class BaseField<Inner, Represent, XOptions extends DefaultXOptions = Defa

options: FieldOptions<XOptions, Inner>;
props: XOptions;
component?: ComponentOptions<Vue>;
component?: Component;

type: ParameterType;
format?: string;
Expand Down Expand Up @@ -201,6 +200,10 @@ export class BaseField<Inner, Represent, XOptions extends DefaultXOptions = Defa
return this.component ?? (this.constructor as FieldConstructor)._component;
}

getArrayComponent(): Component | undefined {
return undefined;
}

getLabelComponent(): Component {
return BaseFieldLabel;
}
Expand Down
5 changes: 5 additions & 0 deletions frontend_src/vstutils/fields/choices/ChoicesField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { FieldOptions, FieldXOptions } from '../base';
import { StringField } from '../text';
import ChoicesFieldMixin from './ChoicesFieldMixin.js';
import type { RepresentData } from '@/vstutils/utils';
import { ChoicesArrayFieldMixin } from './array';

export type RawEnumItem = string | [string, string] | { value: string; prefetch_value: string } | Model;

Expand Down Expand Up @@ -98,6 +99,10 @@ export class ChoicesField extends StringField<ChoicesFieldXOptions> {
return [ChoicesFieldMixin];
}

override getArrayComponent() {
return ChoicesArrayFieldMixin;
}

getContainerCssClasses(data: RepresentData) {
const value = this.getValue(data);
if (value) {
Expand Down
5 changes: 5 additions & 0 deletions frontend_src/vstutils/fields/choices/OrderingChoicesField.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { i18n } from '@/vstutils/translation';
import { type RawEnumItem, ChoicesField } from './ChoicesField';
import { ChoicesArrayFieldMixin } from './array';

class OrderingChoicesField extends ChoicesField {
translateValue(value: string): string {
Expand All @@ -20,6 +21,10 @@ class OrderingChoicesField extends ChoicesField {
}
return preparedItem;
}

override getArrayComponent() {
return ChoicesArrayFieldMixin;
}
}

export default OrderingChoicesField;
10 changes: 10 additions & 0 deletions frontend_src/vstutils/fields/choices/array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { type Component, defineComponent } from 'vue';
import ChoicesArrayFieldEdit from './ChoicesArrayFieldEdit.vue';
import { StringArrayFieldMixin } from '@/vstutils/fields/text/string-array';

export const ChoicesArrayFieldMixin: Component = defineComponent({
components: {
field_content_edit: ChoicesArrayFieldEdit,
},
mixins: [StringArrayFieldMixin],
});
80 changes: 75 additions & 5 deletions frontend_src/vstutils/fields/fk/__tests__/DeepFKField.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { expect, test, describe, beforeAll, beforeEach } from '@jest/globals';
import { reactive } from 'vue';
import { within } from '@testing-library/dom';
import { userEvent } from '@testing-library/user-event';
import fetchMock from 'jest-fetch-mock';
import { createApp, createSchema, mount, waitFor } from '@/unittests';
import { createApp, createSchema, waitFor, mount } from '@/unittests';
import schema from './DeepFKField-schema.json';

describe('DeepFKfield', () => {
let app;
let Category;
let deepFk;
let deepFkArray;

beforeAll(async () => {
fetchMock.enableMocks();
Expand All @@ -26,6 +30,23 @@ describe('DeepFKfield', () => {
only_last_child: true,
},
});
deepFkArray = app.fieldsResolver.resolveField({
name: 'testField',
type: 'array',
items: {
type: 'integer',
format: 'deep_fk',
'x-options': {
makeLink: true,
model: { $ref: '#/definitions/Category' },
usePrefetch: true,
value_field: 'id',
view_field: 'name',
parent_field_name: 'parent',
only_last_child: true,
},
},
});
});

beforeEach(() => {
Expand Down Expand Up @@ -75,12 +96,15 @@ describe('DeepFKfield', () => {
const emitted = wrapper.emitted();

// Wait for categories to load and display
await waitFor(() => expect(wrapper.find('li[data-id="1"]').exists()).toBeTruthy(), {
container: wrapper.element,
});
await waitFor(() => expect(wrapper.find('li[data-id="1"]').exists()).toBeTruthy());

// Select category with subcategory
wrapper.find('li[data-id="1"]').find('.tree-content').trigger('click');
wrapper.find('li[data-id="1"]').trigger('click');
await wrapper.vm.$nextTick();
expect(emitted['set-value']).toBeFalsy();

// Open subcategory
wrapper.find('li[data-id="1"]').find('.tree-arrow').trigger('click');
await wrapper.vm.$nextTick();
expect(emitted['set-value']).toBeFalsy();

Expand All @@ -89,4 +113,50 @@ describe('DeepFKfield', () => {
expect(emitted['set-value'].length).toBe(1);
expect(emitted['set-value'][0][0].value.getPkValue()).toBe(2);
});

test('deep fk field array', async () => {
fetchMock.mockResponseOnce(
JSON.stringify([
{
status: 200,
data: {
count: 2,
results: [
{ id: 1, parent: null, name: 'Cat 1' },
{ id: 2, parent: 1, name: 'Cat 1.1' },
{ id: 4, parent: null, name: 'Cat 3' },
{ id: 3, parent: null, name: 'Cat 2' },
],
},
},
]),
);
const data = reactive({ testField: [2, 3] });
const wrapper = mount({
template: `<transition><field type="edit" :field="field" :data="data" @set-value="setValue" /></transition>`,
components: {
field: deepFkArray.getComponent(),
},
setup() {
const field = deepFkArray;
function setValue(e) {
data.testField = e.value;
}
return { data, field, setValue };
},
});

const user = userEvent.setup({ document: wrapper.vm.$el.ownerDocument });
const screen = within(wrapper.element);

// Cat 1.1 is selected so must be visible when opened
const cat1_1 = await screen.findByText(
(content, el) => content === 'Cat 1.1' && el.parentElement.classList.contains('tree-anchor'),
);

// Unselect Cat 1.1
user.click(cat1_1);
await waitFor(() => expect(data.testField.length).toBe(1));
expect(data.testField[0].id).toBe(3);
});
});
2 changes: 1 addition & 1 deletion frontend_src/vstutils/fields/fk/__tests__/FKfield.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, test, describe, beforeAll, beforeEach } from '@jest/globals';
import fetchMock from 'jest-fetch-mock';
import { FKField } from '../fk';
import { createApp } from '../../../../unittests/create-app.js';
import { FKField } from '../fk/FKField';

describe('FKfield', () => {
/** @type {App} */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ComponentOptions } from 'vue';
import { FKField } from '../fk';
import { FKField } from '../fk/FKField';
import FKAutocompleteFieldMixin from './FKAutocompleteFieldMixin';
import type { FieldOptions } from '@/vstutils/fields/base';
import type { FKFieldXOptions, TInner } from '@/vstutils/fields/fk/fk/FKField';
Expand Down
9 changes: 7 additions & 2 deletions frontend_src/vstutils/fields/fk/deep-fk/DeepFKField.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FKField } from '../fk';
import { DeepFKFieldMixin } from './index';
import { FKField } from '../fk/FKField';
import DeepFKFieldMixin from './DeepFKFieldMixin';
import { DeepFkArrayFieldMixin } from './array';

/**
* DeepFKField extends `FkField`, but displays as tree
Expand Down Expand Up @@ -73,5 +74,9 @@ class DeepFKField extends FKField {
static get mixins() {
return [DeepFKFieldMixin];
}

getArrayComponent() {
return DeepFkArrayFieldMixin;
}
}
export default DeepFKField;
Loading

0 comments on commit e7d0618

Please sign in to comment.