From 3891e033a53bf8ea507386157dd28c93e8094ec0 Mon Sep 17 00:00:00 2001 From: Dmitriy Ovcharenko Date: Tue, 13 Aug 2024 11:47:52 +1000 Subject: [PATCH] Feature(frontend): Parent fk on actions and custom initial values --- .../vstutils/fields/base/BaseField.ts | 35 ++++++++++++++++++- frontend_src/vstutils/fields/base/Field.ts | 4 ++- frontend_src/vstutils/fields/fk/fk/FKField.ts | 16 +++++---- frontend_src/vstutils/models/Model.ts | 2 ++ .../vstutils/models/ModelsResolver.ts | 14 +++++++- frontend_src/vstutils/schema.ts | 10 ++++++ .../store/__tests__/actionViewStore.test.js | 6 ++-- frontend_src/vstutils/store/helpers.ts | 7 +++- frontend_src/vstutils/store/page-types.ts | 2 +- frontend_src/vstutils/store/page.ts | 5 +++ frontend_src/vstutils/utils/index.ts | 4 +++ test_src/test_proj/models/fields_testing.py | 7 ++++ test_src/test_proj/tests.py | 14 +++++++- vstutils/api/schema/inspectors.py | 3 ++ 14 files changed, 114 insertions(+), 15 deletions(-) diff --git a/frontend_src/vstutils/fields/base/BaseField.ts b/frontend_src/vstutils/fields/base/BaseField.ts index b02bfdc3..41447281 100644 --- a/frontend_src/vstutils/fields/base/BaseField.ts +++ b/frontend_src/vstutils/fields/base/BaseField.ts @@ -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'; @@ -145,12 +153,37 @@ export class BaseField extends PropertyDescriptor { @@ -20,6 +21,7 @@ export interface FieldXOptions { redirect?: RedirectOptions; translateFieldName?: string; disableLabelTranslation?: boolean; + initialValue?: FieldInitialValueConfig; [key: string]: unknown; } diff --git a/frontend_src/vstutils/fields/fk/fk/FKField.ts b/frontend_src/vstutils/fields/fk/fk/FKField.ts index f43b3ffb..ef5a4243 100644 --- a/frontend_src/vstutils/fields/fk/fk/FKField.ts +++ b/frontend_src/vstutils/fields/fk/fk/FKField.ts @@ -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< diff --git a/frontend_src/vstutils/models/Model.ts b/frontend_src/vstutils/models/Model.ts index 10730f96..ac51d592 100644 --- a/frontend_src/vstutils/models/Model.ts +++ b/frontend_src/vstutils/models/Model.ts @@ -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; diff --git a/frontend_src/vstutils/models/ModelsResolver.ts b/frontend_src/vstutils/models/ModelsResolver.ts index e191ba9b..a3edc7bb 100644 --- a/frontend_src/vstutils/models/ModelsResolver.ts +++ b/frontend_src/vstutils/models/ModelsResolver.ts @@ -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; } diff --git a/frontend_src/vstutils/schema.ts b/frontend_src/vstutils/schema.ts index f808f128..ad5ea4e1 100644 --- a/frontend_src/vstutils/schema.ts +++ b/frontend_src/vstutils/schema.ts @@ -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; 'x-properties-groups'?: Record; @@ -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 { diff --git a/frontend_src/vstutils/store/__tests__/actionViewStore.test.js b/frontend_src/vstutils/store/__tests__/actionViewStore.test.js index fd32dc9d..6c7e7caa 100644 --- a/frontend_src/vstutils/store/__tests__/actionViewStore.test.js +++ b/frontend_src/vstutils/store/__tests__/actionViewStore.test.js @@ -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', }); @@ -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' }, }, ], }); diff --git a/frontend_src/vstutils/store/helpers.ts b/frontend_src/vstutils/store/helpers.ts index 866235af..c4b372c0 100644 --- a/frontend_src/vstutils/store/helpers.ts +++ b/frontend_src/vstutils/store/helpers.ts @@ -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, }; }; diff --git a/frontend_src/vstutils/store/page-types.ts b/frontend_src/vstutils/store/page-types.ts index 2df7e60a..609be225 100644 --- a/frontend_src/vstutils/store/page-types.ts +++ b/frontend_src/vstutils/store/page-types.ts @@ -18,7 +18,7 @@ export interface BaseViewStore extends StoreGeneric { initLoading: () => void; setLoadingSuccessful: () => void; setLoadingError: (error: unknown) => void; - fetchData: (options?: Record) => Promise; + fetchData: (options?: Record) => Promise | void; getAutoUpdatePk?: () => number | string | undefined; startAutoUpdate: () => void; stopAutoUpdate: () => void; diff --git a/frontend_src/vstutils/store/page.ts b/frontend_src/vstutils/store/page.ts index 2526fa70..7fa7c9b5 100644 --- a/frontend_src/vstutils/store/page.ts +++ b/frontend_src/vstutils/store/page.ts @@ -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, }; }; diff --git a/frontend_src/vstutils/utils/index.ts b/frontend_src/vstutils/utils/index.ts index 3dd65f7b..e3bd7f44 100644 --- a/frontend_src/vstutils/utils/index.ts +++ b/frontend_src/vstutils/utils/index.ts @@ -517,3 +517,7 @@ export function escapeHtml(unsafe: string) { export const OBJECT_NOT_FOUND_TEXT = '[Object not found]'; export type MaybePromise = T | Promise; + +export function assertNever(value: never): never { + throw new Error(`Unexpected value: ${value}`); +} diff --git a/test_src/test_proj/models/fields_testing.py b/test_src/test_proj/models/fields_testing.py index 36e0a759..a167ae04 100644 --- a/test_src/test_proj/models/fields_testing.py +++ b/test_src/test_proj/models/fields_testing.py @@ -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): diff --git a/test_src/test_proj/tests.py b/test_src/test_proj/tests.py index 31d2d702..149295df 100644 --- a/test_src/test_proj/tests.py +++ b/test_src/test_proj/tests.py @@ -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']) @@ -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"]}', diff --git a/vstutils/api/schema/inspectors.py b/vstutils/api/schema/inspectors.py index da071f61..d70fe298 100644 --- a/vstutils/api/schema/inspectors.py +++ b/vstutils/api/schema/inspectors.py @@ -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