Browse Source

feat(vben5): add @abp/features package

pull/1134/head
colin 1 year ago
parent
commit
4f6c5a1139
  1. 39
      apps/vben5/packages/@abp/features/package.json
  2. 1
      apps/vben5/packages/@abp/features/src/api/index.ts
  3. 62
      apps/vben5/packages/@abp/features/src/api/useFeaturesApi.ts
  4. 258
      apps/vben5/packages/@abp/features/src/components/features/FeatureModal.vue
  5. 1
      apps/vben5/packages/@abp/features/src/components/index.ts
  6. 1
      apps/vben5/packages/@abp/features/src/constants/index.ts
  7. 0
      apps/vben5/packages/@abp/features/src/constants/permissions.ts
  8. 4
      apps/vben5/packages/@abp/features/src/index.ts
  9. 63
      apps/vben5/packages/@abp/features/src/types/features.ts
  10. 1
      apps/vben5/packages/@abp/features/src/types/index.ts
  11. 6
      apps/vben5/packages/@abp/features/tsconfig.json
  12. 1
      apps/vben5/packages/@abp/saas/package.json
  13. 4
      apps/vben5/packages/@abp/saas/src/api/index.ts
  14. 21
      apps/vben5/packages/@abp/saas/src/components/tenants/TenantTable.vue
  15. 1
      apps/vben5/packages/@abp/settings/package.json
  16. 23
      apps/vben5/packages/@abp/settings/src/components/settings/SettingForm.vue
  17. 23
      apps/vben5/packages/@abp/settings/src/components/settings/SystemSetting.vue

39
apps/vben5/packages/@abp/features/package.json

@ -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/auditing": "workspace:*",
"@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:*",
"ant-design-vue": "catalog:",
"dayjs": "catalog:",
"vue": "catalog:*",
"vxe-table": "catalog:"
},
"devDependencies": {}
}

1
apps/vben5/packages/@abp/features/src/api/index.ts

@ -0,0 +1 @@
export { useFeaturesApi } from './useFeaturesApi';

62
apps/vben5/packages/@abp/features/src/api/useFeaturesApi.ts

@ -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,
};
}

258
apps/vben5/packages/@abp/features/src/components/features/FeatureModal.vue

@ -0,0 +1,258 @@
<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) => {
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.MaxValue),
minimum: Number(validator.properties.MinValue),
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>

1
apps/vben5/packages/@abp/features/src/components/index.ts

@ -0,0 +1 @@
export { default as FeatureModal } from './features/FeatureModal.vue';

1
apps/vben5/packages/@abp/features/src/constants/index.ts

@ -0,0 +1 @@
export * from './permissions';

0
apps/vben5/packages/@abp/features/src/constants/permissions.ts

4
apps/vben5/packages/@abp/features/src/index.ts

@ -0,0 +1,4 @@
export * from './api';
export * from './components';
export * from './constants';
export * from './types';

63
apps/vben5/packages/@abp/features/src/types/features.ts

@ -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,
};

1
apps/vben5/packages/@abp/features/src/types/index.ts

@ -0,0 +1 @@
export * from './features';

6
apps/vben5/packages/@abp/features/tsconfig.json

@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"include": ["src"],
"exclude": ["node_modules"]
}

1
apps/vben5/packages/@abp/saas/package.json

@ -22,6 +22,7 @@
"dependencies": { "dependencies": {
"@abp/auditing": "workspace:*", "@abp/auditing": "workspace:*",
"@abp/core": "workspace:*", "@abp/core": "workspace:*",
"@abp/features": "workspace:*",
"@abp/request": "workspace:*", "@abp/request": "workspace:*",
"@abp/ui": "workspace:*", "@abp/ui": "workspace:*",
"@ant-design/icons-vue": "catalog:", "@ant-design/icons-vue": "catalog:",

4
apps/vben5/packages/@abp/saas/src/api/index.ts

@ -1,2 +1,2 @@
export * from './useEditionsApi'; export { useEditionsApi } from './useEditionsApi';
export * from './useTenantsApi'; export { useTenantsApi } from './useTenantsApi';

21
apps/vben5/packages/@abp/saas/src/components/tenants/TenantTable.vue

@ -13,6 +13,7 @@ import { $t } from '@vben/locales';
import { AuditLogPermissions, EntityChangeDrawer } from '@abp/auditing'; import { AuditLogPermissions, EntityChangeDrawer } from '@abp/auditing';
import { useFeatures } from '@abp/core'; import { useFeatures } from '@abp/core';
import { FeatureModal } from '@abp/features';
import { useVbenVxeGrid } from '@abp/ui'; import { useVbenVxeGrid } from '@abp/ui';
import { import {
DeleteOutlined, DeleteOutlined,
@ -33,6 +34,7 @@ const CheckIcon = createIconifyIcon('ant-design:check-outlined');
const CloseIcon = createIconifyIcon('ant-design:close-outlined'); const CloseIcon = createIconifyIcon('ant-design:close-outlined');
const AuditLogIcon = createIconifyIcon('fluent-mdl2:compliance-audit'); const AuditLogIcon = createIconifyIcon('fluent-mdl2:compliance-audit');
const ConnectionIcon = createIconifyIcon('mdi:connection'); const ConnectionIcon = createIconifyIcon('mdi:connection');
const FeatureIcon = createIconifyIcon('pajamas:feature-flag');
const { isEnabled } = useFeatures(); const { isEnabled } = useFeatures();
const { hasAccessByCodes } = useAccess(); const { hasAccessByCodes } = useAccess();
@ -131,6 +133,9 @@ const [TenantConnectionStringsModal, connectionStringsModalApi] = useVbenModal({
const [TenantChangeDrawer, entityChangeDrawerApi] = useVbenDrawer({ const [TenantChangeDrawer, entityChangeDrawerApi] = useVbenDrawer({
connectedComponent: EntityChangeDrawer, connectedComponent: EntityChangeDrawer,
}); });
const [TenantFeatureModal, tenantFeatureModalApi] = useVbenModal({
connectedComponent: FeatureModal,
});
const [Grid, { query }] = useVbenVxeGrid({ const [Grid, { query }] = useVbenVxeGrid({
formOptions, formOptions,
gridEvents, gridEvents,
@ -179,6 +184,14 @@ const onMenuClick = (row: TenantDto, info: MenuInfo) => {
entityChangeDrawerApi.open(); entityChangeDrawerApi.open();
break; break;
} }
case 'features': {
tenantFeatureModalApi.setData({
displayName: row.name,
providerKey: row.id,
providerName: 'T',
});
tenantFeatureModalApi.open();
}
} }
}; };
</script> </script>
@ -233,6 +246,13 @@ const onMenuClick = (row: TenantDto, info: MenuInfo) => {
> >
{{ $t('AbpSaas.ConnectionStrings') }} {{ $t('AbpSaas.ConnectionStrings') }}
</MenuItem> </MenuItem>
<MenuItem
v-if="hasAccessByCodes([TenantsPermissions.ManageFeatures])"
key="features"
:icon="h(FeatureIcon)"
>
{{ $t('AbpSaas.ManageFeatures') }}
</MenuItem>
<MenuItem <MenuItem
v-if=" v-if="
isEnabled('AbpAuditing.Logging.AuditLog') && isEnabled('AbpAuditing.Logging.AuditLog') &&
@ -252,6 +272,7 @@ const onMenuClick = (row: TenantDto, info: MenuInfo) => {
</Grid> </Grid>
<TenantModal @change="() => query()" /> <TenantModal @change="() => query()" />
<TenantConnectionStringsModal /> <TenantConnectionStringsModal />
<TenantFeatureModal />
<TenantChangeDrawer /> <TenantChangeDrawer />
</template> </template>

1
apps/vben5/packages/@abp/settings/package.json

@ -21,6 +21,7 @@
}, },
"dependencies": { "dependencies": {
"@abp/core": "workspace:*", "@abp/core": "workspace:*",
"@abp/features": "workspace:*",
"@abp/request": "workspace:*", "@abp/request": "workspace:*",
"@abp/ui": "workspace:*", "@abp/ui": "workspace:*",
"@ant-design/icons-vue": "catalog:", "@ant-design/icons-vue": "catalog:",

23
apps/vben5/packages/@abp/settings/src/components/settings/SettingForm.vue

@ -110,16 +110,19 @@ onMounted(onGet);
<template> <template>
<Card :title="$t('AbpSettingManagement.Settings')"> <Card :title="$t('AbpSettingManagement.Settings')">
<template #extra> <template #extra>
<Button <div class="flex flex-row gap-1">
v-if="settingsUpdateInput.settings.length > 0" <slot name="toolbar"></slot>
:loading="submiting" <Button
class="w-[100px]" v-if="settingsUpdateInput.settings.length > 0"
post-icon="ant-design:setting-outlined" :loading="submiting"
type="primary" class="w-[100px]"
@click="onSubmit" post-icon="ant-design:setting-outlined"
> type="primary"
{{ $t('AbpUi.Submit') }} @click="onSubmit"
</Button> >
{{ $t('AbpUi.Submit') }}
</Button>
</div>
</template> </template>
<Form :label-col="{ span: 5 }" :wrapper-col="{ span: 15 }"> <Form :label-col="{ span: 5 }" :wrapper-col="{ span: 15 }">
<Tabs v-model="activeTab"> <Tabs v-model="activeTab">

23
apps/vben5/packages/@abp/settings/src/components/settings/SystemSetting.vue

@ -3,9 +3,11 @@ import type { SettingsUpdateInput } from '../../types';
import { ref } from 'vue'; import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { isEmail, useAbpStore } from '@abp/core'; import { isEmail, useAbpStore } from '@abp/core';
import { FeatureModal } from '@abp/features';
import { Button, Form, InputSearch, message, Modal } from 'ant-design-vue'; import { Button, Form, InputSearch, message, Modal } from 'ant-design-vue';
import { import {
@ -24,6 +26,9 @@ defineOptions({
const FormItem = Form.Item; const FormItem = Form.Item;
const abpStore = useAbpStore(); const abpStore = useAbpStore();
const [HostFeatureModal, featureModalApi] = useVbenModal({
connectedComponent: FeatureModal,
});
const sending = ref(false); const sending = ref(false);
@ -61,10 +66,27 @@ async function onSendMail(email: string) {
sending.value = false; sending.value = false;
} }
} }
function onFeatureManage() {
featureModalApi.setData({
providerName: 'T',
});
featureModalApi.open();
}
</script> </script>
<template> <template>
<SettingForm :get-api="onGet" :submit-api="onSubmit"> <SettingForm :get-api="onGet" :submit-api="onSubmit">
<template #toolbar>
<Button
ghost
post-icon="ant-design:setting-outlined"
type="primary"
@click="onFeatureManage"
>
{{ $t('AbpFeatureManagement.ManageHostFeatures') }}
</Button>
</template>
<template #send-test-email="{ detail }"> <template #send-test-email="{ detail }">
<FormItem <FormItem
:extra="detail.description" :extra="detail.description"
@ -86,6 +108,7 @@ async function onSendMail(email: string) {
</FormItem> </FormItem>
</template> </template>
</SettingForm> </SettingForm>
<HostFeatureModal />
</template> </template>
<style scoped></style> <style scoped></style>

Loading…
Cancel
Save