Skip to content

Commit

Permalink
Feature(frontend): Parent fk on actions and custom initial values
Browse files Browse the repository at this point in the history
### Changelog:
* Feature(frontend): Allow to use `parent_fk` on actions.
* Feature(frontend): Allow to use data from parent views to set initial values of fields.


Closes: vst/vst-utils#653+
Closes: vst/vst-utils#654+

See merge request vst/vst-utils!663
  • Loading branch information
onegreyonewhite committed Aug 13, 2024
2 parents 9221281 + 3891e03 commit af8e2a6
Show file tree
Hide file tree
Showing 14 changed files with 114 additions and 15 deletions.
35 changes: 34 additions & 1 deletion frontend_src/vstutils/fields/base/BaseField.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import type { ParameterType } from 'swagger-schema-official';
import { defineComponent, markRaw, toRaw } from 'vue';
import type { InnerData, RepresentData } from '../../utils';
import { _translate, deepEqual, X_OPTIONS, stringToCssClass, nameToTitle, capitalize } from '../../utils';
import {
_translate,
deepEqual,
X_OPTIONS,
stringToCssClass,
nameToTitle,
capitalize,
assertNever,
} from '../../utils';
import { pop_up_msg } from '../../popUp';
import type { ModelConstructor } from '../../models';
import BaseFieldMixin from './BaseFieldMixin.vue';
Expand Down Expand Up @@ -145,12 +153,37 @@ export class BaseField<Inner, Represent, XOptions extends DefaultXOptions = Defa
* Returns field default value if any, or empty value otherwise
*/
getInitialValue({ requireValue = false } = {}): Inner | undefined | null {
const customInitialValue = this.getCustomInitialValue();
if (customInitialValue) {
return customInitialValue;
}
if (this.required || requireValue) {
return this.hasDefault ? this.default : this.getEmptyValue();
}
return undefined;
}

protected getCustomInitialValue(): Inner | undefined | null {
const initialValueConfig = this.props?.initialValue;
if (!initialValueConfig) {
return;
}

if (initialValueConfig.type === 'from_first_parent_detail_view_that_has_field') {
for (const item of this.app.store.viewItems.slice(0, -1).toReversed()) {
if (item.view.isDetailPage()) {
const state = item.view.getSavedState();
if (state?.instance && state.instance._fields.has(initialValueConfig.field_name)) {
return state.instance.getInnerValue(initialValueConfig.field_name) as any;
}
}
}
return;
}

assertNever(initialValueConfig.type);
}

/**
* Returns empty value of field
*/
Expand Down
4 changes: 3 additions & 1 deletion frontend_src/vstutils/fields/base/Field.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Schema, ParameterCollectionFormat, ParameterType } from 'swagger-schema-official';
import type { ModelConstructor, Model } from '#vstutils/models/Model';
import type { RepresentData, InnerData } from '#vstutils/utils';
import type { Schema, ParameterCollectionFormat, ParameterType } from 'swagger-schema-official';
import type { FieldInitialValueConfig } from '../../schema';
import type { Component } from 'vue';

export interface ModelPropertyDescriptor<Represent> extends PropertyDescriptor {
Expand All @@ -20,6 +21,7 @@ export interface FieldXOptions {
redirect?: RedirectOptions;
translateFieldName?: string;
disableLabelTranslation?: boolean;
initialValue?: FieldInitialValueConfig;
[key: string]: unknown;
}

Expand Down
16 changes: 10 additions & 6 deletions frontend_src/vstutils/fields/fk/fk/FKField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,17 @@ function getPk() {

function getParentPk() {
const app = getApp();
let parentView = app.store.page.view.parent?.parent;

if (app.store.page.view.isEditPage() && !app.store.page.view.isEditStyleOnly) {
parentView = parentView?.parent;
for (let idx = app.store.viewItems.length - 2; idx >= 0; idx--) {
const item = app.store.viewItems[idx];
if (item?.view.isDetailPage()) {
const pk = item.view.getSavedState()?.instance?.getPkValue();
if (pk) {
return String(pk);
}
break;
}
}

return app.router.currentRoute.params[(parentView as PageView).pkParamName!] || '';
return '';
}

const dependenceTemplates = new Map<
Expand Down
2 changes: 2 additions & 0 deletions frontend_src/vstutils/models/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export interface Model {
_data: InnerData;
readonly sandbox: ModelSandbox;

getInnerValue(fieldName: string): unknown;
getRepresentValue(fieldName: string): unknown;
getPkValue(): string | number | undefined | null;
getViewFieldString(escapeResult?: boolean): string | undefined;
getViewFieldValue(defaultValue?: unknown): unknown;
Expand Down
14 changes: 13 additions & 1 deletion frontend_src/vstutils/models/ModelsResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,25 @@ export class ModelsResolver {
// Set required
const requiredProperties = modelSchema.required ?? [];

const initialValuesConfig = modelSchema['x-initial-values'] ?? {};

signals.emit('models[' + modelName + '].fields.beforeInit', properties);

const fields = Object.entries(properties).map(([fieldName, fieldSchema]) => {
const schema: any = Object.assign({}, fieldSchema);

if (!('x-options' in schema)) {
schema['x-options'] = {};
}

if (initialValuesConfig[fieldName] && !schema['x-options'].initialValue) {
schema['x-options'].initialValue = initialValuesConfig[fieldName];
}

const field =
fieldSchema instanceof BaseField
? fieldSchema
: this.fieldsResolver.resolveField(Object.assign({}, fieldSchema), fieldName);
: this.fieldsResolver.resolveField(schema, fieldName);
if (requiredProperties.includes(fieldName)) {
field.required = true;
}
Expand Down
10 changes: 10 additions & 0 deletions frontend_src/vstutils/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ export interface AppInfo extends swagger.Info {

export const MODEL_MODES = ['DEFAULT', 'STEP'] as const;

export type FieldInitialValueConfig = {
type: 'from_first_parent_detail_view_that_has_field';
field_name: string;
};

export type ModelInitialValuesConfig = {
[key: string]: FieldInitialValueConfig;
};

export type ModelDefinition = swagger.Schema & {
properties?: Record<string, FieldDefinition>;
'x-properties-groups'?: Record<string, string[]>;
Expand All @@ -62,6 +71,7 @@ export type ModelDefinition = swagger.Schema & {
'x-hide-not-required'?: boolean;
'x-display-mode'?: (typeof MODEL_MODES)[number];
'x-visibility-data-field-name'?: string;
'x-initial-values'?: ModelInitialValuesConfig;
};

export interface Operation extends swagger.Operation {
Expand Down
6 changes: 3 additions & 3 deletions frontend_src/vstutils/store/__tests__/actionViewStore.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ test('createActionViewStore', async () => {
expect(store).not.toBeNull();
expect(store.response).toBeTruthy();
expect(store.sandbox).toStrictEqual({
bool: undefined,
text: undefined,
bool: false,
text: '',
choice: 'one',
});

Expand Down Expand Up @@ -50,7 +50,7 @@ test('createActionViewStore', async () => {
{
method: 'post',
path: '/some_list/some_action/',
data: { choice: 'one', text: 'Mshvill' },
data: { bool: false, choice: 'one', text: 'Mshvill' },
},
],
});
Expand Down
7 changes: 6 additions & 1 deletion frontend_src/vstutils/store/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,11 +498,16 @@ export const createActionStore = (view: ActionView) => {
}
}

pageWithEditableData.setInstance(new pageWithEditableData.model.value());
function init() {
pageWithEditableData.setInstance(
new pageWithEditableData.model.value(pageWithEditableData.model.value.getInitialData()),
);
}

return {
...pageWithEditableData,
execute,
init,
};
};

Expand Down
2 changes: 1 addition & 1 deletion frontend_src/vstutils/store/page-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface BaseViewStore extends StoreGeneric {
initLoading: () => void;
setLoadingSuccessful: () => void;
setLoadingError: (error: unknown) => void;
fetchData: (options?: Record<string, unknown>) => Promise<void>;
fetchData: (options?: Record<string, unknown>) => Promise<void> | void;
getAutoUpdatePk?: () => number | string | undefined;
startAutoUpdate: () => void;
stopAutoUpdate: () => void;
Expand Down
5 changes: 5 additions & 0 deletions frontend_src/vstutils/store/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,11 +521,16 @@ export const createActionViewStore = (view: ActionView) => {

base.setLoadingSuccessful();

function fetchData() {
actionStore.init();
}

return {
...base,
...actionStore,
...useOperations({ view, data: actionStore.sandbox }),
entityViewClasses: useEntityViewClasses(actionStore.model, actionStore.sandbox),
execute,
fetchData,
};
};
4 changes: 4 additions & 0 deletions frontend_src/vstutils/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,3 +517,7 @@ export function escapeHtml(unsafe: string) {
export const OBJECT_NOT_FOUND_TEXT = '[Object not found]';

export type MaybePromise<T> = T | Promise<T>;

export function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
7 changes: 7 additions & 0 deletions test_src/test_proj/models/fields_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ def check_named_response_as_result_serializer(view, request, *args, **kwargs):
class PropertyAuthorSerializer(BaseSerializer):
phone = PhoneField(allow_null=True, required=False)

_initial_frontend_values = {
'phone': {
'type': 'from_first_parent_detail_view_that_has_field',
'field_name': 'phone',
},
}


@actions.SimpleAction(serializer_class=PropertyAuthorSerializer, atomic=True, require_confirmation=True)
def simple_property_action(self, request, *args, **kwargs):
Expand Down
14 changes: 13 additions & 1 deletion test_src/test_proj/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2202,6 +2202,18 @@ def has_deep_parent_filter(params):
self.assertFalse(api['paths']['/hosts/instance_suffix_action_test/']['get']['x-list'])
self.assertEqual(len(api['paths']['/hosts/instance_suffix_action_test/']['get']['parameters']), 0)

# Test custom initial values
self.assertDictEqual(
api['definitions']['PropertyAuthor']['x-initial-values'],
{
'phone': {
'field_name': 'phone',
'type': 'from_first_parent_detail_view_that_has_field',
},
},
)


# Check x-list param on get methods
self.assertFalse(api['paths']['/files/query_serializer_test/']['get']['x-list'])
self.assertTrue(api['paths']['/files/query_serializer_test_list/']['get']['x-list'])
Expand Down Expand Up @@ -7654,7 +7666,7 @@ def test_custom_user_claims(self):
oauth2_server_jwk_set,
)
self.assertEqual(id_token['locale'], 'fr-CA')

response = self.client_class().get(
'/api/oauth2/userinfo/',
HTTP_AUTHORIZATION=f'Bearer {response.json()["access_token"]}',
Expand Down
3 changes: 3 additions & 0 deletions vstutils/api/schema/inspectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,9 @@ def handle_schema(self, field: Serializer, schema: openapi.SwaggerDict, use_refe
if display_mode:
schema['x-display-mode'] = display_mode

if initial_frontend_values := getattr(serializer_class, '_initial_frontend_values', None):
schema['x-initial-values'] = initial_frontend_values

schema._handled = True # pylint: disable=protected-access

# TODO: return it when swagger become openapi 3.0.1
Expand Down

0 comments on commit af8e2a6

Please sign in to comment.