committed by
GitHub
43 changed files with 3158 additions and 23 deletions
@ -0,0 +1,15 @@ |
|||
<script lang="ts" setup> |
|||
import { Page } from '@vben/common-ui'; |
|||
|
|||
import { FeatureDefinitionTable } from '@abp/features'; |
|||
|
|||
defineOptions({ |
|||
name: 'FeatureDefinitions', |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<Page> |
|||
<FeatureDefinitionTable /> |
|||
</Page> |
|||
</template> |
|||
@ -0,0 +1,15 @@ |
|||
<script lang="ts" setup> |
|||
import { Page } from '@vben/common-ui'; |
|||
|
|||
import { FeatureGroupDefinitionTable } from '@abp/features'; |
|||
|
|||
defineOptions({ |
|||
name: 'FeatureGroupDefinitions', |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<Page> |
|||
<FeatureGroupDefinitionTable /> |
|||
</Page> |
|||
</template> |
|||
@ -0,0 +1,39 @@ |
|||
{ |
|||
"name": "@abp/features", |
|||
"version": "9.0.4", |
|||
"homepage": "https://github.com/colinin/abp-next-admin", |
|||
"bugs": "https://github.com/colinin/abp-next-admin/issues", |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "git+https://github.com/colinin/abp-next-admin.git", |
|||
"directory": "packages/@abp/features" |
|||
}, |
|||
"license": "MIT", |
|||
"type": "module", |
|||
"sideEffects": [ |
|||
"**/*.css" |
|||
], |
|||
"exports": { |
|||
".": { |
|||
"types": "./src/index.ts", |
|||
"default": "./src/index.ts" |
|||
} |
|||
}, |
|||
"dependencies": { |
|||
"@abp/core": "workspace:*", |
|||
"@abp/request": "workspace:*", |
|||
"@abp/ui": "workspace:*", |
|||
"@ant-design/icons-vue": "catalog:", |
|||
"@vben/access": "workspace:*", |
|||
"@vben/common-ui": "workspace:*", |
|||
"@vben/hooks": "workspace:*", |
|||
"@vben/icons": "workspace:*", |
|||
"@vben/locales": "workspace:*", |
|||
"@vben/utils": "workspace:*", |
|||
"ant-design-vue": "catalog:", |
|||
"dayjs": "catalog:", |
|||
"vue": "catalog:*", |
|||
"vxe-table": "catalog:" |
|||
}, |
|||
"devDependencies": {} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export { useFeatureDefinitionsApi } from './useFeatureDefinitionsApi'; |
|||
export { useFeatureGroupDefinitionsApi } from './useFeatureGroupDefinitionsApi'; |
|||
export { useFeaturesApi } from './useFeaturesApi'; |
|||
@ -0,0 +1,100 @@ |
|||
import type { ListResultDto } from '@abp/core'; |
|||
|
|||
import type { |
|||
FeatureDefinitionCreateDto, |
|||
FeatureDefinitionDto, |
|||
FeatureDefinitionGetListInput, |
|||
FeatureDefinitionUpdateDto, |
|||
} from '../types/definitions'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useFeatureDefinitionsApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 删除功能定义 |
|||
* @param name 功能名称 |
|||
*/ |
|||
function deleteApi(name: string): Promise<void> { |
|||
return request(`/api/feature-management/definitions/${name}`, { |
|||
method: 'DELETE', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 查询功能定义 |
|||
* @param name 功能名称 |
|||
* @returns 功能定义数据传输对象 |
|||
*/ |
|||
function getApi(name: string): Promise<FeatureDefinitionDto> { |
|||
return request<FeatureDefinitionDto>( |
|||
`/api/feature-management/definitions/${name}`, |
|||
{ |
|||
method: 'GET', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 查询功能定义列表 |
|||
* @param input 功能过滤条件 |
|||
* @returns 功能定义数据传输对象列表 |
|||
*/ |
|||
function getListApi( |
|||
input?: FeatureDefinitionGetListInput, |
|||
): Promise<ListResultDto<FeatureDefinitionDto>> { |
|||
return request<ListResultDto<FeatureDefinitionDto>>( |
|||
`/api/feature-management/definitions`, |
|||
{ |
|||
method: 'GET', |
|||
params: input, |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 创建功能定义 |
|||
* @param input 功能定义参数 |
|||
* @returns 功能定义数据传输对象 |
|||
*/ |
|||
function createApi( |
|||
input: FeatureDefinitionCreateDto, |
|||
): Promise<FeatureDefinitionDto> { |
|||
return request<FeatureDefinitionDto>( |
|||
'/api/feature-management/definitions', |
|||
{ |
|||
data: input, |
|||
method: 'POST', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 更新功能定义 |
|||
* @param name 功能名称 |
|||
* @param input 功能定义参数 |
|||
* @returns 功能定义数据传输对象 |
|||
*/ |
|||
function updateApi( |
|||
name: string, |
|||
input: FeatureDefinitionUpdateDto, |
|||
): Promise<FeatureDefinitionDto> { |
|||
return request<FeatureDefinitionDto>( |
|||
`/api/feature-management/definitions/${name}`, |
|||
{ |
|||
data: input, |
|||
method: 'PUT', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
createApi, |
|||
deleteApi, |
|||
getApi, |
|||
getListApi, |
|||
updateApi, |
|||
}; |
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
import type { ListResultDto } from '@abp/core'; |
|||
|
|||
import type { |
|||
FeatureGroupDefinitionCreateDto, |
|||
FeatureGroupDefinitionDto, |
|||
FeatureGroupDefinitionGetListInput, |
|||
FeatureGroupDefinitionUpdateDto, |
|||
} from '../types/groups'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useFeatureGroupDefinitionsApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 删除功能定义 |
|||
* @param name 功能名称 |
|||
*/ |
|||
function deleteApi(name: string): Promise<void> { |
|||
return request(`/api/feature-management/definitions/groups/${name}`, { |
|||
method: 'DELETE', |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 查询功能定义 |
|||
* @param name 功能名称 |
|||
* @returns 功能定义数据传输对象 |
|||
*/ |
|||
function getApi(name: string): Promise<FeatureGroupDefinitionDto> { |
|||
return request<FeatureGroupDefinitionDto>( |
|||
`/api/feature-management/definitions/groups/${name}`, |
|||
{ |
|||
method: 'GET', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 查询功能定义列表 |
|||
* @param input 功能过滤条件 |
|||
* @returns 功能定义数据传输对象列表 |
|||
*/ |
|||
function getListApi( |
|||
input?: FeatureGroupDefinitionGetListInput, |
|||
): Promise<ListResultDto<FeatureGroupDefinitionDto>> { |
|||
return request<ListResultDto<FeatureGroupDefinitionDto>>( |
|||
`/api/feature-management/definitions/groups`, |
|||
{ |
|||
method: 'GET', |
|||
params: input, |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 创建功能定义 |
|||
* @param input 功能定义参数 |
|||
* @returns 功能定义数据传输对象 |
|||
*/ |
|||
function createApi( |
|||
input: FeatureGroupDefinitionCreateDto, |
|||
): Promise<FeatureGroupDefinitionDto> { |
|||
return request<FeatureGroupDefinitionDto>( |
|||
'/api/feature-management/definitions/groups', |
|||
{ |
|||
data: input, |
|||
method: 'POST', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 更新功能定义 |
|||
* @param name 功能名称 |
|||
* @param input 功能定义参数 |
|||
* @returns 功能定义数据传输对象 |
|||
*/ |
|||
function updateApi( |
|||
name: string, |
|||
input: FeatureGroupDefinitionUpdateDto, |
|||
): Promise<FeatureGroupDefinitionDto> { |
|||
return request<FeatureGroupDefinitionDto>( |
|||
`/api/feature-management/definitions/groups/${name}`, |
|||
{ |
|||
data: input, |
|||
method: 'PUT', |
|||
}, |
|||
); |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
createApi, |
|||
deleteApi, |
|||
getApi, |
|||
getListApi, |
|||
updateApi, |
|||
}; |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
import type { |
|||
FeatureProvider, |
|||
GetFeatureListResultDto, |
|||
UpdateFeaturesDto, |
|||
} from '../types/features'; |
|||
|
|||
import { useRequest } from '@abp/request'; |
|||
|
|||
export function useFeaturesApi() { |
|||
const { cancel, request } = useRequest(); |
|||
|
|||
/** |
|||
* 删除功能 |
|||
* @param {FeatureProvider} provider 参数 |
|||
* @returns {Promise<void>} |
|||
*/ |
|||
function deleteApi(provider: FeatureProvider): Promise<void> { |
|||
return request(`/api/feature-management/features`, { |
|||
method: 'DELETE', |
|||
params: provider, |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 查询功能 |
|||
* @param {FeatureProvider} provider 参数 |
|||
* @returns {Promise<GetFeatureListResultDto>} 功能实体数据传输对象 |
|||
*/ |
|||
function getApi(provider: FeatureProvider): Promise<GetFeatureListResultDto> { |
|||
return request<GetFeatureListResultDto>( |
|||
`/api/feature-management/features`, |
|||
{ |
|||
method: 'GET', |
|||
params: provider, |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 更新功能 |
|||
* @param {FeatureProvider} provider |
|||
* @param {UpdateFeaturesDto} input 参数 |
|||
* @returns {Promise<void>} |
|||
*/ |
|||
function updateApi( |
|||
provider: FeatureProvider, |
|||
input: UpdateFeaturesDto, |
|||
): Promise<void> { |
|||
return request(`/api/feature-management/features`, { |
|||
data: input, |
|||
method: 'PUT', |
|||
params: provider, |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
cancel, |
|||
deleteApi, |
|||
getApi, |
|||
updateApi, |
|||
}; |
|||
} |
|||
@ -0,0 +1,467 @@ |
|||
<script setup lang="ts"> |
|||
import type { |
|||
PropertyInfo, |
|||
SelectionStringValueItem, |
|||
StringValueTypeInstance, |
|||
} from '@abp/ui'; |
|||
import type { FormInstance } from 'ant-design-vue'; |
|||
|
|||
import type { FeatureDefinitionDto } from '../../../types/definitions'; |
|||
import type { FeatureGroupDefinitionDto } from '../../../types/groups'; |
|||
|
|||
import { |
|||
defineEmits, |
|||
defineOptions, |
|||
reactive, |
|||
ref, |
|||
toValue, |
|||
unref, |
|||
useTemplateRef, |
|||
} from 'vue'; |
|||
|
|||
import { useVbenModal } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
import { isBoolean, isString } from '@vben/utils'; |
|||
|
|||
import { |
|||
listToTree, |
|||
useLocalization, |
|||
useLocalizationSerializer, |
|||
useValidation, |
|||
ValidationEnum, |
|||
} from '@abp/core'; |
|||
import { LocalizableInput, PropertyTable, StringValueTypeInput } from '@abp/ui'; |
|||
import { |
|||
Checkbox, |
|||
Form, |
|||
Input, |
|||
InputNumber, |
|||
message, |
|||
Select, |
|||
Tabs, |
|||
Textarea, |
|||
TreeSelect, |
|||
} from 'ant-design-vue'; |
|||
|
|||
import { useFeatureDefinitionsApi } from '../../../api/useFeatureDefinitionsApi'; |
|||
import { useFeatureGroupDefinitionsApi } from '../../../api/useFeatureGroupDefinitionsApi'; |
|||
|
|||
defineOptions({ |
|||
name: 'FeatureDefinitionModal', |
|||
}); |
|||
const emits = defineEmits<{ |
|||
(event: 'change', data: FeatureDefinitionDto): void; |
|||
}>(); |
|||
|
|||
const FormItem = Form.Item; |
|||
const TabPane = Tabs.TabPane; |
|||
|
|||
interface DefinitionTreeVo { |
|||
children: DefinitionTreeVo[]; |
|||
displayName: string; |
|||
groupName: string; |
|||
name: string; |
|||
} |
|||
|
|||
type TabKeys = 'basic' | 'props'; |
|||
|
|||
const defaultModel = { |
|||
allowedProviders: [], |
|||
displayName: '', |
|||
extraProperties: {}, |
|||
groupName: '', |
|||
isAvailableToHost: true, |
|||
isEnabled: true, |
|||
isStatic: false, |
|||
isVisibleToClients: false, |
|||
name: '', |
|||
valueType: '', |
|||
} as FeatureDefinitionDto; |
|||
|
|||
const isEditModel = ref(false); |
|||
const validatorNameRef = ref<string>('NULL'); |
|||
const valueTypeNameRef = ref<string>('FreeTextStringValueType'); |
|||
const activeTab = ref<TabKeys>('basic'); |
|||
const form = useTemplateRef<FormInstance>('form'); |
|||
const valueTypeInput = useTemplateRef<StringValueTypeInstance>('valueType'); |
|||
const formModel = ref<FeatureDefinitionDto>({ ...defaultModel }); |
|||
const availableGroups = ref<FeatureGroupDefinitionDto[]>([]); |
|||
const availableDefinitions = ref<DefinitionTreeVo[]>([]); |
|||
const selectionDataSource = ref<{ label: string; value: string }[]>([]); |
|||
|
|||
const { Lr } = useLocalization(); |
|||
const { deserialize, validate } = useLocalizationSerializer(); |
|||
const { getListApi: getGroupsApi } = useFeatureGroupDefinitionsApi(); |
|||
const { defineValidator, fieldRequired, mapEnumValidMessage } = useValidation(); |
|||
const { |
|||
createApi, |
|||
getApi, |
|||
getListApi: getDefinitionsApi, |
|||
updateApi, |
|||
} = useFeatureDefinitionsApi(); |
|||
const formRules = reactive({ |
|||
defaultValue: defineValidator({ |
|||
trigger: 'change', |
|||
validator(_rule, value) { |
|||
const valueType = unref(valueTypeInput); |
|||
if (valueType) { |
|||
return valueType.validate(value); |
|||
} |
|||
return Promise.resolve(); |
|||
}, |
|||
}), |
|||
// description: defineValidator({ |
|||
// trigger: 'blur', |
|||
// validator(_rule, value) { |
|||
// if (!validate(value, { required: false })) { |
|||
// return Promise.reject( |
|||
// $t(ValidationEnum.FieldRequired, [$t('DisplayName:Description')]), |
|||
// ); |
|||
// } |
|||
// return Promise.resolve(); |
|||
// }, |
|||
// }), |
|||
displayName: defineValidator({ |
|||
required: true, |
|||
trigger: 'blur', |
|||
validator(_rule, value) { |
|||
if (!validate(value)) { |
|||
return Promise.reject( |
|||
mapEnumValidMessage(ValidationEnum.FieldRequired, [ |
|||
$t('AbpFeatureManagement.DisplayName:DisplayName'), |
|||
]), |
|||
); |
|||
} |
|||
return Promise.resolve(); |
|||
}, |
|||
}), |
|||
groupName: fieldRequired({ |
|||
name: 'GroupName', |
|||
prefix: 'DisplayName', |
|||
resourceName: 'AbpFeatureManagement', |
|||
trigger: 'blur', |
|||
}), |
|||
name: fieldRequired({ |
|||
name: 'Name', |
|||
prefix: 'DisplayName', |
|||
resourceName: 'AbpFeatureManagement', |
|||
trigger: 'blur', |
|||
}), |
|||
}); |
|||
|
|||
const [Modal, modalApi] = useVbenModal({ |
|||
class: 'w-1/2', |
|||
draggable: true, |
|||
fullscreenButton: false, |
|||
onCancel() { |
|||
modalApi.close(); |
|||
}, |
|||
onConfirm: async () => { |
|||
await form.value?.validate(); |
|||
const input = toValue(formModel); |
|||
if (input.defaultValue && !isString(input.defaultValue)) { |
|||
input.defaultValue = String(input.defaultValue); |
|||
} |
|||
const api = isEditModel.value |
|||
? updateApi(formModel.value.name, input) |
|||
: createApi(input); |
|||
modalApi.setState({ confirmLoading: true, loading: true }); |
|||
api |
|||
.then((res) => { |
|||
message.success($t('AbpUi.SavedSuccessfully')); |
|||
emits('change', res); |
|||
modalApi.close(); |
|||
}) |
|||
.finally(() => { |
|||
modalApi.setState({ confirmLoading: false, loading: false }); |
|||
}); |
|||
}, |
|||
onOpenChange: async (isOpen: boolean) => { |
|||
if (isOpen) { |
|||
isEditModel.value = false; |
|||
activeTab.value = 'basic'; |
|||
formModel.value = { ...defaultModel }; |
|||
availableDefinitions.value = []; |
|||
availableGroups.value = []; |
|||
modalApi.setState({ |
|||
showConfirmButton: true, |
|||
title: $t('AbpFeatureManagement.FeatureDefinitions:AddNew'), |
|||
}); |
|||
try { |
|||
modalApi.setState({ loading: true }); |
|||
const { groupName, name } = modalApi.getData<FeatureDefinitionDto>(); |
|||
name && (await onGet(name)); |
|||
await onInitGroups(groupName); |
|||
} finally { |
|||
modalApi.setState({ loading: false }); |
|||
} |
|||
} |
|||
}, |
|||
title: $t('AbpFeatureManagement.FeatureDefinitions:AddNew'), |
|||
}); |
|||
async function onInitGroups(name?: string) { |
|||
const { items } = await getGroupsApi({ filter: name }); |
|||
availableGroups.value = items.map((group) => { |
|||
const localizableGroup = deserialize(group.displayName); |
|||
return { |
|||
...group, |
|||
displayName: Lr(localizableGroup.resourceName, localizableGroup.name), |
|||
}; |
|||
}); |
|||
if (name) { |
|||
formModel.value.groupName = name; |
|||
await onGroupChange(name); |
|||
} |
|||
} |
|||
async function onGet(name: string) { |
|||
isEditModel.value = true; |
|||
const dto = await getApi(name); |
|||
formModel.value = dto; |
|||
modalApi.setState({ |
|||
showConfirmButton: !dto.isStatic, |
|||
title: `${$t('AbpFeatureManagement.FeatureDefinitions')} - ${dto.name}`, |
|||
}); |
|||
} |
|||
async function onGroupChange(name?: string) { |
|||
const { items } = await getDefinitionsApi({ |
|||
groupName: name, |
|||
}); |
|||
const features = items.map((item) => { |
|||
const displayName = deserialize(item.displayName); |
|||
const description = deserialize(item.description); |
|||
return { |
|||
...item, |
|||
description: Lr(description.resourceName, description.name), |
|||
disabled: item.name === formModel.value.name, |
|||
displayName: Lr(displayName.resourceName, displayName.name), |
|||
}; |
|||
}); |
|||
availableDefinitions.value = listToTree(features, { |
|||
id: 'name', |
|||
pid: 'parentName', |
|||
}); |
|||
} |
|||
function onPropChange(prop: PropertyInfo) { |
|||
formModel.value.extraProperties ??= {}; |
|||
formModel.value.extraProperties[prop.key] = prop.value; |
|||
} |
|||
function onPropDelete(prop: PropertyInfo) { |
|||
formModel.value.extraProperties ??= {}; |
|||
delete formModel.value.extraProperties[prop.key]; |
|||
} |
|||
function onValueTypeNameChange(valueTypeName: string) { |
|||
valueTypeNameRef.value = valueTypeName; |
|||
switch (valueTypeName) { |
|||
case 'ToggleStringValueType': { |
|||
if (!isBoolean(formModel.value.defaultValue)) { |
|||
formModel.value.defaultValue ??= 'false'; |
|||
} |
|||
break; |
|||
} |
|||
default: { |
|||
formModel.value.defaultValue ??= undefined; |
|||
break; |
|||
} |
|||
} |
|||
form.value?.clearValidate(); |
|||
} |
|||
|
|||
function onValidatorNameChange(validatorName: string) { |
|||
validatorNameRef.value = validatorName; |
|||
} |
|||
|
|||
function onSelectionChange(items: SelectionStringValueItem[]) { |
|||
if (items.length === 0) { |
|||
formModel.value.defaultValue = undefined; |
|||
selectionDataSource.value = []; |
|||
return; |
|||
} |
|||
selectionDataSource.value = items.map((item) => { |
|||
return { |
|||
label: Lr(item.displayText.resourceName, item.displayText.name), |
|||
value: item.value, |
|||
}; |
|||
}); |
|||
if (!items.some((item) => item.value === formModel.value.defaultValue)) { |
|||
formModel.value.defaultValue = undefined; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Modal> |
|||
<Form |
|||
ref="form" |
|||
:model="formModel" |
|||
:rules="formRules" |
|||
:label-col="{ span: 6 }" |
|||
:wrapper-col="{ span: 18 }" |
|||
> |
|||
<Tabs v-model:active-key="activeTab"> |
|||
<!-- 基本信息 --> |
|||
<TabPane key="basic" :tab="$t('AbpFeatureManagement.BasicInfo')"> |
|||
<FormItem |
|||
:label="$t('AbpFeatureManagement.DisplayName:GroupName')" |
|||
name="groupName" |
|||
> |
|||
<Select |
|||
v-model:value="formModel.groupName" |
|||
:allow-clear="true" |
|||
:disabled="formModel.isStatic" |
|||
:field-names="{ |
|||
label: 'displayName', |
|||
value: 'name', |
|||
}" |
|||
:options="availableGroups" |
|||
@change="(e) => onGroupChange(e?.toString())" |
|||
/> |
|||
</FormItem> |
|||
<FormItem |
|||
v-if="availableDefinitions.length > 0" |
|||
:label="$t('AbpFeatureManagement.DisplayName:ParentName')" |
|||
name="parentName" |
|||
> |
|||
<TreeSelect |
|||
v-model:value="formModel.parentName" |
|||
:allow-clear="true" |
|||
:disabled="formModel.isStatic" |
|||
:field-names="{ |
|||
label: 'displayName', |
|||
value: 'name', |
|||
children: 'children', |
|||
}" |
|||
:tree-data="availableDefinitions" |
|||
/> |
|||
</FormItem> |
|||
<FormItem |
|||
:label="$t('AbpFeatureManagement.DisplayName:Name')" |
|||
name="name" |
|||
> |
|||
<Input |
|||
v-model:value="formModel.name" |
|||
:disabled="formModel.isStatic" |
|||
autocomplete="off" |
|||
/> |
|||
</FormItem> |
|||
<FormItem |
|||
:label="$t('AbpFeatureManagement.DisplayName:DisplayName')" |
|||
name="displayName" |
|||
> |
|||
<LocalizableInput |
|||
v-model:value="formModel.displayName" |
|||
:disabled="formModel.isStatic" |
|||
/> |
|||
</FormItem> |
|||
<FormItem |
|||
:label="$t('AbpFeatureManagement.DisplayName:Description')" |
|||
name="description" |
|||
> |
|||
<LocalizableInput |
|||
v-model:value="formModel.description" |
|||
:disabled="formModel.isStatic" |
|||
/> |
|||
</FormItem> |
|||
<FormItem |
|||
name="defaultValue" |
|||
:label="$t('AbpFeatureManagement.DisplayName:DefaultValue')" |
|||
> |
|||
<Select |
|||
v-if="valueTypeNameRef === 'SelectionStringValueType'" |
|||
:disabled="formModel.isStatic" |
|||
:allow-clear="true" |
|||
v-model:value="formModel.defaultValue" |
|||
:options="selectionDataSource" |
|||
/> |
|||
<Textarea |
|||
v-else-if=" |
|||
valueTypeNameRef === 'FreeTextStringValueType' && |
|||
(validatorNameRef === 'NULL' || validatorNameRef === 'STRING') |
|||
" |
|||
:disabled="formModel.isStatic" |
|||
:allow-clear="true" |
|||
:auto-size="{ minRows: 3 }" |
|||
v-model:value="formModel.defaultValue" |
|||
/> |
|||
<InputNumber |
|||
v-else-if=" |
|||
valueTypeNameRef === 'FreeTextStringValueType' && |
|||
validatorNameRef === 'NUMERIC' |
|||
" |
|||
style="width: 100%" |
|||
:disabled="formModel.isStatic" |
|||
v-model:value="formModel.defaultValue" |
|||
/> |
|||
<Checkbox |
|||
v-else-if=" |
|||
valueTypeNameRef === 'ToggleStringValueType' && |
|||
validatorNameRef === 'BOOLEAN' |
|||
" |
|||
:disabled="formModel.isStatic" |
|||
:checked="formModel.defaultValue === 'true'" |
|||
@change=" |
|||
(e) => |
|||
(formModel.defaultValue = String( |
|||
e.target.checked, |
|||
).toLowerCase()) |
|||
" |
|||
> |
|||
{{ $t('AbpFeatureManagement.DisplayName:DefaultValue') }} |
|||
</Checkbox> |
|||
</FormItem> |
|||
<FormItem |
|||
name="isVisibleToClients" |
|||
:label="$t('AbpFeatureManagement.DisplayName:IsVisibleToClients')" |
|||
:extra="$t('AbpFeatureManagement.Description:IsVisibleToClients')" |
|||
> |
|||
<Checkbox v-model:checked="formModel.isVisibleToClients"> |
|||
{{ $t('AbpFeatureManagement.DisplayName:IsVisibleToClients') }} |
|||
</Checkbox> |
|||
</FormItem> |
|||
<FormItem |
|||
name="isAvailableToHost" |
|||
:label="$t('AbpFeatureManagement.DisplayName:IsAvailableToHost')" |
|||
:extra="$t('AbpFeatureManagement.Description:IsAvailableToHost')" |
|||
> |
|||
<Checkbox v-model:checked="formModel.isAvailableToHost"> |
|||
{{ $t('AbpFeatureManagement.DisplayName:IsAvailableToHost') }} |
|||
</Checkbox> |
|||
</FormItem> |
|||
</TabPane> |
|||
<TabPane |
|||
key="valueType" |
|||
:tab="$t('AbpFeatureManagement.ValueValidator')" |
|||
force-render |
|||
> |
|||
<FormItem |
|||
name="valueType" |
|||
label="" |
|||
:label-col="{ span: 0 }" |
|||
:wrapper-col="{ span: 24 }" |
|||
> |
|||
<StringValueTypeInput |
|||
ref="valueType" |
|||
:disabled="formModel.isStatic" |
|||
:allow-delete="true" |
|||
:allow-edit="true" |
|||
v-model:value="formModel.valueType" |
|||
@change:value-type="onValueTypeNameChange" |
|||
@change:validator="onValidatorNameChange" |
|||
@change:selection="onSelectionChange" |
|||
/> |
|||
</FormItem> |
|||
</TabPane> |
|||
<!-- 属性 --> |
|||
<TabPane key="props" :tab="$t('AbpFeatureManagement.Properties')"> |
|||
<PropertyTable |
|||
:data="formModel.extraProperties" |
|||
:disabled="formModel.isStatic" |
|||
@change="onPropChange" |
|||
@delete="onPropDelete" |
|||
/> |
|||
</TabPane> |
|||
</Tabs> |
|||
</Form> |
|||
</Modal> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,346 @@ |
|||
<script setup lang="ts"> |
|||
import type { VbenFormProps, VxeGridListeners, VxeGridProps } from '@abp/ui'; |
|||
|
|||
import type { FeatureDefinitionDto } from '../../../types/definitions'; |
|||
import type { FeatureGroupDefinitionDto } from '../../../types/groups'; |
|||
|
|||
import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue'; |
|||
|
|||
import { useVbenModal } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { |
|||
listToTree, |
|||
useLocalization, |
|||
useLocalizationSerializer, |
|||
} from '@abp/core'; |
|||
import { useVbenVxeGrid } from '@abp/ui'; |
|||
import { |
|||
CheckOutlined, |
|||
CloseOutlined, |
|||
DeleteOutlined, |
|||
EditOutlined, |
|||
PlusOutlined, |
|||
} from '@ant-design/icons-vue'; |
|||
import { Button, message, Modal } from 'ant-design-vue'; |
|||
import { VxeGrid } from 'vxe-table'; |
|||
|
|||
import { useFeatureDefinitionsApi } from '../../../api/useFeatureDefinitionsApi'; |
|||
import { useFeatureGroupDefinitionsApi } from '../../../api/useFeatureGroupDefinitionsApi'; |
|||
import { GroupDefinitionsPermissions } from '../../../constants/permissions'; |
|||
|
|||
defineOptions({ |
|||
name: 'FeatureDefinitionTable', |
|||
}); |
|||
|
|||
interface PermissionVo { |
|||
children: PermissionVo[]; |
|||
displayName: string; |
|||
groupName: string; |
|||
isEnabled: boolean; |
|||
isStatic: boolean; |
|||
name: string; |
|||
parentName?: string; |
|||
providers: string[]; |
|||
stateCheckers: string[]; |
|||
} |
|||
interface PermissionGroupVo { |
|||
displayName: string; |
|||
name: string; |
|||
permissions: PermissionVo[]; |
|||
} |
|||
|
|||
const permissionGroups = ref<PermissionGroupVo[]>([]); |
|||
const pageState = reactive({ |
|||
current: 1, |
|||
size: 10, |
|||
total: 0, |
|||
}); |
|||
|
|||
const { Lr } = useLocalization(); |
|||
const { deserialize } = useLocalizationSerializer(); |
|||
const { getListApi: getGroupsApi } = useFeatureGroupDefinitionsApi(); |
|||
const { deleteApi, getListApi: getPermissionsApi } = useFeatureDefinitionsApi(); |
|||
|
|||
const formOptions: VbenFormProps = { |
|||
// 默认展开 |
|||
collapsed: false, |
|||
handleReset: onReset, |
|||
async handleSubmit(params) { |
|||
pageState.current = 1; |
|||
await onGet(params); |
|||
}, |
|||
schema: [ |
|||
{ |
|||
component: 'Input', |
|||
fieldName: 'filter', |
|||
formItemClass: 'col-span-2 items-baseline', |
|||
label: $t('AbpUi.Search'), |
|||
}, |
|||
], |
|||
// 控制表单是否显示折叠按钮 |
|||
showCollapseButton: true, |
|||
// 按下回车时是否提交表单 |
|||
submitOnEnter: true, |
|||
}; |
|||
|
|||
const gridOptions: VxeGridProps<FeatureGroupDefinitionDto> = { |
|||
columns: [ |
|||
{ |
|||
align: 'center', |
|||
type: 'seq', |
|||
width: 50, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'group', |
|||
slots: { content: 'group' }, |
|||
type: 'expand', |
|||
width: 50, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'name', |
|||
minWidth: 150, |
|||
title: $t('AbpFeatureManagement.DisplayName:Name'), |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'displayName', |
|||
minWidth: 150, |
|||
title: $t('AbpFeatureManagement.DisplayName:DisplayName'), |
|||
}, |
|||
], |
|||
expandConfig: { |
|||
padding: true, |
|||
trigger: 'row', |
|||
}, |
|||
exportConfig: {}, |
|||
keepSource: true, |
|||
toolbarConfig: { |
|||
custom: true, |
|||
export: true, |
|||
refresh: false, |
|||
zoom: true, |
|||
}, |
|||
}; |
|||
const subGridColumns: VxeGridProps<FeatureDefinitionDto>['columns'] = [ |
|||
{ |
|||
align: 'center', |
|||
type: 'seq', |
|||
width: 50, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'name', |
|||
minWidth: 150, |
|||
title: $t('AbpFeatureManagement.DisplayName:Name'), |
|||
treeNode: true, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'displayName', |
|||
minWidth: 120, |
|||
title: $t('AbpFeatureManagement.DisplayName:DisplayName'), |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'description', |
|||
minWidth: 120, |
|||
title: $t('AbpFeatureManagement.DisplayName:Description'), |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'isVisibleToClients', |
|||
minWidth: 120, |
|||
slots: { default: 'isVisibleToClients' }, |
|||
title: $t('AbpFeatureManagement.DisplayName:IsVisibleToClients'), |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'isAvailableToHost', |
|||
minWidth: 120, |
|||
slots: { default: 'isAvailableToHost' }, |
|||
title: $t('AbpFeatureManagement.DisplayName:IsAvailableToHost'), |
|||
}, |
|||
{ |
|||
field: 'action', |
|||
fixed: 'right', |
|||
slots: { default: 'action' }, |
|||
title: $t('AbpUi.Actions'), |
|||
width: 180, |
|||
}, |
|||
]; |
|||
|
|||
const gridEvents: VxeGridListeners<FeatureGroupDefinitionDto> = { |
|||
pageChange(params) { |
|||
pageState.current = params.currentPage; |
|||
pageState.size = params.pageSize; |
|||
onPageChange(); |
|||
}, |
|||
}; |
|||
|
|||
const [GroupGrid, gridApi] = useVbenVxeGrid({ |
|||
formOptions, |
|||
gridEvents, |
|||
gridOptions, |
|||
}); |
|||
|
|||
const [FeatureDefinitionModal, modalApi] = useVbenModal({ |
|||
connectedComponent: defineAsyncComponent( |
|||
() => import('./FeatureDefinitionModal.vue'), |
|||
), |
|||
}); |
|||
|
|||
async function onGet(input?: Record<string, string>) { |
|||
try { |
|||
gridApi.setLoading(true); |
|||
const groupRes = await getGroupsApi(input); |
|||
const permissionRes = await getPermissionsApi(input); |
|||
pageState.total = groupRes.items.length; |
|||
permissionGroups.value = groupRes.items.map((group) => { |
|||
const localizableGroup = deserialize(group.displayName); |
|||
const permissions = permissionRes.items |
|||
.filter((permission) => permission.groupName === group.name) |
|||
.map((permission) => { |
|||
const displayName = deserialize(permission.displayName); |
|||
const description = deserialize(permission.displayName); |
|||
return { |
|||
...permission, |
|||
description: Lr(description.resourceName, description.name), |
|||
displayName: Lr(displayName.resourceName, displayName.name), |
|||
}; |
|||
}); |
|||
return { |
|||
...group, |
|||
displayName: Lr(localizableGroup.resourceName, localizableGroup.name), |
|||
permissions: listToTree(permissions, { |
|||
id: 'name', |
|||
pid: 'parentName', |
|||
}), |
|||
}; |
|||
}); |
|||
onPageChange(); |
|||
} finally { |
|||
gridApi.setLoading(false); |
|||
} |
|||
} |
|||
|
|||
async function onReset() { |
|||
await gridApi.formApi.resetForm(); |
|||
const input = await gridApi.formApi.getValues(); |
|||
await onGet(input); |
|||
} |
|||
|
|||
function onPageChange() { |
|||
const items = permissionGroups.value.slice( |
|||
(pageState.current - 1) * pageState.size, |
|||
pageState.current * pageState.size, |
|||
); |
|||
gridApi.setGridOptions({ |
|||
data: items, |
|||
pagerConfig: { |
|||
currentPage: pageState.current, |
|||
pageSize: pageState.size, |
|||
total: pageState.total, |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
function onCreate() { |
|||
modalApi.setData({}); |
|||
modalApi.open(); |
|||
} |
|||
|
|||
function onUpdate(row: FeatureDefinitionDto) { |
|||
modalApi.setData(row); |
|||
modalApi.open(); |
|||
} |
|||
|
|||
function onDelete(row: FeatureDefinitionDto) { |
|||
Modal.confirm({ |
|||
centered: true, |
|||
content: `${$t('AbpUi.ItemWillBeDeletedMessageWithFormat', [row.name])}`, |
|||
onOk: async () => { |
|||
await deleteApi(row.name); |
|||
message.success($t('AbpUi.SuccessfullyDeleted')); |
|||
onGet(); |
|||
}, |
|||
title: $t('AbpUi.AreYouSure'), |
|||
}); |
|||
} |
|||
|
|||
onMounted(onGet); |
|||
</script> |
|||
|
|||
<template> |
|||
<GroupGrid :table-title="$t('AbpFeatureManagement.FeatureDefinitions')"> |
|||
<template #toolbar-tools> |
|||
<Button |
|||
:icon="h(PlusOutlined)" |
|||
type="primary" |
|||
v-access:code="[GroupDefinitionsPermissions.Create]" |
|||
@click="onCreate" |
|||
> |
|||
{{ $t('AbpFeatureManagement.FeatureDefinitions:AddNew') }} |
|||
</Button> |
|||
</template> |
|||
<template #group="{ row: group }"> |
|||
<VxeGrid |
|||
:columns="subGridColumns" |
|||
:data="group.permissions" |
|||
:tree-config="{ |
|||
trigger: 'row', |
|||
rowField: 'name', |
|||
childrenField: 'children', |
|||
}" |
|||
> |
|||
<template #isVisibleToClients="{ row }"> |
|||
<div class="flex flex-row justify-center"> |
|||
<CheckOutlined |
|||
v-if="row.isVisibleToClients" |
|||
class="text-green-500" |
|||
/> |
|||
<CloseOutlined v-else class="text-red-500" /> |
|||
</div> |
|||
</template> |
|||
<template #isAvailableToHost="{ row }"> |
|||
<div class="flex flex-row justify-center"> |
|||
<CheckOutlined |
|||
v-if="row.isAvailableToHost" |
|||
class="text-green-500" |
|||
/> |
|||
<CloseOutlined v-else class="text-red-500" /> |
|||
</div> |
|||
</template> |
|||
<template #action="{ row: permission }"> |
|||
<div class="flex flex-row"> |
|||
<Button |
|||
:icon="h(EditOutlined)" |
|||
block |
|||
type="link" |
|||
v-access:code="[GroupDefinitionsPermissions.Update]" |
|||
@click="onUpdate(permission)" |
|||
> |
|||
{{ $t('AbpUi.Edit') }} |
|||
</Button> |
|||
<Button |
|||
v-if="!permission.isStatic" |
|||
:icon="h(DeleteOutlined)" |
|||
block |
|||
danger |
|||
type="link" |
|||
v-access:code="[GroupDefinitionsPermissions.Delete]" |
|||
@click="onDelete(permission)" |
|||
> |
|||
{{ $t('AbpUi.Delete') }} |
|||
</Button> |
|||
</div> |
|||
</template> |
|||
</VxeGrid> |
|||
</template> |
|||
</GroupGrid> |
|||
<FeatureDefinitionModal @change="() => onGet()" /> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,150 @@ |
|||
<script setup lang="ts"> |
|||
import type { PropertyInfo } from '@abp/ui'; |
|||
import type { FormInstance } from 'ant-design-vue'; |
|||
|
|||
import type { FeatureGroupDefinitionDto } from '../../../types/groups'; |
|||
|
|||
import { defineEmits, defineOptions, ref, toValue, useTemplateRef } from 'vue'; |
|||
|
|||
import { useVbenModal } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { LocalizableInput, PropertyTable } from '@abp/ui'; |
|||
import { Form, Input, message, Tabs } from 'ant-design-vue'; |
|||
|
|||
import { useFeatureGroupDefinitionsApi } from '../../../api/useFeatureGroupDefinitionsApi'; |
|||
|
|||
defineOptions({ |
|||
name: 'FeatureGroupDefinitionModal', |
|||
}); |
|||
const emits = defineEmits<{ |
|||
(event: 'change', data: FeatureGroupDefinitionDto): void; |
|||
}>(); |
|||
|
|||
const FormItem = Form.Item; |
|||
const TabPane = Tabs.TabPane; |
|||
|
|||
type TabKeys = 'basic' | 'props'; |
|||
|
|||
const defaultModel = {} as FeatureGroupDefinitionDto; |
|||
|
|||
const isEditModel = ref(false); |
|||
const activeTab = ref<TabKeys>('basic'); |
|||
const form = useTemplateRef<FormInstance>('form'); |
|||
const formModel = ref<FeatureGroupDefinitionDto>({ ...defaultModel }); |
|||
|
|||
const { cancel, createApi, getApi, updateApi } = |
|||
useFeatureGroupDefinitionsApi(); |
|||
const [Modal, modalApi] = useVbenModal({ |
|||
class: 'w-1/2', |
|||
draggable: true, |
|||
fullscreenButton: false, |
|||
onCancel() { |
|||
modalApi.close(); |
|||
}, |
|||
onClosed() { |
|||
cancel('FeatureGroupDefinitionModal has closed!'); |
|||
}, |
|||
onConfirm: async () => { |
|||
await form.value?.validate(); |
|||
const api = isEditModel.value |
|||
? updateApi(formModel.value.name, toValue(formModel)) |
|||
: createApi(toValue(formModel)); |
|||
modalApi.setState({ confirmLoading: true, loading: true }); |
|||
api |
|||
.then((res) => { |
|||
message.success($t('AbpUi.SavedSuccessfully')); |
|||
emits('change', res); |
|||
modalApi.close(); |
|||
}) |
|||
.finally(() => { |
|||
modalApi.setState({ confirmLoading: false, loading: false }); |
|||
}); |
|||
}, |
|||
onOpenChange: async (isOpen: boolean) => { |
|||
if (isOpen) { |
|||
isEditModel.value = false; |
|||
activeTab.value = 'basic'; |
|||
formModel.value = { ...defaultModel }; |
|||
modalApi.setState({ |
|||
showConfirmButton: true, |
|||
title: $t('AbpFeatureManagement.GroupDefinitions:AddNew'), |
|||
}); |
|||
try { |
|||
modalApi.setState({ loading: true }); |
|||
const { name } = modalApi.getData<FeatureGroupDefinitionDto>(); |
|||
name && (await onGet(name)); |
|||
} finally { |
|||
modalApi.setState({ loading: false }); |
|||
} |
|||
} |
|||
}, |
|||
title: $t('AbpFeatureManagement.GroupDefinitions:AddNew'), |
|||
}); |
|||
async function onGet(name: string) { |
|||
isEditModel.value = true; |
|||
const dto = await getApi(name); |
|||
formModel.value = dto; |
|||
modalApi.setState({ |
|||
showConfirmButton: !dto.isStatic, |
|||
title: `${$t('AbpFeatureManagement.GroupDefinitions')} - ${dto.name}`, |
|||
}); |
|||
} |
|||
function onPropChange(prop: PropertyInfo) { |
|||
formModel.value.extraProperties ??= {}; |
|||
formModel.value.extraProperties[prop.key] = prop.value; |
|||
} |
|||
function onPropDelete(prop: PropertyInfo) { |
|||
formModel.value.extraProperties ??= {}; |
|||
delete formModel.value.extraProperties[prop.key]; |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Modal> |
|||
<Form |
|||
ref="form" |
|||
:label-col="{ span: 6 }" |
|||
:model="formModel" |
|||
:wrapper-col="{ span: 18 }" |
|||
> |
|||
<Tabs v-model:active-key="activeTab"> |
|||
<!-- 基本信息 --> |
|||
<TabPane key="basic" :tab="$t('AbpFeatureManagement.BasicInfo')"> |
|||
<FormItem |
|||
:label="$t('AbpFeatureManagement.DisplayName:Name')" |
|||
name="name" |
|||
required |
|||
> |
|||
<Input |
|||
v-model:value="formModel.name" |
|||
:disabled="formModel.isStatic" |
|||
autocomplete="off" |
|||
/> |
|||
</FormItem> |
|||
<FormItem |
|||
:label="$t('AbpFeatureManagement.DisplayName:DisplayName')" |
|||
name="displayName" |
|||
required |
|||
> |
|||
<LocalizableInput |
|||
v-model:value="formModel.displayName" |
|||
:disabled="formModel.isStatic" |
|||
/> |
|||
</FormItem> |
|||
</TabPane> |
|||
<!-- 属性 --> |
|||
<TabPane key="props" :tab="$t('AbpFeatureManagement.Properties')"> |
|||
<PropertyTable |
|||
:data="formModel.extraProperties" |
|||
:disabled="formModel.isStatic" |
|||
@change="onPropChange" |
|||
@delete="onPropDelete" |
|||
/> |
|||
</TabPane> |
|||
</Tabs> |
|||
</Form> |
|||
</Modal> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,261 @@ |
|||
<script setup lang="ts"> |
|||
import type { VbenFormProps, VxeGridListeners, VxeGridProps } from '@abp/ui'; |
|||
import type { MenuInfo } from 'ant-design-vue/es/menu/src/interface'; |
|||
|
|||
import type { FeatureGroupDefinitionDto } from '../../../types/groups'; |
|||
|
|||
import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue'; |
|||
|
|||
import { useAccess } from '@vben/access'; |
|||
import { useVbenModal } from '@vben/common-ui'; |
|||
import { createIconifyIcon } from '@vben/icons'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { useLocalization, useLocalizationSerializer } from '@abp/core'; |
|||
import { useVbenVxeGrid } from '@abp/ui'; |
|||
import { |
|||
DeleteOutlined, |
|||
EditOutlined, |
|||
EllipsisOutlined, |
|||
PlusOutlined, |
|||
} from '@ant-design/icons-vue'; |
|||
import { Button, Dropdown, Menu, message, Modal } from 'ant-design-vue'; |
|||
|
|||
import { useFeatureGroupDefinitionsApi } from '../../../api/useFeatureGroupDefinitionsApi'; |
|||
import { |
|||
FeatureDefinitionsPermissions, |
|||
GroupDefinitionsPermissions, |
|||
} from '../../../constants/permissions'; |
|||
|
|||
defineOptions({ |
|||
name: 'FeatureGroupDefinitionTable', |
|||
}); |
|||
|
|||
const MenuItem = Menu.Item; |
|||
|
|||
const FeaturesOutlined = createIconifyIcon('pajamas:feature-flag'); |
|||
|
|||
const permissionGroups = ref<FeatureGroupDefinitionDto[]>([]); |
|||
const pageState = reactive({ |
|||
current: 1, |
|||
size: 10, |
|||
total: 0, |
|||
}); |
|||
|
|||
const { Lr } = useLocalization(); |
|||
const { hasAccessByCodes } = useAccess(); |
|||
const { deserialize } = useLocalizationSerializer(); |
|||
const { deleteApi, getListApi } = useFeatureGroupDefinitionsApi(); |
|||
|
|||
const formOptions: VbenFormProps = { |
|||
// 默认展开 |
|||
collapsed: false, |
|||
handleReset: onReset, |
|||
async handleSubmit(params) { |
|||
pageState.current = 1; |
|||
await onGet(params); |
|||
}, |
|||
schema: [ |
|||
{ |
|||
component: 'Input', |
|||
fieldName: 'filter', |
|||
formItemClass: 'col-span-2 items-baseline', |
|||
label: $t('AbpUi.Search'), |
|||
}, |
|||
], |
|||
// 控制表单是否显示折叠按钮 |
|||
showCollapseButton: true, |
|||
// 按下回车时是否提交表单 |
|||
submitOnEnter: true, |
|||
}; |
|||
|
|||
const gridOptions: VxeGridProps<FeatureGroupDefinitionDto> = { |
|||
columns: [ |
|||
{ |
|||
align: 'left', |
|||
field: 'name', |
|||
minWidth: 150, |
|||
title: $t('AbpFeatureManagement.DisplayName:Name'), |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
field: 'displayName', |
|||
minWidth: 150, |
|||
title: $t('AbpFeatureManagement.DisplayName:DisplayName'), |
|||
}, |
|||
{ |
|||
field: 'action', |
|||
fixed: 'right', |
|||
slots: { default: 'action' }, |
|||
title: $t('AbpUi.Actions'), |
|||
width: 220, |
|||
}, |
|||
], |
|||
exportConfig: {}, |
|||
keepSource: true, |
|||
toolbarConfig: { |
|||
custom: true, |
|||
export: true, |
|||
refresh: false, |
|||
zoom: true, |
|||
}, |
|||
}; |
|||
|
|||
const gridEvents: VxeGridListeners<FeatureGroupDefinitionDto> = { |
|||
pageChange(params) { |
|||
pageState.current = params.currentPage; |
|||
pageState.size = params.pageSize; |
|||
onPageChange(); |
|||
}, |
|||
}; |
|||
|
|||
const [Grid, gridApi] = useVbenVxeGrid({ |
|||
formOptions, |
|||
gridEvents, |
|||
gridOptions, |
|||
}); |
|||
|
|||
const [FeatureGroupDefinitionModal, groupModalApi] = useVbenModal({ |
|||
connectedComponent: defineAsyncComponent( |
|||
() => import('./FeatureGroupDefinitionModal.vue'), |
|||
), |
|||
}); |
|||
const [FeatureDefinitionModal, defineModalApi] = useVbenModal({ |
|||
connectedComponent: defineAsyncComponent( |
|||
() => import('../features/FeatureDefinitionModal.vue'), |
|||
), |
|||
}); |
|||
|
|||
async function onGet(input?: Record<string, string>) { |
|||
try { |
|||
gridApi.setLoading(true); |
|||
const { items } = await getListApi(input); |
|||
pageState.total = items.length; |
|||
permissionGroups.value = items.map((item) => { |
|||
const localizableString = deserialize(item.displayName); |
|||
return { |
|||
...item, |
|||
displayName: Lr(localizableString.resourceName, localizableString.name), |
|||
}; |
|||
}); |
|||
onPageChange(); |
|||
} finally { |
|||
gridApi.setLoading(false); |
|||
} |
|||
} |
|||
|
|||
async function onReset() { |
|||
await gridApi.formApi.resetForm(); |
|||
const input = await gridApi.formApi.getValues(); |
|||
await onGet(input); |
|||
} |
|||
|
|||
function onPageChange() { |
|||
const items = permissionGroups.value.slice( |
|||
(pageState.current - 1) * pageState.size, |
|||
pageState.current * pageState.size, |
|||
); |
|||
gridApi.setGridOptions({ |
|||
data: items, |
|||
pagerConfig: { |
|||
currentPage: pageState.current, |
|||
pageSize: pageState.size, |
|||
total: pageState.total, |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
function onCreate() { |
|||
groupModalApi.setData({}); |
|||
groupModalApi.open(); |
|||
} |
|||
|
|||
function onUpdate(row: FeatureGroupDefinitionDto) { |
|||
groupModalApi.setData(row); |
|||
groupModalApi.open(); |
|||
} |
|||
|
|||
function onMenuClick(row: FeatureGroupDefinitionDto, info: MenuInfo) { |
|||
switch (info.key) { |
|||
case 'features': { |
|||
defineModalApi.setData({ |
|||
groupName: row.name, |
|||
}); |
|||
defineModalApi.open(); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
function onDelete(row: FeatureGroupDefinitionDto) { |
|||
Modal.confirm({ |
|||
centered: true, |
|||
content: `${$t('AbpUi.ItemWillBeDeletedMessageWithFormat', [row.name])}`, |
|||
onOk: async () => { |
|||
await deleteApi(row.name); |
|||
message.success($t('AbpUi.SuccessfullyDeleted')); |
|||
onGet(); |
|||
}, |
|||
title: $t('AbpUi.AreYouSure'), |
|||
}); |
|||
} |
|||
|
|||
onMounted(onGet); |
|||
</script> |
|||
|
|||
<template> |
|||
<Grid :table-title="$t('AbpFeatureManagement.GroupDefinitions')"> |
|||
<template #toolbar-tools> |
|||
<Button |
|||
:icon="h(PlusOutlined)" |
|||
type="primary" |
|||
v-access:code="[GroupDefinitionsPermissions.Create]" |
|||
@click="onCreate" |
|||
> |
|||
{{ $t('AbpFeatureManagement.GroupDefinitions:AddNew') }} |
|||
</Button> |
|||
</template> |
|||
<template #action="{ row }"> |
|||
<div class="flex flex-row"> |
|||
<Button |
|||
:icon="h(EditOutlined)" |
|||
block |
|||
type="link" |
|||
v-access:code="[GroupDefinitionsPermissions.Update]" |
|||
@click="onUpdate(row)" |
|||
> |
|||
{{ $t('AbpUi.Edit') }} |
|||
</Button> |
|||
<Button |
|||
v-if="!row.isStatic" |
|||
:icon="h(DeleteOutlined)" |
|||
block |
|||
danger |
|||
type="link" |
|||
v-access:code="[GroupDefinitionsPermissions.Delete]" |
|||
@click="onDelete(row)" |
|||
> |
|||
{{ $t('AbpUi.Delete') }} |
|||
</Button> |
|||
<Dropdown v-if="!row.isStatic"> |
|||
<template #overlay> |
|||
<Menu @click="(info) => onMenuClick(row, info)"> |
|||
<MenuItem |
|||
v-if="hasAccessByCodes([FeatureDefinitionsPermissions.Create])" |
|||
key="features" |
|||
:icon="h(FeaturesOutlined)" |
|||
> |
|||
{{ $t('AbpFeatureManagement.GroupDefinitions:AddNew') }} |
|||
</MenuItem> |
|||
</Menu> |
|||
</template> |
|||
<Button :icon="h(EllipsisOutlined)" type="link" /> |
|||
</Dropdown> |
|||
</div> |
|||
</template> |
|||
</Grid> |
|||
<FeatureGroupDefinitionModal @change="() => onGet()" /> |
|||
<FeatureDefinitionModal /> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,262 @@ |
|||
<script setup lang="ts"> |
|||
import type { SelectionStringValueType, Validator } from '@abp/core'; |
|||
import type { FormInstance } from 'ant-design-vue/es/form/Form'; |
|||
|
|||
import type { FeatureGroupDto, UpdateFeaturesDto } from '../../types/features'; |
|||
|
|||
import { computed, ref, toValue, useTemplateRef } from 'vue'; |
|||
|
|||
import { useVbenModal } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { useValidation } from '@abp/core'; |
|||
import { |
|||
Checkbox, |
|||
Form, |
|||
Input, |
|||
InputNumber, |
|||
message, |
|||
Select, |
|||
Tabs, |
|||
} from 'ant-design-vue'; |
|||
|
|||
import { useFeaturesApi } from '../../api/useFeaturesApi'; |
|||
|
|||
const FormItem = Form.Item; |
|||
const TabPane = Tabs.TabPane; |
|||
|
|||
interface ModalState { |
|||
displayName?: string; |
|||
providerKey?: string; |
|||
providerName: string; |
|||
readonly?: boolean; |
|||
} |
|||
|
|||
interface FormModel { |
|||
groups: FeatureGroupDto[]; |
|||
} |
|||
|
|||
const activeTabKey = ref(''); |
|||
const modelState = ref<ModalState>(); |
|||
const formModel = ref<FormModel>({ groups: [] }); |
|||
const form = useTemplateRef<FormInstance>('form'); |
|||
|
|||
const getModalTitle = computed(() => { |
|||
if (modelState.value?.displayName) { |
|||
return `${$t('AbpFeatureManagement.Features')} - ${modelState.value.displayName}`; |
|||
} |
|||
return $t('AbpFeatureManagement.Features'); |
|||
}); |
|||
|
|||
const { getApi, updateApi } = useFeaturesApi(); |
|||
const { |
|||
fieldMustBeetWeen, |
|||
fieldMustBeStringWithMinimumLengthAndMaximumLength, |
|||
fieldRequired, |
|||
} = useValidation(); |
|||
|
|||
const [Modal, modalApi] = useVbenModal({ |
|||
centered: true, |
|||
class: 'w-1/2', |
|||
async onConfirm() { |
|||
await form.value?.validate(); |
|||
await onSubmit(); |
|||
}, |
|||
async onOpenChange(isOpen) { |
|||
if (isOpen) { |
|||
formModel.value = { groups: [] }; |
|||
await onGet(); |
|||
if (formModel.value?.groups.length > 0) { |
|||
activeTabKey.value = formModel.value.groups[0]?.name!; |
|||
} |
|||
} |
|||
}, |
|||
}); |
|||
function mapFeatures(groups: FeatureGroupDto[]) { |
|||
groups.forEach((group) => { |
|||
group.features.forEach((feature) => { |
|||
if (feature.valueType.name === 'SelectionStringValueType') { |
|||
const valueType = |
|||
feature.valueType as unknown as SelectionStringValueType; |
|||
valueType.itemSource.items.forEach((valueItem) => { |
|||
if (valueItem.displayText.resourceName === 'Fixed') { |
|||
valueItem.displayName = valueItem.displayText.name; |
|||
return; |
|||
} |
|||
valueItem.displayName = $t( |
|||
`${valueItem.displayText.resourceName}.${valueItem.displayText.name}`, |
|||
); |
|||
}); |
|||
} else { |
|||
switch (feature.valueType?.validator.name) { |
|||
case 'BOOLEAN': { |
|||
feature.value = |
|||
String(feature.value).toLocaleLowerCase() === 'true'; |
|||
break; |
|||
} |
|||
case 'NUMERIC': { |
|||
feature.value = Number(feature.value); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
}); |
|||
return groups; |
|||
} |
|||
function getFeatureInput(groups: FeatureGroupDto[]): UpdateFeaturesDto { |
|||
const input: UpdateFeaturesDto = { |
|||
features: [], |
|||
}; |
|||
groups.forEach((g) => { |
|||
g.features.forEach((f) => { |
|||
if (f.value !== null) { |
|||
input.features.push({ |
|||
name: f.name, |
|||
value: String(f.value), |
|||
}); |
|||
} |
|||
}); |
|||
}); |
|||
return input; |
|||
} |
|||
function createRules(field: string, validator: Validator) { |
|||
const featureRules: { [key: string]: any }[] = []; |
|||
if (validator.properties) { |
|||
switch (validator.name) { |
|||
case 'NUMERIC': { |
|||
featureRules.push( |
|||
...fieldMustBeetWeen({ |
|||
end: Number(validator.properties.MaxValue), |
|||
name: field, |
|||
start: Number(validator.properties.MinValue), |
|||
trigger: 'change', |
|||
}), |
|||
); |
|||
break; |
|||
} |
|||
case 'STRING': { |
|||
if ( |
|||
validator.properties.AllowNull && |
|||
validator.properties.AllowNull.toLowerCase() === 'true' |
|||
) { |
|||
featureRules.push( |
|||
...fieldRequired({ |
|||
name: field, |
|||
trigger: 'blur', |
|||
}), |
|||
); |
|||
} |
|||
featureRules.push( |
|||
...fieldMustBeStringWithMinimumLengthAndMaximumLength({ |
|||
maximum: Number(validator.properties.MaxLength), |
|||
minimum: Number(validator.properties.MinLength), |
|||
name: field, |
|||
trigger: 'blur', |
|||
}), |
|||
); |
|||
break; |
|||
} |
|||
default: { |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
return featureRules; |
|||
} |
|||
async function onGet() { |
|||
try { |
|||
modalApi.setState({ loading: true }); |
|||
const state = modalApi.getData<ModalState>(); |
|||
const { groups } = await getApi({ |
|||
providerKey: state.providerKey, |
|||
providerName: state.providerName, |
|||
}); |
|||
formModel.value = { |
|||
groups: mapFeatures(groups), |
|||
}; |
|||
modelState.value = state; |
|||
} finally { |
|||
modalApi.setState({ loading: false }); |
|||
} |
|||
} |
|||
async function onSubmit() { |
|||
try { |
|||
modalApi.setState({ submitting: true }); |
|||
const model = toValue(formModel); |
|||
const state = modalApi.getData<ModalState>(); |
|||
const input = getFeatureInput(model.groups); |
|||
await updateApi( |
|||
{ |
|||
providerKey: state.providerKey, |
|||
providerName: state.providerName, |
|||
}, |
|||
input, |
|||
); |
|||
message.success($t('AbpUi.SavedSuccessfully')); |
|||
await onGet(); |
|||
} finally { |
|||
modalApi.setState({ submitting: false }); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Modal :title="getModalTitle"> |
|||
<Form :model="formModel" ref="form"> |
|||
<Tabs tab-position="left" v-model:active-key="activeTabKey"> |
|||
<TabPane |
|||
v-for="(group, gi) in formModel.groups" |
|||
:key="group.name" |
|||
:tab="group.displayName" |
|||
> |
|||
<template v-for="(feature, fi) in group.features" :key="feature.name"> |
|||
<FormItem |
|||
v-if="feature.valueType !== null" |
|||
:name="['groups', gi, 'features', fi, 'value']" |
|||
:label="feature.displayName" |
|||
:extra="feature.description" |
|||
:rules=" |
|||
createRules(feature.displayName, feature.valueType.validator) |
|||
" |
|||
> |
|||
<Checkbox |
|||
v-if=" |
|||
feature.valueType.name === 'ToggleStringValueType' && |
|||
feature.valueType.validator.name === 'BOOLEAN' |
|||
" |
|||
v-model:checked="feature.value" |
|||
> |
|||
{{ feature.displayName }} |
|||
</Checkbox> |
|||
<div |
|||
v-else-if="feature.valueType.name === 'FreeTextStringValueType'" |
|||
> |
|||
<InputNumber |
|||
v-if="feature.valueType.validator.name === 'NUMERIC'" |
|||
style="width: 100%" |
|||
v-model:value="feature.value" |
|||
/> |
|||
<Input |
|||
v-else |
|||
v-model:value="feature.value" |
|||
autocomplete="off" |
|||
/> |
|||
</div> |
|||
<Select |
|||
v-else-if=" |
|||
feature.valueType.name === 'SelectionStringValueType' |
|||
" |
|||
v-model:value="feature.value" |
|||
:options="feature.valueType.itemSource.items" |
|||
:field-names="{ label: 'displayName', value: 'value' }" |
|||
/> |
|||
</FormItem> |
|||
</template> |
|||
</TabPane> |
|||
</Tabs> |
|||
</Form> |
|||
</Modal> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,3 @@ |
|||
export { default as FeatureDefinitionTable } from './definitions/features/FeatureDefinitionTable.vue'; |
|||
export { default as FeatureGroupDefinitionTable } from './definitions/groups/FeatureGroupDefinitionTable.vue'; |
|||
export { default as FeatureModal } from './features/FeatureModal.vue'; |
|||
@ -0,0 +1 @@ |
|||
export * from './permissions'; |
|||
@ -0,0 +1,20 @@ |
|||
/** 分组权限 */ |
|||
export const GroupDefinitionsPermissions = { |
|||
/** 新增 */ |
|||
Create: 'FeatureManagement.GroupDefinitions.Create', |
|||
Default: 'FeatureManagement.GroupDefinitions', |
|||
/** 删除 */ |
|||
Delete: 'FeatureManagement.GroupDefinitions.Delete', |
|||
/** 更新 */ |
|||
Update: 'FeatureManagement.GroupDefinitions.Update', |
|||
}; |
|||
/** 功能定义权限 */ |
|||
export const FeatureDefinitionsPermissions = { |
|||
/** 新增 */ |
|||
Create: 'FeatureManagement.Definitions.Create', |
|||
Default: 'FeatureManagement.Definitions', |
|||
/** 删除 */ |
|||
Delete: 'FeatureManagement.Definitions.Delete', |
|||
/** 更新 */ |
|||
Update: 'FeatureManagement.Definitions.Update', |
|||
}; |
|||
@ -0,0 +1,4 @@ |
|||
export * from './api'; |
|||
export * from './components'; |
|||
export * from './constants'; |
|||
export * from './types'; |
|||
@ -0,0 +1,47 @@ |
|||
import type { IHasConcurrencyStamp, IHasExtraProperties } from '@abp/core'; |
|||
|
|||
interface FeatureDefinitionDto extends IHasExtraProperties { |
|||
allowedProviders: string[]; |
|||
defaultValue?: string; |
|||
description?: string; |
|||
displayName: string; |
|||
groupName: string; |
|||
isAvailableToHost: boolean; |
|||
isStatic: boolean; |
|||
isVisibleToClients: boolean; |
|||
name: string; |
|||
parentName?: string; |
|||
valueType: string; |
|||
} |
|||
|
|||
interface FeatureDefinitionGetListInput { |
|||
filter?: string; |
|||
groupName?: string; |
|||
} |
|||
|
|||
interface FeatureDefinitionCreateOrUpdateDto extends IHasExtraProperties { |
|||
allowedProviders: string[]; |
|||
defaultValue?: string; |
|||
description?: string; |
|||
displayName: string; |
|||
isAvailableToHost: boolean; |
|||
isVisibleToClients: boolean; |
|||
parentName?: string; |
|||
valueType: string; |
|||
} |
|||
|
|||
interface FeatureDefinitionUpdateDto |
|||
extends FeatureDefinitionCreateOrUpdateDto, |
|||
IHasConcurrencyStamp {} |
|||
interface FeatureDefinitionCreateDto |
|||
extends FeatureDefinitionCreateOrUpdateDto { |
|||
groupName: string; |
|||
name: string; |
|||
} |
|||
|
|||
export type { |
|||
FeatureDefinitionCreateDto, |
|||
FeatureDefinitionDto, |
|||
FeatureDefinitionGetListInput, |
|||
FeatureDefinitionUpdateDto, |
|||
}; |
|||
@ -0,0 +1,63 @@ |
|||
import type { Dictionary, NameValue } from '@abp/core'; |
|||
|
|||
interface FeatureProvider { |
|||
providerKey?: string; |
|||
providerName: string; |
|||
} |
|||
|
|||
interface IValueValidator { |
|||
[key: string]: any; |
|||
isValid(value?: any): boolean; |
|||
name: string; |
|||
properties: Dictionary<string, any>; |
|||
} |
|||
|
|||
interface IStringValueType { |
|||
[key: string]: any; |
|||
name: string; |
|||
properties: Dictionary<string, any>; |
|||
validator: IValueValidator; |
|||
} |
|||
|
|||
interface FeatureProviderDto { |
|||
key: string; |
|||
name: string; |
|||
} |
|||
|
|||
interface FeatureDto { |
|||
depth: number; |
|||
description?: string; |
|||
displayName: string; |
|||
name: string; |
|||
parentName?: string; |
|||
provider: FeatureProviderDto; |
|||
value?: any; |
|||
valueType: IStringValueType; |
|||
} |
|||
|
|||
interface FeatureGroupDto { |
|||
displayName: string; |
|||
features: FeatureDto[]; |
|||
name: string; |
|||
} |
|||
|
|||
interface GetFeatureListResultDto { |
|||
groups: FeatureGroupDto[]; |
|||
} |
|||
|
|||
type UpdateFeatureDto = NameValue<string>; |
|||
|
|||
interface UpdateFeaturesDto { |
|||
features: UpdateFeatureDto[]; |
|||
} |
|||
|
|||
export type { |
|||
FeatureDto, |
|||
FeatureGroupDto, |
|||
FeatureProvider, |
|||
GetFeatureListResultDto, |
|||
IStringValueType, |
|||
IValueValidator, |
|||
UpdateFeatureDto, |
|||
UpdateFeaturesDto, |
|||
}; |
|||
@ -0,0 +1,31 @@ |
|||
import type { IHasConcurrencyStamp, IHasExtraProperties } from '@abp/core'; |
|||
|
|||
interface FeatureGroupDefinitionDto extends IHasExtraProperties { |
|||
displayName: string; |
|||
isStatic: boolean; |
|||
name: string; |
|||
} |
|||
|
|||
interface FeatureGroupDefinitionGetListInput { |
|||
filter?: string; |
|||
} |
|||
|
|||
interface FeatureGroupDefinitionCreateOrUpdateDto extends IHasExtraProperties { |
|||
displayName: string; |
|||
} |
|||
|
|||
interface FeatureGroupDefinitionUpdateDto |
|||
extends FeatureGroupDefinitionCreateOrUpdateDto, |
|||
IHasConcurrencyStamp {} |
|||
|
|||
interface FeatureGroupDefinitionCreateDto |
|||
extends FeatureGroupDefinitionCreateOrUpdateDto { |
|||
name: string; |
|||
} |
|||
|
|||
export type { |
|||
FeatureGroupDefinitionCreateDto, |
|||
FeatureGroupDefinitionDto, |
|||
FeatureGroupDefinitionGetListInput, |
|||
FeatureGroupDefinitionUpdateDto, |
|||
}; |
|||
@ -0,0 +1,3 @@ |
|||
export * from './definitions'; |
|||
export * from './features'; |
|||
export * from './groups'; |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/tsconfig/web.json", |
|||
"include": ["src"], |
|||
"exclude": ["node_modules"] |
|||
} |
|||
@ -1 +1 @@ |
|||
export * from './useGdprRequestsApi'; |
|||
export { useGdprRequestsApi } from './useGdprRequestsApi'; |
|||
|
|||
@ -1,2 +1,2 @@ |
|||
export * from './useEmailMessagesApi'; |
|||
export * from './useSmsMessagesApi'; |
|||
export { useEmailMessagesApi } from './useEmailMessagesApi'; |
|||
export { useSmsMessagesApi } from './useSmsMessagesApi'; |
|||
|
|||
@ -1,2 +1,2 @@ |
|||
export * from './useEditionsApi'; |
|||
export * from './useTenantsApi'; |
|||
export { useEditionsApi } from './useEditionsApi'; |
|||
export { useTenantsApi } from './useTenantsApi'; |
|||
|
|||
@ -1,4 +1,5 @@ |
|||
export { default as LocalizableInput } from './localizable-input/LocalizableInput.vue'; |
|||
export { default as PropertyTable } from './properties/PropertyTable.vue'; |
|||
export * from './properties/types'; |
|||
export * from './string-value-type'; |
|||
export type * from './vxe-table'; |
|||
|
|||
@ -0,0 +1,765 @@ |
|||
<!-- eslint-disable vue/custom-event-name-casing --> |
|||
<script setup lang="ts"> |
|||
import type { LocalizableStringInfo } from '@abp/core'; |
|||
import type { RuleObject } from 'ant-design-vue/lib/form'; |
|||
import type { ColumnsType } from 'ant-design-vue/lib/table'; |
|||
|
|||
import type { StringValueTypeInstance } from './interface'; |
|||
import type { SelectionStringValueItem, StringValueType } from './valueType'; |
|||
|
|||
import { computed, reactive, ref, unref, watch } from 'vue'; |
|||
|
|||
import { createIconifyIcon } from '@vben/icons'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { |
|||
isNullOrWhiteSpace, |
|||
useLocalization, |
|||
useLocalizationSerializer, |
|||
} from '@abp/core'; |
|||
import { |
|||
Button, |
|||
Card, |
|||
Checkbox, |
|||
Col, |
|||
Form, |
|||
Input, |
|||
InputNumber, |
|||
Modal, |
|||
Row, |
|||
Select, |
|||
Table, |
|||
} from 'ant-design-vue'; |
|||
|
|||
import LocalizableInput from '../localizable-input/LocalizableInput.vue'; |
|||
import { |
|||
AlwaysValidValueValidator, |
|||
BooleanValueValidator, |
|||
NumericValueValidator, |
|||
StringValueValidator, |
|||
} from './validator'; |
|||
import { |
|||
FreeTextStringValueType, |
|||
SelectionStringValueType, |
|||
ToggleStringValueType, |
|||
valueTypeSerializer, |
|||
} from './valueType'; |
|||
|
|||
interface Props { |
|||
allowDelete: boolean; |
|||
allowEdit: boolean; |
|||
disabled: boolean; |
|||
value: string; |
|||
} |
|||
|
|||
const props = withDefaults(defineProps<Props>(), { |
|||
allowDelete: false, |
|||
allowEdit: false, |
|||
disabled: false, |
|||
value: '{}', |
|||
}); |
|||
const emits = defineEmits<{ |
|||
(event: 'update:value', data: string | undefined): void; |
|||
(event: 'change', data: string | undefined): void; |
|||
(event: 'change:valueType', data: string): void; |
|||
(event: 'change:validator', data: string): void; |
|||
(event: 'change:selection', data: SelectionStringValueItem[]): void; |
|||
}>(); |
|||
const FormItemRest = Form.ItemRest; |
|||
const FormItem = Form.Item; |
|||
const Option = Select.Option; |
|||
const DeleteOutlined = createIconifyIcon('ant-design:delete-outlined'); |
|||
const EditOutlined = createIconifyIcon('ant-design:edit-outlined'); |
|||
|
|||
const { Lr } = useLocalization(); |
|||
const { |
|||
deserialize, |
|||
serialize, |
|||
validate: validateLocalizer, |
|||
} = useLocalizationSerializer(); |
|||
interface Selection { |
|||
form: { |
|||
editFlag: boolean; |
|||
model: any; |
|||
rules?: Record<string, RuleObject>; |
|||
}; |
|||
modal: { |
|||
maskClosable?: boolean; |
|||
minHeight?: number; |
|||
onCancel?: (e: MouseEvent) => void; |
|||
onOk?: (e: MouseEvent) => void; |
|||
title?: string; |
|||
visible?: boolean; |
|||
width?: number; |
|||
}; |
|||
} |
|||
interface State { |
|||
selection: Selection; |
|||
value?: string; |
|||
valueType: StringValueType; |
|||
} |
|||
|
|||
const formRef = ref<any>(); |
|||
const state = reactive<State>({ |
|||
selection: { |
|||
form: { |
|||
editFlag: false, |
|||
model: {}, |
|||
rules: { |
|||
displayText: { |
|||
validator: (_rule, value) => { |
|||
if (!validateLocalizer(value)) { |
|||
return Promise.reject( |
|||
$t( |
|||
'component.value_type_nput.type.SELECTION.displayTextNotBeEmpty', |
|||
), |
|||
); |
|||
} |
|||
const items = (state.valueType as SelectionStringValueType) |
|||
.itemSource.items; |
|||
if (items.some((x) => serialize(x.displayText) === value)) { |
|||
return Promise.reject( |
|||
$t( |
|||
'component.value_type_nput.type.SELECTION.duplicateKeyOrValue', |
|||
), |
|||
); |
|||
} |
|||
return Promise.resolve(); |
|||
}, |
|||
}, |
|||
value: { |
|||
validator: (_rule, value) => { |
|||
const items = (state.valueType as SelectionStringValueType) |
|||
.itemSource.items; |
|||
if (items.some((x) => x.value === value)) { |
|||
return Promise.reject( |
|||
$t( |
|||
'component.value_type_nput.type.SELECTION.duplicateKeyOrValue', |
|||
), |
|||
); |
|||
} |
|||
return Promise.resolve(); |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
modal: { |
|||
maskClosable: false, |
|||
minHeight: 400, |
|||
onCancel: handleCancel, |
|||
onOk: handleSubmit, |
|||
title: $t('component.value_type_nput.type.SELECTION.modal.title'), |
|||
visible: false, |
|||
width: 600, |
|||
}, |
|||
}, |
|||
valueType: new FreeTextStringValueType(), |
|||
}); |
|||
const getTableColumns = computed(() => { |
|||
const columns: ColumnsType = [ |
|||
{ |
|||
align: 'left', |
|||
dataIndex: 'displayText', |
|||
fixed: 'left', |
|||
key: 'displayText', |
|||
title: $t('component.value_type_nput.type.SELECTION.displayText'), |
|||
width: 180, |
|||
}, |
|||
{ |
|||
align: 'left', |
|||
dataIndex: 'value', |
|||
fixed: 'left', |
|||
key: 'value', |
|||
title: $t('component.value_type_nput.type.SELECTION.value'), |
|||
width: 200, |
|||
}, |
|||
]; |
|||
if (!props.disabled) { |
|||
columns.push({ |
|||
align: 'center', |
|||
dataIndex: 'action', |
|||
fixed: 'right', |
|||
key: 'action', |
|||
title: $t('component.value_type_nput.type.SELECTION.actions.title'), |
|||
width: 220, |
|||
}); |
|||
} |
|||
return columns; |
|||
}); |
|||
watch( |
|||
() => props.value, |
|||
(value) => { |
|||
if (isNullOrWhiteSpace(value) || value === '{}') { |
|||
state.valueType = new FreeTextStringValueType(); |
|||
} else { |
|||
_formatValueType(value); |
|||
} |
|||
}, |
|||
{ |
|||
immediate: true, |
|||
}, |
|||
); |
|||
watch( |
|||
() => state.valueType, |
|||
(valueType) => { |
|||
const isSelection = valueType.name === 'SelectionStringValueType'; |
|||
if ( |
|||
isSelection && |
|||
(valueType as SelectionStringValueType).itemSource.items.length < 0 |
|||
) { |
|||
return; |
|||
} |
|||
state.value = valueTypeSerializer.serialize(valueType); |
|||
emits('change:valueType', state.valueType.name); |
|||
emits('change:validator', state.valueType.validator.name); |
|||
if (isSelection) { |
|||
emits( |
|||
'change:selection', |
|||
(valueType as SelectionStringValueType).itemSource.items, |
|||
); |
|||
} |
|||
}, |
|||
{ |
|||
deep: true, |
|||
immediate: false, |
|||
}, |
|||
); |
|||
watch( |
|||
() => state.value, |
|||
(value) => { |
|||
emits('change', value); |
|||
emits('update:value', value); |
|||
}, |
|||
); |
|||
const getDisplayName = (displayName: LocalizableStringInfo) => { |
|||
return Lr(displayName.resourceName, displayName.name); |
|||
}; |
|||
|
|||
function validate(value: any) { |
|||
if (state.valueType.name === 'SelectionStringValueType') { |
|||
const items = (state.valueType as SelectionStringValueType).itemSource |
|||
.items; |
|||
if (items.length === 0) { |
|||
return Promise.reject( |
|||
$t('component.value_type_nput.type.SELECTION.itemsNotBeEmpty'), |
|||
); |
|||
} |
|||
if (value && !items.some((item) => item.value === value)) { |
|||
return Promise.reject( |
|||
$t('component.value_type_nput.type.SELECTION.itemsNotFound'), |
|||
); |
|||
} |
|||
} |
|||
if (!state.valueType.validator.isValid(value)) { |
|||
return Promise.reject( |
|||
$t('component.value_type_nput.validator.isInvalidValue', [ |
|||
$t( |
|||
`component.value_type_nput.validator.${state.valueType.validator.name}.name`, |
|||
), |
|||
]), |
|||
); |
|||
} |
|||
return Promise.resolve(value); |
|||
} |
|||
|
|||
function _formatValueType(valueTypeString: string) { |
|||
try { |
|||
state.valueType = valueTypeSerializer.deserialize(valueTypeString); |
|||
} catch { |
|||
console.warn( |
|||
'Unable to serialize validator example, check that "valueType" value is valid', |
|||
); |
|||
} |
|||
} |
|||
|
|||
function handleValueTypeChange(type?: string) { |
|||
switch (type) { |
|||
case 'SELECTION': |
|||
case 'SelectionStringValueType': { |
|||
state.valueType = new SelectionStringValueType(); |
|||
break; |
|||
} |
|||
case 'TOGGLE': |
|||
case 'ToggleStringValueType': { |
|||
state.valueType = new ToggleStringValueType(); |
|||
break; |
|||
} |
|||
default: { |
|||
state.valueType = new FreeTextStringValueType(); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
function handleValidatorChange(validator?: string) { |
|||
switch (validator) { |
|||
case 'BOOLEAN': { |
|||
state.valueType.validator = new BooleanValueValidator(); |
|||
break; |
|||
} |
|||
case 'NULL': { |
|||
state.valueType.validator = new AlwaysValidValueValidator(); |
|||
break; |
|||
} |
|||
case 'NUMERIC': { |
|||
state.valueType.validator = new NumericValueValidator(); |
|||
break; |
|||
} |
|||
default: { |
|||
state.valueType.validator = new StringValueValidator(); |
|||
break; |
|||
} |
|||
} |
|||
emits('change:validator', state.valueType.validator.name); |
|||
} |
|||
|
|||
function handleAddNew() { |
|||
state.selection.form.model = {}; |
|||
state.selection.form.editFlag = false; |
|||
state.selection.modal.visible = true; |
|||
} |
|||
|
|||
function handleClean() { |
|||
const items = (state.valueType as SelectionStringValueType).itemSource.items; |
|||
items.length = 0; |
|||
emits('change:selection', items); |
|||
} |
|||
|
|||
function handleEdit(record: Record<string, any>) { |
|||
state.selection.form.model = { |
|||
displayText: serialize(record.displayText), |
|||
value: record.value, |
|||
}; |
|||
state.selection.form.editFlag = true; |
|||
state.selection.modal.visible = true; |
|||
} |
|||
|
|||
function handleCancel() { |
|||
state.selection.form.model = {}; |
|||
state.selection.form.editFlag = false; |
|||
state.selection.modal.visible = false; |
|||
} |
|||
|
|||
function handleSubmit() { |
|||
const form = unref(formRef); |
|||
form?.validate().then(() => { |
|||
const displayText = state.selection.form.model.displayText; |
|||
const items = (state.valueType as SelectionStringValueType).itemSource |
|||
.items; |
|||
if (state.selection.form.editFlag) { |
|||
const findIndex = items.findIndex( |
|||
(x) => serialize(x.displayText) === displayText, |
|||
); |
|||
items[findIndex] && |
|||
(items[findIndex].value = state.selection.form.model.value); |
|||
} else { |
|||
items.push({ |
|||
displayText: deserialize(displayText), |
|||
value: state.selection.form.model.value, |
|||
}); |
|||
} |
|||
form.resetFields(); |
|||
state.selection.modal.visible = false; |
|||
emits('change:selection', items); |
|||
}); |
|||
} |
|||
|
|||
function handleDelete(record: Record<string, any>) { |
|||
const displayText = serialize(record.displayText); |
|||
const items = (state.valueType as SelectionStringValueType).itemSource.items; |
|||
const findIndex = items.findIndex( |
|||
(x) => serialize(x.displayText) === displayText, |
|||
); |
|||
items.splice(findIndex, 1); |
|||
emits('change:selection', items); |
|||
} |
|||
|
|||
defineExpose<StringValueTypeInstance>({ |
|||
validate, |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="container"> |
|||
<FormItemRest> |
|||
<Card> |
|||
<template #title> |
|||
<div class="type"> |
|||
<Row> |
|||
<Col :span="11"> |
|||
<span>{{ $t('component.value_type_nput.type.name') }}</span> |
|||
</Col> |
|||
<Col :span="2"> |
|||
<div class="w-full"></div> |
|||
</Col> |
|||
<Col :span="11"> |
|||
<span>{{ |
|||
$t('component.value_type_nput.validator.name') |
|||
}}</span> |
|||
</Col> |
|||
</Row> |
|||
<Row> |
|||
<Col :span="11"> |
|||
<Select |
|||
:disabled="props.disabled" |
|||
:value="state.valueType.name" |
|||
@change="(value) => handleValueTypeChange(value?.toString())" |
|||
> |
|||
<Option value="FreeTextStringValueType"> |
|||
{{ $t('component.value_type_nput.type.FREE_TEXT.name') }} |
|||
</Option> |
|||
<Option value="ToggleStringValueType"> |
|||
{{ $t('component.value_type_nput.type.TOGGLE.name') }} |
|||
</Option> |
|||
<Option value="SelectionStringValueType"> |
|||
{{ $t('component.value_type_nput.type.SELECTION.name') }} |
|||
</Option> |
|||
</Select> |
|||
</Col> |
|||
<Col :span="2"> |
|||
<div class="w-full"></div> |
|||
</Col> |
|||
<Col :span="11"> |
|||
<Select |
|||
:disabled="props.disabled" |
|||
:value="state.valueType.validator.name" |
|||
@change="(value) => handleValidatorChange(value?.toString())" |
|||
> |
|||
<Option value="NULL"> |
|||
{{ $t('component.value_type_nput.validator.NULL.name') }} |
|||
</Option> |
|||
<Option |
|||
value="BOOLEAN" |
|||
:disabled="state.valueType.name !== 'ToggleStringValueType'" |
|||
> |
|||
{{ $t('component.value_type_nput.validator.BOOLEAN.name') }} |
|||
</Option> |
|||
<Option |
|||
value="NUMERIC" |
|||
:disabled=" |
|||
state.valueType.name !== 'FreeTextStringValueType' |
|||
" |
|||
> |
|||
{{ $t('component.value_type_nput.validator.NUMERIC.name') }} |
|||
</Option> |
|||
<Option |
|||
value="STRING" |
|||
:disabled=" |
|||
state.valueType.name !== 'FreeTextStringValueType' |
|||
" |
|||
> |
|||
{{ $t('component.value_type_nput.validator.STRING.name') }} |
|||
</Option> |
|||
</Select> |
|||
</Col> |
|||
</Row> |
|||
</div> |
|||
</template> |
|||
<div class="wrap"> |
|||
<Row> |
|||
<Col :span="24"> |
|||
<div v-if="state.valueType.name === 'FreeTextStringValueType'"> |
|||
<div |
|||
v-if="state.valueType.validator.name === 'NUMERIC'" |
|||
class="numeric" |
|||
> |
|||
<Row> |
|||
<Col :span="11"> |
|||
<span>{{ |
|||
$t( |
|||
'component.value_type_nput.validator.NUMERIC.minValue', |
|||
) |
|||
}}</span> |
|||
</Col> |
|||
<Col :span="2"> |
|||
<div class="w-full"></div> |
|||
</Col> |
|||
<Col :span="11"> |
|||
<span>{{ |
|||
$t( |
|||
'component.value_type_nput.validator.NUMERIC.maxValue', |
|||
) |
|||
}}</span> |
|||
</Col> |
|||
</Row> |
|||
<Row> |
|||
<Col :span="11"> |
|||
<InputNumber |
|||
:disabled="props.disabled" |
|||
class="w-full" |
|||
v-model:value=" |
|||
(state.valueType.validator as NumericValueValidator) |
|||
.minValue |
|||
" |
|||
/> |
|||
</Col> |
|||
<Col :span="2"> |
|||
<div class="w-full"></div> |
|||
</Col> |
|||
<Col :span="11"> |
|||
<InputNumber |
|||
:disabled="props.disabled" |
|||
class="w-full" |
|||
v-model:value=" |
|||
(state.valueType.validator as NumericValueValidator) |
|||
.maxValue |
|||
" |
|||
/> |
|||
</Col> |
|||
</Row> |
|||
</div> |
|||
<div |
|||
v-else-if="state.valueType.validator.name === 'STRING'" |
|||
class="string" |
|||
> |
|||
<Row style="margin-top: 10px"> |
|||
<Col :span="24"> |
|||
<Checkbox |
|||
:disabled="props.disabled" |
|||
class="w-full" |
|||
v-model:checked=" |
|||
(state.valueType.validator as StringValueValidator) |
|||
.allowNull |
|||
" |
|||
> |
|||
{{ |
|||
$t( |
|||
'component.value_type_nput.validator.STRING.allowNull', |
|||
) |
|||
}} |
|||
</Checkbox> |
|||
</Col> |
|||
</Row> |
|||
<Row style="margin-top: 10px"> |
|||
<Col :span="24"> |
|||
<span>{{ |
|||
$t( |
|||
'component.value_type_nput.validator.STRING.regularExpression', |
|||
) |
|||
}}</span> |
|||
</Col> |
|||
</Row> |
|||
<Row> |
|||
<Col :span="24"> |
|||
<Input |
|||
:disabled="props.disabled" |
|||
class="w-full" |
|||
v-model:value=" |
|||
(state.valueType.validator as StringValueValidator) |
|||
.regularExpression |
|||
" |
|||
/> |
|||
</Col> |
|||
</Row> |
|||
<Row style="margin-top: 10px"> |
|||
<Col :span="11"> |
|||
<span>{{ |
|||
$t( |
|||
'component.value_type_nput.validator.STRING.minLength', |
|||
) |
|||
}}</span> |
|||
</Col> |
|||
<Col :span="2"> |
|||
<div class="w-full"></div> |
|||
</Col> |
|||
<Col :span="11"> |
|||
<span>{{ |
|||
$t( |
|||
'component.value_type_nput.validator.STRING.maxLength', |
|||
) |
|||
}}</span> |
|||
</Col> |
|||
</Row> |
|||
<Row> |
|||
<Col :span="11"> |
|||
<InputNumber |
|||
:disabled="props.disabled" |
|||
class="w-full" |
|||
v-model:value=" |
|||
(state.valueType.validator as StringValueValidator) |
|||
.minLength |
|||
" |
|||
/> |
|||
</Col> |
|||
<Col :span="2"> |
|||
<div class="w-full"></div> |
|||
</Col> |
|||
<Col :span="11"> |
|||
<InputNumber |
|||
:disabled="props.disabled" |
|||
class="w-full" |
|||
v-model:value=" |
|||
(state.valueType.validator as StringValueValidator) |
|||
.maxLength |
|||
" |
|||
/> |
|||
</Col> |
|||
</Row> |
|||
</div> |
|||
</div> |
|||
<div |
|||
v-else-if="state.valueType.name === 'SelectionStringValueType'" |
|||
> |
|||
<Card class="selection"> |
|||
<template #title> |
|||
<Row> |
|||
<Col :span="12"> |
|||
<div |
|||
class="valid" |
|||
v-if=" |
|||
(state.valueType as SelectionStringValueType) |
|||
.itemSource.items.length <= 0 |
|||
" |
|||
> |
|||
<span>{{ |
|||
$t( |
|||
'component.value_type_nput.type.SELECTION.itemsNotBeEmpty', |
|||
) |
|||
}}</span> |
|||
</div> |
|||
</Col> |
|||
<Col :span="12"> |
|||
<div class="toolbar" v-if="!props.disabled"> |
|||
<Button type="primary" @click="handleAddNew"> |
|||
{{ |
|||
$t( |
|||
'component.value_type_nput.type.SELECTION.actions.create', |
|||
) |
|||
}} |
|||
</Button> |
|||
<Button danger @click="handleClean"> |
|||
{{ |
|||
$t( |
|||
'component.value_type_nput.type.SELECTION.actions.clean', |
|||
) |
|||
}} |
|||
</Button> |
|||
</div> |
|||
</Col> |
|||
</Row> |
|||
</template> |
|||
<Table |
|||
:columns="getTableColumns" |
|||
:data-source=" |
|||
(state.valueType as SelectionStringValueType).itemSource |
|||
.items |
|||
" |
|||
> |
|||
<template #bodyCell="{ column, record }"> |
|||
<template v-if="column.key === 'displayText'"> |
|||
<span>{{ getDisplayName(record.displayText) }}</span> |
|||
</template> |
|||
<template v-else-if="column.key === 'action'"> |
|||
<div class="flex flex-row items-center gap-1"> |
|||
<Button |
|||
v-if="props.allowEdit" |
|||
type="link" |
|||
@click="() => handleEdit(record)" |
|||
> |
|||
<template #icon> |
|||
<EditOutlined /> |
|||
</template> |
|||
{{ |
|||
$t( |
|||
'component.value_type_nput.type.SELECTION.actions.update', |
|||
) |
|||
}} |
|||
</Button> |
|||
<Button |
|||
v-if="props.allowDelete" |
|||
type="link" |
|||
@click="() => handleDelete(record)" |
|||
danger |
|||
> |
|||
<template #icon> |
|||
<DeleteOutlined /> |
|||
</template> |
|||
{{ |
|||
$t( |
|||
'component.value_type_nput.type.SELECTION.actions.delete', |
|||
) |
|||
}} |
|||
</Button> |
|||
</div> |
|||
</template> |
|||
</template> |
|||
</Table> |
|||
</Card> |
|||
</div> |
|||
</Col> |
|||
</Row> |
|||
</div> |
|||
</Card> |
|||
</FormItemRest> |
|||
<Modal class="modal" v-bind="state.selection.modal"> |
|||
<Form |
|||
ref="formRef" |
|||
class="form" |
|||
v-bind="state.selection.form" |
|||
:label-col="{ span: 6 }" |
|||
:wrapper-col="{ span: 18 }" |
|||
> |
|||
<FormItem |
|||
name="displayText" |
|||
required |
|||
:label="$t('component.value_type_nput.type.SELECTION.displayText')" |
|||
> |
|||
<LocalizableInput |
|||
:disabled="props.disabled || state.selection.form.editFlag" |
|||
v-model:value="state.selection.form.model.displayText" |
|||
/> |
|||
</FormItem> |
|||
<FormItem |
|||
name="value" |
|||
required |
|||
:label="$t('component.value_type_nput.type.SELECTION.value')" |
|||
> |
|||
<Input |
|||
:disabled="props.disabled" |
|||
v-model:value="state.selection.form.model.value" |
|||
/> |
|||
</FormItem> |
|||
</Form> |
|||
</Modal> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" scoped> |
|||
.container { |
|||
.type { |
|||
width: 100% !important; |
|||
min-height: 70px; |
|||
} |
|||
|
|||
.wrap { |
|||
width: 100% !important; |
|||
|
|||
.selection { |
|||
.valid { |
|||
color: red; |
|||
font-size: 14; |
|||
} |
|||
|
|||
.toolbar { |
|||
flex: 1; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: flex-end; |
|||
margin-bottom: 8px; |
|||
|
|||
> * { |
|||
margin-right: 8px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.modal { |
|||
height: 500px; |
|||
|
|||
.form { |
|||
margin: 10px; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,4 @@ |
|||
export * from './interface'; |
|||
export { default as StringValueTypeInput } from './StringValueTypeInput.vue'; |
|||
export * from './validator'; |
|||
export * from './valueType'; |
|||
@ -0,0 +1,3 @@ |
|||
export interface StringValueTypeInstance { |
|||
validate(value: any): Promise<any>; |
|||
} |
|||
@ -0,0 +1,143 @@ |
|||
import type { Dictionary } from '@abp/core'; |
|||
|
|||
import { isBoolean, isNumber } from '@vben/utils'; |
|||
|
|||
import { isNullOrUnDef, isNullOrWhiteSpace } from '@abp/core'; |
|||
|
|||
export interface ValueValidator { |
|||
isValid(value?: any): boolean; |
|||
name: string; |
|||
|
|||
properties: Dictionary<string, any>; |
|||
} |
|||
|
|||
export class AlwaysValidValueValidator implements ValueValidator { |
|||
name = 'NULL'; |
|||
properties: Dictionary<string, any>; |
|||
constructor() { |
|||
this.properties = {}; |
|||
} |
|||
isValid(_value?: any): boolean { |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
export class BooleanValueValidator implements ValueValidator { |
|||
name = 'BOOLEAN'; |
|||
properties: Dictionary<string, any>; |
|||
constructor() { |
|||
this.properties = {}; |
|||
} |
|||
isValid(value?: any): boolean { |
|||
if (isNullOrUnDef(value)) return true; |
|||
if (isBoolean(value)) return true; |
|||
const bolString = String(value).toLowerCase(); |
|||
if (bolString === 'true' || bolString === 'false') return true; |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
export class NumericValueValidator implements ValueValidator { |
|||
name = 'NUMERIC'; |
|||
properties: Dictionary<string, any>; |
|||
get maxValue(): number | undefined { |
|||
return Number(this.properties.MaxValue); |
|||
} |
|||
|
|||
set maxValue(value: number) { |
|||
this.properties.MaxValue = value; |
|||
} |
|||
|
|||
get minValue(): number | undefined { |
|||
return Number(this.properties.MinValue); |
|||
} |
|||
|
|||
set minValue(value: number) { |
|||
this.properties.MinValue = value; |
|||
} |
|||
|
|||
constructor() { |
|||
this.properties = {}; |
|||
} |
|||
|
|||
_isValidInternal(value: number): boolean { |
|||
if (this.minValue && value < this.minValue) return false; |
|||
if (this.maxValue && value > this.maxValue) return false; |
|||
return true; |
|||
} |
|||
|
|||
isValid(value?: any): boolean { |
|||
if (isNullOrUnDef(value)) return true; |
|||
if (isNumber(value)) return this._isValidInternal(value); |
|||
const numString = String(value); |
|||
if (!isNullOrUnDef(numString)) { |
|||
const num = Number(numString); |
|||
if (num) return this._isValidInternal(num); |
|||
} |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
export class StringValueValidator implements ValueValidator { |
|||
name = 'STRING'; |
|||
properties: Dictionary<string, any>; |
|||
get allowNull(): boolean { |
|||
return ( |
|||
String(this.properties.AllowNull ?? 'true')?.toLowerCase() === 'true' |
|||
); |
|||
} |
|||
|
|||
set allowNull(value: boolean) { |
|||
this.properties.AllowNull = value; |
|||
} |
|||
|
|||
get maxLength(): number | undefined { |
|||
return Number(this.properties.MaxLength); |
|||
} |
|||
|
|||
set maxLength(value: number) { |
|||
this.properties.MaxLength = value; |
|||
} |
|||
|
|||
get minLength(): number | undefined { |
|||
return Number(this.properties.MinLength); |
|||
} |
|||
|
|||
set minLength(value: number) { |
|||
this.properties.MinLength = value; |
|||
} |
|||
|
|||
get regularExpression(): string { |
|||
return String(this.properties.RegularExpression ?? ''); |
|||
} |
|||
|
|||
set regularExpression(value: string) { |
|||
this.properties.RegularExpression = value; |
|||
} |
|||
|
|||
constructor() { |
|||
this.properties = {}; |
|||
} |
|||
|
|||
isValid(value?: any): boolean { |
|||
if (!this.allowNull && isNullOrUnDef(value)) return false; |
|||
const valueString = String(value); |
|||
if (!this.allowNull && isNullOrWhiteSpace(valueString.trim())) return false; |
|||
if ( |
|||
this.minLength && |
|||
this.minLength > 0 && |
|||
valueString.length < this.minLength |
|||
) |
|||
return false; |
|||
if ( |
|||
this.maxLength && |
|||
this.maxLength > 0 && |
|||
valueString.length > this.maxLength |
|||
) |
|||
return false; |
|||
if (!isNullOrWhiteSpace(this.regularExpression)) { |
|||
return new RegExp(this.regularExpression).test(valueString); |
|||
} |
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
import type { Dictionary, LocalizableStringInfo } from '@abp/core'; |
|||
|
|||
import type { ValueValidator } from './validator'; |
|||
|
|||
import { |
|||
AlwaysValidValueValidator, |
|||
BooleanValueValidator, |
|||
NumericValueValidator, |
|||
StringValueValidator, |
|||
} from './validator'; |
|||
|
|||
export interface StringValueType { |
|||
name: string; |
|||
properties: Dictionary<string, any>; |
|||
validator: ValueValidator; |
|||
} |
|||
|
|||
export interface SelectionStringValueItem { |
|||
displayText: LocalizableStringInfo; |
|||
value: string; |
|||
} |
|||
|
|||
export interface SelectionStringValueItemSource { |
|||
items: SelectionStringValueItem[]; |
|||
} |
|||
|
|||
export class FreeTextStringValueType implements StringValueType { |
|||
name = 'FreeTextStringValueType'; |
|||
properties: Dictionary<string, any>; |
|||
validator: ValueValidator; |
|||
constructor(validator?: ValueValidator) { |
|||
this.properties = {}; |
|||
this.validator = validator ?? new AlwaysValidValueValidator(); |
|||
} |
|||
} |
|||
|
|||
export class ToggleStringValueType implements StringValueType { |
|||
name = 'ToggleStringValueType'; |
|||
properties: Dictionary<string, any>; |
|||
validator: ValueValidator; |
|||
constructor(validator?: ValueValidator) { |
|||
this.properties = {}; |
|||
this.validator = validator ?? new BooleanValueValidator(); |
|||
} |
|||
} |
|||
|
|||
export class SelectionStringValueType implements StringValueType { |
|||
itemSource: SelectionStringValueItemSource; |
|||
name = 'SelectionStringValueType'; |
|||
properties: Dictionary<string, any>; |
|||
validator: ValueValidator; |
|||
constructor(validator?: ValueValidator) { |
|||
this.properties = {}; |
|||
this.itemSource = { |
|||
items: [], |
|||
}; |
|||
this.validator = validator ?? new AlwaysValidValueValidator(); |
|||
} |
|||
} |
|||
|
|||
class StringValueTypeSerializer { |
|||
_deserializeValidator(validator: any): ValueValidator { |
|||
let convertValidator: ValueValidator = new AlwaysValidValueValidator(); |
|||
if (validator.name) { |
|||
switch (validator.name) { |
|||
case 'BOOLEAN': { |
|||
convertValidator = new BooleanValueValidator(); |
|||
break; |
|||
} |
|||
case 'NULL': { |
|||
convertValidator = new AlwaysValidValueValidator(); |
|||
break; |
|||
} |
|||
case 'NUMERIC': { |
|||
convertValidator = new NumericValueValidator(); |
|||
break; |
|||
} |
|||
case 'STRING': { |
|||
convertValidator = new StringValueValidator(); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
convertValidator.properties = validator.properties; |
|||
return convertValidator; |
|||
} |
|||
|
|||
deserialize(value: string): StringValueType { |
|||
let valueType: StringValueType; |
|||
const valueTypeObj = JSON.parse(value); |
|||
switch (valueTypeObj.name) { |
|||
case 'SELECTION': |
|||
case 'SelectionStringValueType': { |
|||
valueType = new SelectionStringValueType(); |
|||
(valueType as SelectionStringValueType).itemSource = |
|||
valueTypeObj.itemSource; |
|||
break; |
|||
} |
|||
case 'TOGGLE': |
|||
case 'ToggleStringValueType': { |
|||
valueType = new ToggleStringValueType(); |
|||
break; |
|||
} |
|||
default: { |
|||
valueType = new FreeTextStringValueType(); |
|||
break; |
|||
} |
|||
} |
|||
valueType.properties = valueTypeObj.properties; |
|||
valueType.validator = this._deserializeValidator(valueTypeObj.validator); |
|||
return valueType; |
|||
} |
|||
|
|||
serialize(value: StringValueType): string { |
|||
const valueTypeString = JSON.stringify(value); |
|||
return valueTypeString; |
|||
} |
|||
} |
|||
|
|||
export const valueTypeSerializer = new StringValueTypeSerializer(); |
|||
Loading…
Reference in new issue