diff --git a/frontend_src/vstutils/fields/__tests__/fields-container-classes.test.ts b/frontend_src/vstutils/fields/__tests__/fields-container-classes.test.ts new file mode 100644 index 00000000..8321344f --- /dev/null +++ b/frontend_src/vstutils/fields/__tests__/fields-container-classes.test.ts @@ -0,0 +1,49 @@ +import { expect, test } from '@jest/globals'; +import { createApp, createSchema } from '@/unittests'; +import { useEntityViewClasses } from '@/vstutils/store'; +import { ref } from 'vue'; +import { emptyRepresentData } from '@/vstutils/utils'; + +test('fields container classes', async () => { + const app = await createApp({ schema: createSchema() }); + + const TestModel = app.modelsResolver.bySchemaObject({ + properties: { + some_boolean: { type: 'boolean' }, + some_choice: { type: 'string', enum: ['option1', 'option2'] }, + some_string: { type: 'string' }, + some_object: { + type: 'object', + properties: { + some_boolean: { type: 'boolean' }, + some_choice: { type: 'string', enum: ['option1', 'option2'] }, + some_string: { type: 'string' }, + }, + }, + }, + }); + + const data = ref(emptyRepresentData()); + const classes = useEntityViewClasses(ref(TestModel), data); + + expect(classes.value).toStrictEqual([]); + + data.value = { + ...emptyRepresentData(), + some_boolean: true, + some_choice: 'option1', + some_string: 'some str', + some_object: { + some_boolean: false, + some_choice: 'option2', + some_string: 'other str', + }, + }; + + expect(classes.value).toStrictEqual([ + 'field-some_boolean-true', + 'field-some_choice-option1', + 'field-some_object-some_boolean-false', + 'field-some_object-some_choice-option2', + ]); +}); diff --git a/frontend_src/vstutils/fields/base/BaseField.ts b/frontend_src/vstutils/fields/base/BaseField.ts index 3c8eb119..9c2670dc 100644 --- a/frontend_src/vstutils/fields/base/BaseField.ts +++ b/frontend_src/vstutils/fields/base/BaseField.ts @@ -1,7 +1,7 @@ import type { Schema, ParameterType, ParameterCollectionFormat } from 'swagger-schema-official'; import { defineComponent, markRaw, toRaw } from 'vue'; import type { InnerData, RepresentData } from '../../utils'; -import { _translate, capitalize, deepEqual, nameToTitle, X_OPTIONS } from '../../utils'; +import { _translate, capitalize, deepEqual, nameToTitle, X_OPTIONS, stringToCssClass } from '../../utils'; import { pop_up_msg } from '../../popUp'; import type { Model, ModelConstructor } from '../../models'; import BaseFieldMixin from './BaseFieldMixin.vue'; @@ -116,6 +116,8 @@ export interface Field< isSameValues(data1: RepresentData, data2: RepresentData): boolean; parseFieldError(errorData: unknown, instanceData: InnerData): unknown; + + getContainerCssClasses(data: RepresentData): string[] | undefined; } export type FieldMixin = ComponentOptionsMixin | ComponentOptions | typeof Vue; @@ -408,6 +410,17 @@ export class BaseField; } + + protected formatContainerCssClass(value: string | null | undefined) { + if (value === undefined) { + return `field-${this.name}`; + } + return `field-${this.name}-${stringToCssClass(value)}`; + } + + getContainerCssClasses(_data: RepresentData): string[] | undefined { + return undefined; + } } export type FieldConstructor = typeof BaseField; diff --git a/frontend_src/vstutils/fields/boolean/BooleanField.js b/frontend_src/vstutils/fields/boolean/BooleanField.js index 2c9d21b6..32278e54 100644 --- a/frontend_src/vstutils/fields/boolean/BooleanField.js +++ b/frontend_src/vstutils/fields/boolean/BooleanField.js @@ -59,6 +59,13 @@ class BooleanField extends BaseField { static get mixins() { return [BooleanFieldMixin]; } + + getContainerCssClasses(data) { + const value = this.getValue(data); + if (value !== undefined) { + return [this.formatContainerCssClass(String(value))]; + } + } } export default BooleanField; diff --git a/frontend_src/vstutils/fields/choices/ChoicesField.ts b/frontend_src/vstutils/fields/choices/ChoicesField.ts index 5386f58c..b3b4c93f 100644 --- a/frontend_src/vstutils/fields/choices/ChoicesField.ts +++ b/frontend_src/vstutils/fields/choices/ChoicesField.ts @@ -4,6 +4,7 @@ import { i18n } from '@/vstutils/translation'; import type { FieldOptions, FieldXOptions } from '../base'; import { StringField } from '../text'; import ChoicesFieldMixin from './ChoicesFieldMixin.js'; +import type { RepresentData } from '@/vstutils/utils'; export type RawEnumItem = string | [string, string] | { value: string; prefetch_value: string } | Model; @@ -96,4 +97,12 @@ export class ChoicesField extends StringField { static get mixins() { return [ChoicesFieldMixin]; } + + getContainerCssClasses(data: RepresentData) { + const value = this.getValue(data); + if (value) { + return [this.formatContainerCssClass(value)]; + } + return []; + } } diff --git a/frontend_src/vstutils/fields/nested-object.ts b/frontend_src/vstutils/fields/nested-object.ts index d72a5414..a7b86cce 100644 --- a/frontend_src/vstutils/fields/nested-object.ts +++ b/frontend_src/vstutils/fields/nested-object.ts @@ -186,4 +186,19 @@ export class NestedObjectField } return validatedValue; } + + getContainerCssClasses(data: RepresentData) { + const value = this.getValue(data); + const classes = []; + for (const field of this.nestedModel!.fields.values()) { + const fieldClasses = field.getContainerCssClasses(value ?? emptyRepresentData()); + if (fieldClasses) { + const prefix = `field-${this.name}-`; + for (const fieldClass of fieldClasses) { + classes.push(fieldClass.replace(/^field-/, prefix)); + } + } + } + return classes; + } } diff --git a/frontend_src/vstutils/store/helpers.ts b/frontend_src/vstutils/store/helpers.ts index 352fc904..34d13e38 100644 --- a/frontend_src/vstutils/store/helpers.ts +++ b/frontend_src/vstutils/store/helpers.ts @@ -22,7 +22,6 @@ import { filterOperations, signals } from '@/vstutils/signals'; import { i18n } from '@/vstutils/translation'; import { useRoute } from 'vue-router/composables'; import { - classesFromFields, getApp, getRedirectUrlFromResponse, IGNORED_FILTERS, @@ -33,6 +32,7 @@ import { pathToArray, emptyRepresentData, emptyInnerData, + classesFromFields, } from '@/vstutils/utils'; import type { Ref } from 'vue'; @@ -126,7 +126,7 @@ export const useQuerySet = (view: IView) => { return { queryset, setQuerySet }; }; -export const useEntityViewClasses = (modelClass: Ref, data: Ref>) => { +export const useEntityViewClasses = (modelClass: Ref, data: Ref) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return const flatFields = computed(() => Array.from(modelClass.value.fields.values())); // eslint-disable-next-line @typescript-eslint/no-unsafe-return diff --git a/frontend_src/vstutils/utils/todo.js b/frontend_src/vstutils/utils/todo.js index 1989686d..615c6cf4 100644 --- a/frontend_src/vstutils/utils/todo.js +++ b/frontend_src/vstutils/utils/todo.js @@ -906,17 +906,24 @@ export function generateBase32String(length = 32) { return generateRandomString(length, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'); } +/** + * @param {Iterable} fields + * @param {RepresentData} data + * @return {string[]} + */ export function classesFromFields(fields, data) { - return fields - .filter((f) => f.format === 'choices' || f.type === 'boolean') - .map((f) => { - let cls = `field-${f.name}`; - const value = f._getValueFromData(data); - if (value !== undefined) { - cls += `-${stringToCssClass(value)}`; + const classes = new Set(); + + for (const field of fields) { + const classNames = field.getContainerCssClasses(data); + if (classNames) { + for (const cls of classNames) { + classes.add(cls); } - return cls; - }); + } + } + + return Array.from(classes); } export function parseResponseMessage(data) { @@ -1007,6 +1014,10 @@ export function chunkArray(array, chunkSize) { }, []); } +/** + * @param {unknown} str + * @returns {string} + */ export function stringToCssClass(str) { if (typeof str !== 'string') str = String(str); return str.replace(/\s/g, ''); diff --git a/package.json b/package.json index a936718e..2041fda6 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,8 @@ "jquery.scrollto/jquery": "3.7.1", "admin-lte/jquery-validation": "^1.19.5", "admin-lte/jszip": "^3.10.1", - "admin-lte/**/jquery": "3.7.1" + "admin-lte/**/jquery": "3.7.1", + "admin-lte/sweetalert2": "10.16.9" }, "devDependencies": { "@babel/core": "^7.16.7", diff --git a/vstutils/__init__.py b/vstutils/__init__.py index d00aad55..d9dcb7b5 100644 --- a/vstutils/__init__.py +++ b/vstutils/__init__.py @@ -1,2 +1,2 @@ # pylint: disable=django-not-available -__version__: str = '5.8.5' +__version__: str = '5.8.6' diff --git a/yarn.lock b/yarn.lock index b8427684..239310db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7229,10 +7229,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -sweetalert2@^10.16.9: - version "10.16.11" - resolved "https://registry.yarnpkg.com/sweetalert2/-/sweetalert2-10.16.11.tgz#c8604059df2f31e06df56b02bd12b2f07ef7f098" - integrity sha512-Rdfabv2G89Tr8vmUTb1auWCYYesKBEWnkYPSi7XaiCIW0ZXXGK8Nw1wYKPEMLU6O8gMSMJe5m6MRKqMQsAQy9A== +sweetalert2@10.16.9, sweetalert2@^10.16.9: + version "10.16.9" + resolved "https://registry.yarnpkg.com/sweetalert2/-/sweetalert2-10.16.9.tgz#8ed86f2fa811a136667a48357e204348705be8c9" + integrity sha512-oNe+md5tmmS3fGfVHa7gVPlun7Td2oANSacnZCeghnrr3OHBi6UPVPU+GFrymwaDqwQspACilLRmRnM7aTjNPA== symbol-tree@^3.2.4: version "3.2.4"