Browse Source

upgrade(abp): upgrade abp framework to 7.4.0

pull/891/head
colin 2 years ago
parent
commit
1e107d5027
  1. 4
      apps/vue/.env.development
  2. 40
      apps/vue/src/api/feature-management/definitions/features/index.ts
  3. 36
      apps/vue/src/api/feature-management/definitions/features/model/index.ts
  4. 40
      apps/vue/src/api/feature-management/definitions/groups/index.ts
  5. 19
      apps/vue/src/api/feature-management/definitions/groups/model/index.ts
  6. 20
      apps/vue/src/api/feature-management/features/index.ts
  7. 44
      apps/vue/src/api/feature-management/features/model/index.ts
  8. 26
      apps/vue/src/api/feature/feature.ts
  9. 34
      apps/vue/src/api/feature/model/featureModel.ts
  10. 2
      apps/vue/src/api/messages/notifications.ts
  11. 3
      apps/vue/src/api/oss-management/oss.ts
  12. 41
      apps/vue/src/api/settings-management/definitions/index.ts
  13. 32
      apps/vue/src/api/settings-management/definitions/model/index.ts
  14. 60
      apps/vue/src/api/settings-management/settings/index.ts
  15. 0
      apps/vue/src/api/settings-management/settings/model/index.ts
  16. 60
      apps/vue/src/api/settings/settings.ts
  17. 6
      apps/vue/src/api/text-templating/contents/index.ts
  18. 2
      apps/vue/src/api/text-templating/definitions/index.ts
  19. 5
      apps/vue/src/api/text-templating/definitions/model/index.ts
  20. 41
      apps/vue/src/api/webhooks/definitions/groups/index.ts
  21. 19
      apps/vue/src/api/webhooks/definitions/groups/model/index.ts
  22. 41
      apps/vue/src/api/webhooks/definitions/webhooks/index.ts
  23. 28
      apps/vue/src/api/webhooks/definitions/webhooks/model/index.ts
  24. 32
      apps/vue/src/api/webhooks/model/sendAttemptsModel.ts
  25. 45
      apps/vue/src/api/webhooks/model/subscriptionsModel.ts
  26. 73
      apps/vue/src/api/webhooks/send-attempts.ts
  27. 46
      apps/vue/src/api/webhooks/send-attempts/index.ts
  28. 40
      apps/vue/src/api/webhooks/send-attempts/model/index.ts
  29. 85
      apps/vue/src/api/webhooks/subscriptions.ts
  30. 55
      apps/vue/src/api/webhooks/subscriptions/index.ts
  31. 49
      apps/vue/src/api/webhooks/subscriptions/model/index.ts
  32. 271
      apps/vue/src/components/Abp/ExtraPropertyDictionary/ExtraPropertyDictionary.vue
  33. 3
      apps/vue/src/components/Abp/ExtraPropertyDictionary/index.ts
  34. 145
      apps/vue/src/components/Abp/FeatureModal/hooks/useFeatures.ts
  35. 0
      apps/vue/src/components/Abp/FeatureModal/index.ts
  36. 100
      apps/vue/src/components/Abp/FeatureModal/src/FeatureModal.vue
  37. 168
      apps/vue/src/components/Abp/LocalizableInput/LocalizableInput.vue
  38. 3
      apps/vue/src/components/Abp/LocalizableInput/index.ts
  39. 9
      apps/vue/src/components/Abp/LocalizableInput/props.ts
  40. 551
      apps/vue/src/components/Abp/StringValueType/StringValueTypeInput.vue
  41. 3
      apps/vue/src/components/Abp/StringValueType/index.ts
  42. 134
      apps/vue/src/components/Abp/StringValueType/validator.ts
  43. 122
      apps/vue/src/components/Abp/StringValueType/valueType.ts
  44. 4
      apps/vue/src/components/Abp/index.ts
  45. 7
      apps/vue/src/components/Form/src/componentMap.ts
  46. 4
      apps/vue/src/components/Form/src/types/index.ts
  47. 34
      apps/vue/src/components/Permission/src/PermissionModal.vue
  48. 2
      apps/vue/src/components/SettingManagement/src/SettingForm.vue
  49. 2
      apps/vue/src/hooks/abp/useDefineSettings.ts
  50. 32
      apps/vue/src/hooks/abp/useFeatures.ts
  51. 24
      apps/vue/src/hooks/abp/useLocalization.ts
  52. 101
      apps/vue/src/hooks/abp/useLocalizationSerializer.ts
  53. 85
      apps/vue/src/locales/lang/en/component.ts
  54. 81
      apps/vue/src/locales/lang/zh-CN/component.ts
  55. 2
      apps/vue/src/store/modules/settings.ts
  56. 23
      apps/vue/src/utils/table.ts
  57. 4
      apps/vue/src/views/account/center/Setting.vue
  58. 433
      apps/vue/src/views/feature-management/definitions/features/components/FeatureDefinitionModal.vue
  59. 268
      apps/vue/src/views/feature-management/definitions/features/components/FeatureDefinitionTable.vue
  60. 22
      apps/vue/src/views/feature-management/definitions/features/datas/ModalData.ts
  61. 69
      apps/vue/src/views/feature-management/definitions/features/datas/TableData.ts
  62. 16
      apps/vue/src/views/feature-management/definitions/features/index.vue
  63. 199
      apps/vue/src/views/feature-management/definitions/groups/components/GroupDefinitionModal.vue
  64. 159
      apps/vue/src/views/feature-management/definitions/groups/components/GroupDefinitionTable.vue
  65. 15
      apps/vue/src/views/feature-management/definitions/groups/datas/ModalData.ts
  66. 35
      apps/vue/src/views/feature-management/definitions/groups/datas/TableData.ts
  67. 16
      apps/vue/src/views/feature-management/definitions/groups/index.vue
  68. 145
      apps/vue/src/views/feature/hooks/useFeature.ts
  69. 89
      apps/vue/src/views/feature/src/FeatureModal.vue
  70. 6
      apps/vue/src/views/localization/languages/components/LanguageTable.vue
  71. 6
      apps/vue/src/views/localization/resources/components/ResourceTable.vue
  72. 4
      apps/vue/src/views/localization/texts/components/ModalData.ts
  73. 7
      apps/vue/src/views/localization/texts/components/TextTable.vue
  74. 4
      apps/vue/src/views/openiddict/applications/components/ApplicationModal.vue
  75. 22
      apps/vue/src/views/openiddict/applications/components/ApplicationTable.vue
  76. 49
      apps/vue/src/views/oss-management/objects/components/FileList.vue
  77. 45
      apps/vue/src/views/oss-management/objects/components/FolderTree.vue
  78. 2
      apps/vue/src/views/oss-management/objects/components/OssFolderModal.vue
  79. 25
      apps/vue/src/views/oss-management/objects/components/OssManagePage.vue
  80. 8
      apps/vue/src/views/oss-management/objects/components/OssUploadModal.vue
  81. 2
      apps/vue/src/views/oss-management/objects/index.vue
  82. 2
      apps/vue/src/views/saas/editions/components/EditionTable.vue
  83. 2
      apps/vue/src/views/saas/tenant/components/TenantTable.vue
  84. 2
      apps/vue/src/views/saas/tenant/hooks/useTenantTable.ts
  85. 230
      apps/vue/src/views/settings-management/definitions/components/SettingDefinitionModal.vue
  86. 155
      apps/vue/src/views/settings-management/definitions/components/SettingDefinitionTable.vue
  87. 30
      apps/vue/src/views/settings-management/definitions/datas/ModalData.ts
  88. 83
      apps/vue/src/views/settings-management/definitions/datas/TableData.ts
  89. 16
      apps/vue/src/views/settings-management/definitions/index.vue
  90. 10
      apps/vue/src/views/settings-management/definitions/types/props.ts
  91. 93
      apps/vue/src/views/settings-management/settings/index.vue
  92. 6
      apps/vue/src/views/sys/settings/index.vue
  93. 252
      apps/vue/src/views/text-templating/templates/components/TemplateDefinitionModal.vue
  94. 65
      apps/vue/src/views/text-templating/templates/components/TemplateDefinitionTable.vue
  95. 173
      apps/vue/src/views/text-templating/templates/datas/ModalData.ts
  96. 10
      apps/vue/src/views/text-templating/templates/datas/TableData.ts
  97. 201
      apps/vue/src/views/webhooks/definitions/groups/components/GroupDefinitionModal.vue
  98. 147
      apps/vue/src/views/webhooks/definitions/groups/components/GroupDefinitionTable.vue
  99. 15
      apps/vue/src/views/webhooks/definitions/groups/datas/ModalData.ts
  100. 35
      apps/vue/src/views/webhooks/definitions/groups/datas/TableData.ts

4
apps/vue/.env.development

@ -6,7 +6,7 @@ VITE_PUBLIC_PATH=/
# Cross-domain proxy, you can configure multiple # Cross-domain proxy, you can configure multiple
# Please note that no line breaks # Please note that no line breaks
VITE_PROXY=[["/connect","http://127.0.0.1:44385"],["/api","http://127.0.0.1:30000"],["/signalr-hubs","ws://127.0.0.1:30000"]] VITE_PROXY=[["/connect","http://127.0.0.1:30000"],["/api","http://127.0.0.1:30000"],["/signalr-hubs","ws://127.0.0.1:30000"]]
# VITE_PROXY=[["/api","https://vvbin.cn/test"]] # VITE_PROXY=[["/api","https://vvbin.cn/test"]]
# Delete console # Delete console
@ -25,6 +25,6 @@ VITE_GLOB_API_URL_PREFIX=
VITE_GLOB_MULTITENANCY_KEY='__tenant' VITE_GLOB_MULTITENANCY_KEY='__tenant'
# STS Connect # STS Connect
VITE_GLOB_AUTHORITY='http://127.0.0.1:44385' VITE_GLOB_AUTHORITY='http://127.0.0.1:30000'
VITE_GLOB_CLIENT_ID='vue-admin-client' VITE_GLOB_CLIENT_ID='vue-admin-client'
VITE_GLOB_CLIENT_SECRET='1q2w3e*' VITE_GLOB_CLIENT_SECRET='1q2w3e*'

40
apps/vue/src/api/feature-management/definitions/features/index.ts

@ -0,0 +1,40 @@
import { defHttp } from '/@/utils/http/axios';
import {
FeatureDefinitionDto,
FeatureDefinitionCreateDto,
FeatureDefinitionUpdateDto,
FeatureDefinitionGetListInput,
} from './model';
export const CreateAsyncByInput = (input: FeatureDefinitionCreateDto) => {
return defHttp.post<FeatureDefinitionDto>({
url: '/api/feature-management/definitions',
data: input,
});
};
export const DeleteAsyncByName = (name: string) => {
return defHttp.delete<void>({
url: `/api/feature-management/definitions/${name}`,
});
};
export const GetAsyncByName = (name: string) => {
return defHttp.get<FeatureDefinitionDto>({
url: `/api/feature-management/definitions/${name}`,
});
};
export const GetListAsyncByInput = (input: FeatureDefinitionGetListInput) => {
return defHttp.get<ListResultDto<FeatureDefinitionDto>>({
url: '/api/feature-management/definitions',
params: input,
});
};
export const UpdateAsyncByNameAndInput = (name: string, input: FeatureDefinitionUpdateDto) => {
return defHttp.put<FeatureDefinitionDto>({
url: `/api/feature-management/definitions/${name}`,
data: input,
});
};

36
apps/vue/src/api/feature-management/definitions/features/model/index.ts

@ -0,0 +1,36 @@
interface FeatureDefinitionCreateOrUpdateDto extends IHasExtraProperties {
displayName: string;
parentName?: string;
description?: string;
defaultValue?: string;
valueType: string;
isVisibleToClients: boolean;
isAvailableToHost: boolean;
allowedProviders: string[];
}
export interface FeatureDefinitionCreateDto extends FeatureDefinitionCreateOrUpdateDto {
name: string;
groupName: string;
}
export interface FeatureDefinitionDto extends IHasExtraProperties {
name: string;
groupName: string;
displayName: string;
parentName?: string;
description?: string;
defaultValue?: string;
valueType: string;
isStatic: boolean;
isVisibleToClients: boolean;
isAvailableToHost: boolean;
allowedProviders: string[];
}
export interface FeatureDefinitionGetListInput {
filter?: string;
groupName?: string;
}
export type FeatureDefinitionUpdateDto = FeatureDefinitionCreateOrUpdateDto;

40
apps/vue/src/api/feature-management/definitions/groups/index.ts

@ -0,0 +1,40 @@
import { defHttp } from '/@/utils/http/axios';
import {
FeatureGroupDefinitionDto,
FeatureGroupDefinitionCreateDto,
FeatureGroupDefinitionUpdateDto,
FeatureGroupDefinitionGetListInput,
} from './model';
export const CreateAsyncByInput = (input: FeatureGroupDefinitionCreateDto) => {
return defHttp.post<FeatureGroupDefinitionDto>({
url: '/api/feature-management/definitions/groups',
data: input,
});
};
export const DeleteAsyncByName = (name: string) => {
return defHttp.delete<void>({
url: `/api/feature-management/definitions/groups/${name}`,
});
};
export const GetAsyncByName = (name: string) => {
return defHttp.get<FeatureGroupDefinitionDto>({
url: `/api/feature-management/definitions/groups/${name}`,
});
};
export const GetListAsyncByInput = (input: FeatureGroupDefinitionGetListInput) => {
return defHttp.get<ListResultDto<FeatureGroupDefinitionDto>>({
url: '/api/feature-management/definitions/groups',
params: input,
});
};
export const UpdateAsyncByNameAndInput = (name: string, input: FeatureGroupDefinitionUpdateDto) => {
return defHttp.put<FeatureGroupDefinitionDto>({
url: `/api/feature-management/definitions/groups/${name}`,
data: input,
});
};

19
apps/vue/src/api/feature-management/definitions/groups/model/index.ts

@ -0,0 +1,19 @@
interface FeatureGroupDefinitionCreateOrUpdateDto extends IHasExtraProperties {
displayName: string;
}
export interface FeatureGroupDefinitionCreateDto extends FeatureGroupDefinitionCreateOrUpdateDto {
name: string;
}
export interface FeatureGroupDefinitionDto extends IHasExtraProperties {
name: string;
displayName: string;
isStatic: boolean;
}
export interface FeatureGroupDefinitionGetListInput {
filter?: string;
}
export type FeatureGroupDefinitionUpdateDto = FeatureGroupDefinitionCreateOrUpdateDto;

20
apps/vue/src/api/feature-management/features/index.ts

@ -0,0 +1,20 @@
import { defAbpHttp } from '/@/utils/http/abp';
import { FeatureGroupResult, UpdateFeatures, FeatureUpdateByProvider, FeatureGetByProvider } from './model';
export const GetByProvider = (provider: FeatureGetByProvider) => {
return defAbpHttp.get<FeatureGroupResult>({
url: '/api/feature-management/features',
params: provider,
});
};
export const UpdateByProvider = (
provider: FeatureUpdateByProvider,
input: UpdateFeatures
) => {
return defAbpHttp.put<void>({
url: '/api/feature-management/features',
data: input,
params: provider,
});
};

44
apps/vue/src/api/feature-management/features/model/index.ts

@ -0,0 +1,44 @@
export interface Provider {
name: string;
key: string;
}
export interface Feature {
name: string;
displayName: string;
value: any;
provider: Provider;
description?: string;
valueType: ValueType;
depth: number;
parentName?: string;
}
export interface FeatureGroup {
name: string;
displayName: string;
features: Feature[];
}
export class FeatureGroupResult {
groups!: FeatureGroup[];
}
export interface UpdateFeature {
name: string;
value: string;
}
export interface UpdateFeatures {
features: UpdateFeature[];
}
export interface FeatureGetByProvider {
providerName: string;
providerKey: string | null;
}
export interface FeatureUpdateByProvider {
providerName: string;
providerKey: string | null;
}

26
apps/vue/src/api/feature/feature.ts

@ -1,26 +0,0 @@
import { defAbpHttp } from '/@/utils/http/abp';
import { FeatureGroupResult, UpdateFeatures } from './model/featureModel';
/** 与 multi-tenancy中不同,此为管理tenant api */
enum Api {
Get = '/api/feature-management/features',
Update = '/api/feature-management/features',
}
export const get = (provider: { providerName: string; providerKey: string | null }) => {
return defAbpHttp.get<FeatureGroupResult>({
url: Api.Get,
params: provider,
});
};
export const update = (
provider: { providerName: string; providerKey: string | null },
input: UpdateFeatures
) => {
return defAbpHttp.put<void>({
url: Api.Update,
data: input,
params: provider,
});
};

34
apps/vue/src/api/feature/model/featureModel.ts

@ -1,34 +0,0 @@
export interface Provider {
name: string;
key: string;
}
export interface Feature {
name: string;
displayName: string;
value: any;
provider: Provider;
description?: string;
valueType: ValueType;
depth: number;
parentName?: string;
}
export interface FeatureGroup {
name: string;
displayName: string;
features: Feature[];
}
export class FeatureGroupResult {
groups!: FeatureGroup[];
}
export interface UpdateFeature {
name: string;
value: string;
}
export interface UpdateFeatures {
features: UpdateFeature[];
}

2
apps/vue/src/api/messages/notifications.ts

@ -10,7 +10,7 @@ import { format } from '/@/utils/strings';
enum Api { enum Api {
GetById = '/api/notifications/my-notifilers/{id}', GetById = '/api/notifications/my-notifilers/{id}',
GetList = '/api/notifications/my-notifilers', GetList = '/api/notifications/my-notifilers',
GetAssignableNotifiers = '/api/notifications/notifilers/assignables', GetAssignableNotifiers = '/api/notifications/assignables',
Read = '/api/notifications/my-notifilers/{id}/read', Read = '/api/notifications/my-notifilers/{id}/read',
MarkReadState = '/api/notifications/my-notifilers/mark-read-state', MarkReadState = '/api/notifications/my-notifilers/mark-read-state',
} }

3
apps/vue/src/api/oss-management/oss.ts

@ -202,9 +202,6 @@ export const deleteObject = (input: GetOssObjectRequest) => {
url: Api.DeleteObject, url: Api.DeleteObject,
params: input, params: input,
}, },
{
joinParamsToUrl: true,
},
); );
}; };

41
apps/vue/src/api/settings-management/definitions/index.ts

@ -0,0 +1,41 @@
import { defHttp } from '/@/utils/http/axios';
import {
SettingDefinitionDto,
SettingDefinitionCreateDto,
SettingDefinitionUpdateDto,
SettingDefinitionGetListInput,
} from './model';
export const GetAsyncByName = (name: string) => {
return defHttp.get<SettingDefinitionDto>({
url: `/api/setting-management/settings/definitions/${name}`,
});
};
export const GetListAsyncByInput = (input: SettingDefinitionGetListInput) => {
return defHttp.get<ListResultDto<SettingDefinitionDto>>({
url: `/api/setting-management/settings/definitions`,
params: input,
});
}
export const CreateAsyncByInput = (input: SettingDefinitionCreateDto) => {
return defHttp.post<SettingDefinitionDto>({
url: `/api/setting-management/settings/definitions`,
data: input,
});
};
export const UpdateAsyncByNameAndInput = (name: string, input: SettingDefinitionUpdateDto) => {
return defHttp.put<SettingDefinitionDto>({
url: `/api/setting-management/settings/definitions/${name}`,
data: input,
});
};
export const DeleteOrRestoreAsyncByName = (name: string) => {
return defHttp.delete<void>({
url: `/api/setting-management/settings/definitions/${name}`,
});
};

32
apps/vue/src/api/settings-management/definitions/model/index.ts

@ -0,0 +1,32 @@
export interface SettingDefinitionDto extends ExtensibleObject {
name: string;
displayName: string;
description?: string;
defaultValue?: string;
isVisibleToClients: boolean;
providers: string[];
isInherited: boolean;
isEncrypted: boolean;
isStatic: boolean;
}
export interface SettingDefinitionGetListInput {
filter?: string;
providerName?: string;
}
interface SettingDefinitionCreateOrUpdateDto extends IHasConcurrencyStamp, IHasExtraProperties {
displayName: string;
description?: string;
defaultValue?: string;
isInherited: boolean;
isEncrypted: boolean;
isVisibleToClients: boolean;
providers: string[];
}
export interface SettingDefinitionCreateDto extends SettingDefinitionCreateOrUpdateDto {
name: string;
}
export type SettingDefinitionUpdateDto = SettingDefinitionCreateOrUpdateDto;

60
apps/vue/src/api/settings-management/settings/index.ts

@ -0,0 +1,60 @@
import { defHttp } from '/@/utils/http/axios';
import { SettingGroupResult, SettingsUpdate } from './model';
enum Api {
GetGlobalSettings = '/api/setting-management/settings/by-global',
SetGlobalSettings = '/api/setting-management/settings/change-global',
GetCurrentTenantSettings = '/api/setting-management/settings/by-current-tenant',
SetCurrentTenantSettings = '/api/setting-management/settings/change-current-tenant',
GetCurrentUserSettings = '/api/setting-management/settings/by-current-user',
SetCurrentUserSettings = '/api/setting-management/settings/change-current-user',
SendTestEmail = '/api/setting-management/settings/send-test-email'
}
export const getGlobalSettings = () => {
return defHttp.get<SettingGroupResult>({
url: Api.GetGlobalSettings,
});
};
export const setGlobalSettings = (payload: SettingsUpdate) => {
return defHttp.put({
data: payload,
url: Api.SetGlobalSettings,
});
};
export const getCurrentTenantSettings = () => {
return defHttp.get<SettingGroupResult>({
url: Api.GetCurrentTenantSettings,
});
};
export const setCurrentTenantSettings = (payload: SettingsUpdate) => {
return defHttp.put({
data: payload,
url: Api.SetCurrentTenantSettings,
});
};
export const getCurrentUserSettings = () => {
return defHttp.get<SettingGroupResult>({
url: Api.GetCurrentUserSettings,
});
};
export const setCurrentUserSettings = (payload: SettingsUpdate) => {
return defHttp.put({
data: payload,
url: Api.SetCurrentUserSettings,
});
};
export const sendTestEmail = (emailAddress: string) => {
return defHttp.post({
data: {
emailAddress: emailAddress
},
url: Api.SendTestEmail,
});
}

0
apps/vue/src/api/settings/model/settingModel.ts → apps/vue/src/api/settings-management/settings/model/index.ts

60
apps/vue/src/api/settings/settings.ts

@ -1,60 +0,0 @@
import { defHttp } from '/@/utils/http/axios';
import { SettingGroupResult, SettingsUpdate } from './model/settingModel';
enum Api {
GetGlobalSettings = '/api/setting-management/settings/by-global',
SetGlobalSettings = '/api/setting-management/settings/change-global',
GetCurrentTenantSettings = '/api/setting-management/settings/by-current-tenant',
SetCurrentTenantSettings = '/api/setting-management/settings/change-current-tenant',
GetCurrentUserSettings = '/api/setting-management/settings/by-current-user',
SetCurrentUserSettings = '/api/setting-management/settings/change-current-user',
SendTestEmail = '/api/setting-management/settings/send-test-email'
}
export const getGlobalSettings = () => {
return defHttp.get<SettingGroupResult>({
url: Api.GetGlobalSettings,
});
};
export const setGlobalSettings = (payload: SettingsUpdate) => {
return defHttp.put({
data: payload,
url: Api.SetGlobalSettings,
});
};
export const getCurrentTenantSettings = () => {
return defHttp.get<SettingGroupResult>({
url: Api.GetCurrentTenantSettings,
});
};
export const setCurrentTenantSettings = (payload: SettingsUpdate) => {
return defHttp.put({
data: payload,
url: Api.SetCurrentTenantSettings,
});
};
export const getCurrentUserSettings = () => {
return defHttp.get<SettingGroupResult>({
url: Api.GetCurrentUserSettings,
});
};
export const setCurrentUserSettings = (payload: SettingsUpdate) => {
return defHttp.put({
data: payload,
url: Api.SetCurrentUserSettings,
});
};
export const sendTestEmail = (emailAddress: string) => {
return defHttp.post({
data: {
emailAddress: emailAddress
},
url: Api.SendTestEmail,
});
}

6
apps/vue/src/api/text-templating/contents/index.ts

@ -7,7 +7,7 @@ import {
} from './model'; } from './model';
const remoteServiceName = 'AbpTextTemplating'; const remoteServiceName = 'AbpTextTemplating';
const controllerName = 'TextTemplate'; const controllerName = 'TextTemplateContent';
export const GetAsyncByInput = (input: TextTemplateContentGetInput) => { export const GetAsyncByInput = (input: TextTemplateContentGetInput) => {
return defAbpHttp.request<TextTemplateContentDto>({ return defAbpHttp.request<TextTemplateContentDto>({
@ -15,7 +15,9 @@ export const GetAsyncByInput = (input: TextTemplateContentGetInput) => {
controller: controllerName, controller: controllerName,
action: 'GetAsync', action: 'GetAsync',
uniqueName: 'GetAsyncByInput', uniqueName: 'GetAsyncByInput',
params: input, params: {
input: input,
},
}); });
}; };

2
apps/vue/src/api/text-templating/definitions/index.ts

@ -44,7 +44,7 @@ export const GetByNameAsyncByName = (name: string) => {
}; };
export const GetListAsyncByInput = (input: TextTemplateDefinitionGetListInput) => { export const GetListAsyncByInput = (input: TextTemplateDefinitionGetListInput) => {
return defAbpHttp.request<PagedResultDto<TextTemplateDefinitionDto>>({ return defAbpHttp.request<ListResultDto<TextTemplateDefinitionDto>>({
service: remoteServiceName, service: remoteServiceName,
controller: controllerName, controller: controllerName,
action: 'GetListAsync', action: 'GetListAsync',

5
apps/vue/src/api/text-templating/definitions/model/index.ts

@ -1,7 +1,6 @@
export interface TextTemplateDefinitionDto { export interface TextTemplateDefinitionDto {
name: string; name: string;
displayName: string; displayName: string;
formatedDisplayName?: string;
defaultCultureName?: string; defaultCultureName?: string;
isInlineLocalized: boolean; isInlineLocalized: boolean;
isLayout: boolean; isLayout: boolean;
@ -27,6 +26,8 @@ export interface TextTemplateDefinitionCreateDto extends TextTemplateDefinitionC
export interface TextTemplateDefinitionUpdateDto extends TextTemplateDefinitionCreateOrUpdateDto, IHasConcurrencyStamp { } export interface TextTemplateDefinitionUpdateDto extends TextTemplateDefinitionCreateOrUpdateDto, IHasConcurrencyStamp { }
export interface TextTemplateDefinitionGetListInput extends PagedAndSortedResultRequestDto { export interface TextTemplateDefinitionGetListInput {
filter?: string; filter?: string;
isLayout?: boolean;
isStatic?: boolean;
} }

41
apps/vue/src/api/webhooks/definitions/groups/index.ts

@ -0,0 +1,41 @@
import { defHttp } from '/@/utils/http/axios';
import {
WebhookGroupDefinitionDto,
WebhookGroupDefinitionCreateDto,
WebhookGroupDefinitionUpdateDto,
WebhookGroupDefinitionGetListInput,
} from './model';
export const GetAsyncByName = (name: string) => {
return defHttp.get<WebhookGroupDefinitionDto>({
url: `/api/webhooks/definitions/groups/${name}`,
});
};
export const GetListAsyncByInput = (input: WebhookGroupDefinitionGetListInput) => {
return defHttp.get<ListResultDto<WebhookGroupDefinitionDto>>({
url: `/api/webhooks/definitions/groups`,
params: input,
});
}
export const CreateAsyncByInput = (input: WebhookGroupDefinitionCreateDto) => {
return defHttp.post<WebhookGroupDefinitionDto>({
url: `/api/webhooks/definitions/groups`,
data: input,
});
};
export const UpdateAsyncByNameAndInput = (name: string, input: WebhookGroupDefinitionUpdateDto) => {
return defHttp.put<WebhookGroupDefinitionDto>({
url: `/api/webhooks/definitions/groups/${name}`,
data: input,
});
};
export const DeleteAsyncByName = (name: string) => {
return defHttp.delete<void>({
url: `/api/webhooks/definitions/groups/${name}`,
});
};

19
apps/vue/src/api/webhooks/definitions/groups/model/index.ts

@ -0,0 +1,19 @@
interface WebhookGroupDefinitionCreateOrUpdateDto extends IHasExtraProperties {
displayName: string;
}
export interface WebhookGroupDefinitionCreateDto extends WebhookGroupDefinitionCreateOrUpdateDto {
name: string;
}
export interface WebhookGroupDefinitionDto extends IHasExtraProperties {
name: string;
displayName: string;
isStatic: boolean;
}
export interface WebhookGroupDefinitionGetListInput {
filter?: string;
}
export type WebhookGroupDefinitionUpdateDto = WebhookGroupDefinitionCreateOrUpdateDto;

41
apps/vue/src/api/webhooks/definitions/webhooks/index.ts

@ -0,0 +1,41 @@
import { defHttp } from '/@/utils/http/axios';
import {
WebhookDefinitionDto,
WebhookDefinitionCreateDto,
WebhookDefinitionUpdateDto,
WebhookDefinitionGetListInput,
} from './model';
export const GetAsyncByName = (name: string) => {
return defHttp.get<WebhookDefinitionDto>({
url: `/api/webhooks/definitions/${name}`,
});
};
export const GetListAsyncByInput = (input: WebhookDefinitionGetListInput) => {
return defHttp.get<ListResultDto<WebhookDefinitionDto>>({
url: `/api/webhooks/definitions`,
params: input,
});
}
export const CreateAsyncByInput = (input: WebhookDefinitionCreateDto) => {
return defHttp.post<WebhookDefinitionDto>({
url: `/api/webhooks/definitions`,
data: input,
});
};
export const UpdateAsyncByNameAndInput = (name: string, input: WebhookDefinitionUpdateDto) => {
return defHttp.put<WebhookDefinitionDto>({
url: `/api/webhooks/definitions/${name}`,
data: input,
});
};
export const DeleteAsyncByName = (name: string) => {
return defHttp.delete<void>({
url: `/api/webhooks/definitions/${name}`,
});
};

28
apps/vue/src/api/webhooks/definitions/webhooks/model/index.ts

@ -0,0 +1,28 @@
interface WebhookDefinitionCreateOrUpdateDto extends IHasExtraProperties {
displayName: string;
description?: string;
isEnabled: boolean;
requiredFeatures: string[];
}
export interface WebhookDefinitionDto extends IHasExtraProperties {
groupName: string;
name: string;
displayName: string;
description?: string;
isEnabled: boolean;
isStatic: boolean;
requiredFeatures: string[];
}
export interface WebhookDefinitionCreateDto extends WebhookDefinitionCreateOrUpdateDto {
groupName: string;
name: string;
}
export interface WebhookDefinitionGetListInput {
filter?: string;
groupName?: string;
}
export type WebhookDefinitionUpdateDto = WebhookDefinitionCreateOrUpdateDto;

32
apps/vue/src/api/webhooks/model/sendAttemptsModel.ts

@ -1,32 +0,0 @@
import { HttpStatusCode } from '/@/enums/httpEnum';
export interface WebhookEvent {
tenantId?: string;
webhookName: string;
data: string;
creationTime: Date;
}
export interface WebhookSendAttempt {
id: string;
tenantId?: string;
webhookEventId: string;
webhookSubscriptionId: string;
response: string;
responseStatusCode?: HttpStatusCode;
creationTime: Date;
lastModificationTime?: Date;
sendExactSameData: boolean;
requestHeaders: Record<string, string>;
responseHeaders: Record<string, string>;
webhookEvent: WebhookEvent;
}
export interface WebhookSendAttemptGetListInput extends PagedAndSortedResultRequestDto {
filter?: string;
webhookEventId?: string;
subscriptionId?: string;
responseStatusCode?: HttpStatusCode;
beginCreationTime?: Date;
endCreationTime?: Date;
}

45
apps/vue/src/api/webhooks/model/subscriptionsModel.ts

@ -1,45 +0,0 @@
export interface WebhookSubscription extends CreationAuditedEntityDto<string>, IHasConcurrencyStamp {
tenantId?: string;
webhookUri: string;
description?: string;
secret: string;
isActive: boolean;
webhooks: string[];
headers: Dictionary<string, string>;
}
export interface WebhookSubscriptionCreateOrUpdate {
webhookUri: string;
description?: string;
secret: string;
isActive: boolean;
webhooks: string[];
headers: Dictionary<string, string>;
}
export type CreateWebhookSubscription = WebhookSubscriptionCreateOrUpdate;
export interface UpdateWebhookSubscription extends WebhookSubscriptionCreateOrUpdate , IHasConcurrencyStamp {};
export interface WebhookAvailable {
name: string;
displayName: string;
description: string;
}
export interface WebhookAvailableGroup {
name: string;
displayName: string;
webhooks: WebhookAvailable[];
}
export interface WebhookSubscriptionGetListInput extends PagedAndSortedResultRequestDto {
filter?: string;
tenantId?: string;
webhookUri?: string;
secret?: string;
isActive?: boolean;
webhooks?: string;
beginCreationTime?: Date;
endCreationTime?: Date;
}

73
apps/vue/src/api/webhooks/send-attempts.ts

@ -1,73 +0,0 @@
import { defAbpHttp } from '/@/utils/http/abp';
import { WebhookSendAttempt, WebhookSendAttemptGetListInput } from './model/sendAttemptsModel';
const remoteServiceName = 'WebhooksManagement';
const controllerName = 'WebhookSendRecord';
export const getById = (id: string) => {
return defAbpHttp.request<WebhookSendAttempt>({
service: remoteServiceName,
controller: controllerName,
action: 'GetAsync',
params: {
id: id,
},
});
};
export const deleteById = (id: string) => {
return defAbpHttp.request<WebhookSendAttempt>({
service: remoteServiceName,
controller: controllerName,
action: 'DeleteAsync',
params: {
id: id,
},
});
}
export const deleteMany = (keys: string[]) => {
return defAbpHttp.request<void>({
service: remoteServiceName,
controller: controllerName,
action: 'DeleteManyAsync',
uniqueName: 'DeleteManyAsyncByInput',
data: {
recordIds: keys,
},
});
}
export const getList = (input: WebhookSendAttemptGetListInput) => {
return defAbpHttp.request<PagedResultDto<WebhookSendAttempt>>({
service: remoteServiceName,
controller: controllerName,
action: 'GetListAsync',
params: {
input: input,
},
});
};
export const resend = (id: string) => {
return defAbpHttp.request<void>({
service: remoteServiceName,
controller: controllerName,
action: 'ResendAsync',
params: {
id: id,
},
});
}
export const resendMany = (keys: string[]) => {
return defAbpHttp.request<void>({
service: remoteServiceName,
controller: controllerName,
action: 'ResendManyAsync',
uniqueName: 'ResendManyAsyncByInput',
data: {
recordIds: keys,
},
});
}

46
apps/vue/src/api/webhooks/send-attempts/index.ts

@ -0,0 +1,46 @@
import { defAbpHttp } from '/@/utils/http/abp';
import {
WebhookSendAttempt,
WebhookSendAttemptGetListInput,
WebhookSendRecordDeleteManyInput,
WebhookSendRecordResendManyInput,
} from './model';
export const GetAsyncById = (id: string) => {
return defAbpHttp.get<WebhookSendAttempt>({
url: `/api/webhooks/send-attempts/${id}`,
});
};
export const DeleteAsyncById = (id: string) => {
return defAbpHttp.delete<void>({
url: `/api/webhooks/send-attempts/${id}`,
});
};
export const DeleteManyAsyncByInput = (input: WebhookSendRecordDeleteManyInput) => {
return defAbpHttp.delete<void>({
url: `/api/webhooks/send-attempts/delete-many`,
data: input,
});
};
export const GetListAsyncByInput = (input: WebhookSendAttemptGetListInput) => {
return defAbpHttp.get<PagedResultDto<WebhookSendAttempt>>({
url: `/api/webhooks/send-attempts`,
params: input,
});
};
export const ResendAsyncById = (id: string) => {
return defAbpHttp.post<void>({
url: `/api/webhooks/send-attempts/${id}/resend`,
});
};
export const ResendManyAsyncByInput = (input: WebhookSendRecordResendManyInput) => {
return defAbpHttp.post<void>({
url: `/api/webhooks/send-attempts/resend-many`,
data: input,
});
};

40
apps/vue/src/api/webhooks/send-attempts/model/index.ts

@ -0,0 +1,40 @@
import { HttpStatusCode } from '/@/enums/httpEnum';
export interface WebhookEvent {
tenantId?: string;
webhookName: string;
data: string;
creationTime: Date;
}
export interface WebhookSendAttempt {
id: string;
tenantId?: string;
webhookEventId: string;
webhookSubscriptionId: string;
response: string;
responseStatusCode?: HttpStatusCode;
creationTime: Date;
lastModificationTime?: Date;
sendExactSameData: boolean;
requestHeaders: Record<string, string>;
responseHeaders: Record<string, string>;
webhookEvent: WebhookEvent;
}
export interface WebhookSendAttemptGetListInput extends PagedAndSortedResultRequestDto {
filter?: string;
webhookEventId?: string;
subscriptionId?: string;
responseStatusCode?: HttpStatusCode;
beginCreationTime?: Date;
endCreationTime?: Date;
}
export interface WebhookSendRecordDeleteManyInput {
recordIds: string[];
}
export interface WebhookSendRecordResendManyInput {
recordIds: string[];
}

85
apps/vue/src/api/webhooks/subscriptions.ts

@ -1,85 +0,0 @@
import { defAbpHttp } from '/@/utils/http/abp';
import {
WebhookSubscription,
WebhookAvailableGroup,
CreateWebhookSubscription,
UpdateWebhookSubscription,
WebhookSubscriptionGetListInput,
} from './model/subscriptionsModel';
const remoteServiceName = 'WebhooksManagement';
const controllerName = 'WebhookSubscription';
export const create = (input: CreateWebhookSubscription) => {
return defAbpHttp.request<WebhookSubscription>({
service: remoteServiceName,
controller: controllerName,
action: 'CreateAsync',
data: input,
});
};
export const update = (id: string, input: UpdateWebhookSubscription) => {
return defAbpHttp.request<WebhookSubscription>({
service: remoteServiceName,
controller: controllerName,
action: 'UpdateAsync',
data: input,
params: {
id: id,
},
});
};
export const getById = (id: string) => {
return defAbpHttp.request<WebhookSubscription>({
service: remoteServiceName,
controller: controllerName,
action: 'GetAsync',
params: {
id: id,
},
});
};
export const deleteById = (id: string) => {
return defAbpHttp.request<WebhookSubscription>({
service: remoteServiceName,
controller: controllerName,
action: 'DeleteAsync',
params: {
id: id,
},
});
};
export const deleteMany = (keys: string[]) => {
return defAbpHttp.request<void>({
service: remoteServiceName,
controller: controllerName,
action: 'DeleteManyAsync',
uniqueName: 'DeleteManyAsyncByInput',
data: {
recordIds: keys,
},
});
};
export const getList = (input: WebhookSubscriptionGetListInput) => {
return defAbpHttp.request<PagedResultDto<WebhookSubscription>>({
service: remoteServiceName,
controller: controllerName,
action: 'GetListAsync',
params: {
input: input,
},
});
};
export const getAllAvailableWebhooks = () => {
return defAbpHttp.request<ListResultDto<WebhookAvailableGroup>>({
service: remoteServiceName,
controller: controllerName,
action: 'GetAllAvailableWebhooksAsync',
});
};

55
apps/vue/src/api/webhooks/subscriptions/index.ts

@ -0,0 +1,55 @@
import { defAbpHttp } from '/@/utils/http/abp';
import {
WebhookSubscription,
WebhookAvailableGroup,
CreateWebhookSubscription,
UpdateWebhookSubscription,
WebhookSubscriptionGetListInput,
WebhookSubscriptionDeleteManyInput,
} from './model';
export const CreateAsyncByInput = (input: CreateWebhookSubscription) => {
return defAbpHttp.post<WebhookSubscription>({
url: `/api/webhooks/subscriptions`,
data: input,
});
};
export const UpdateAsyncByIdAndInput = (id: string, input: UpdateWebhookSubscription) => {
return defAbpHttp.put<WebhookSubscription>({
url: `/api/webhooks/subscriptions/${id}`,
data: input,
});
};
export const GetAsyncById = (id: string) => {
return defAbpHttp.get<WebhookSubscription>({
url: `/api/webhooks/subscriptions/${id}`,
});
};
export const DeleteAsyncById = (id: string) => {
return defAbpHttp.delete<void>({
url: `/api/webhooks/subscriptions/${id}`,
});
};
export const DeleteManyAsyncByInput = (input: WebhookSubscriptionDeleteManyInput) => {
return defAbpHttp.delete<void>({
url: `/api/webhooks/subscriptions/delete-many`,
data: input,
});
};
export const GetListAsyncByInput = (input: WebhookSubscriptionGetListInput) => {
return defAbpHttp.get<PagedResultDto<WebhookSubscription>>({
url: `/api/webhooks/subscriptions`,
params: input,
});
};
export const GetAllAvailableWebhooksAsync = () => {
return defAbpHttp.get<ListResultDto<WebhookAvailableGroup>>({
url: `/api/webhooks/subscriptions/availables`,
});
};

49
apps/vue/src/api/webhooks/subscriptions/model/index.ts

@ -0,0 +1,49 @@
export interface WebhookSubscription extends CreationAuditedEntityDto<string>, IHasConcurrencyStamp {
tenantId?: string;
webhookUri: string;
description?: string;
secret: string;
isActive: boolean;
webhooks: string[];
headers: Dictionary<string, string>;
}
export interface WebhookSubscriptionCreateOrUpdate {
webhookUri: string;
description?: string;
secret: string;
isActive: boolean;
webhooks: string[];
headers: Dictionary<string, string>;
}
export type CreateWebhookSubscription = WebhookSubscriptionCreateOrUpdate;
export interface UpdateWebhookSubscription extends WebhookSubscriptionCreateOrUpdate , IHasConcurrencyStamp {};
export interface WebhookAvailable {
name: string;
displayName: string;
description: string;
}
export interface WebhookAvailableGroup {
name: string;
displayName: string;
webhooks: WebhookAvailable[];
}
export interface WebhookSubscriptionGetListInput extends PagedAndSortedResultRequestDto {
filter?: string;
tenantId?: string;
webhookUri?: string;
secret?: string;
isActive?: boolean;
webhooks?: string;
beginCreationTime?: Date;
endCreationTime?: Date;
}
export interface WebhookSubscriptionDeleteManyInput {
recordIds: string[];
}

271
apps/vue/src/components/Abp/ExtraPropertyDictionary/ExtraPropertyDictionary.vue

@ -0,0 +1,271 @@
<template>
<div style="width: 100%">
<div :class="`${prefixCls}__toolbar`" v-if="!props.disabled">
<Button type="primary" @click="handleAddNew">{{ t('component.extra_property_dictionary.actions.create') }}</Button>
<Button v-if="props.allowDelete" danger @click="handleClean">{{ t('component.extra_property_dictionary.actions.clean') }}</Button>
</div>
<Card :title="t('component.extra_property_dictionary.title')">
<Table v-bind="state.table">
<template v-if="!props.disabled" #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<div :class="`${prefixCls}__action`">
<Button v-if="props.allowEdit" type="link" @click="() => handleEdit(record)">
<template #icon>
<EditOutlined />
</template>
{{ t('component.extra_property_dictionary.actions.update') }}
</Button>
<Divider v-if="props.allowEdit && props.allowDelete" type="vertical" />
<Button v-if="props.allowDelete" type="link" @click="() => handleDelete(record)" class="ant-btn-error">
<template #icon>
<DeleteOutlined />
</template>
{{ t('component.extra_property_dictionary.actions.delete') }}
</Button>
</div>
</template>
</template>
</Table>
</Card>
<Modal v-bind="state.modal">
<Form
ref="formRef"
:class="`${prefixCls}__form`"
v-bind="state.form"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18}"
>
<FormItem name="key" required :label="t('component.extra_property_dictionary.key')">
<Input :disabled="state.editFlag" v-model:value="state.form.model.key" />
</FormItem>
<FormItem name="value" required :label="t('component.extra_property_dictionary.value')">
<Input v-model:value="state.form.model.value" />
</FormItem>
</Form>
</Modal>
</div>
</template>
<script lang="ts" setup>
import type { TableProps } from 'ant-design-vue';
import type { RuleObject } from 'ant-design-vue/lib/form';
import { cloneDeep } from 'lodash-es';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
import { nextTick, reactive, ref, unref, watch } from 'vue';
import { Button, Card, Divider, Form, Input, Table, Modal } from 'ant-design-vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useI18n } from '/@/hooks/web/useI18n';
import { propTypes } from '/@/utils/propTypes';
const FormItem = Form.Item;
interface State {
editFlag: boolean,
table: TableProps,
modal: {
title?: string,
visible?: boolean,
maskClosable?: boolean,
width?: number,
minHeight?: number,
onOk?: (e: MouseEvent) => void,
onCancel?: (e: MouseEvent) => void,
},
form: {
model: any,
rules?: Dictionary<string, RuleObject>,
},
}
const emits = defineEmits(['change', 'update:value']);
const props = defineProps({
value: {
type: Object as PropType<Dictionary<string, string>>,
},
allowEdit: propTypes.bool.def(false),
allowDelete: propTypes.bool.def(false),
disabled: propTypes.bool.def(false),
});
const { prefixCls } = useDesign('extra-property-dictionary');
const { t } = useI18n();
const formRef = ref<any>();
const state = reactive<State>({
editFlag: false,
modal: {
width: 600,
minHeight: 400,
visible: false,
maskClosable: false,
onOk: handleSubmit,
onCancel: handleCancel,
},
form: {
model: {},
rules: {
key: {
validator: (_rule, value) => {
if (!state.editFlag && state.table.dataSource && state.table.dataSource.findIndex(x => x.key === value) >= 0) {
return Promise.reject(t('component.extra_property_dictionary.validator.duplicateKey'));
}
return Promise.resolve();
}
},
},
},
table: {
sticky: true,
rowKey: "key",
columns: [
{
title: t('component.extra_property_dictionary.key'),
dataIndex: 'key',
align: 'left',
fixed: 'left',
width: 180,
},
{
title: t('component.extra_property_dictionary.value'),
dataIndex: 'value',
align: 'left',
fixed: 'left',
width: 'auto',
}
],
dataSource: [],
scroll: {
x: 1500,
},
}
});
watch(
() => props.value,
(dataSource) => {
if (!dataSource) {
state.table.dataSource = [];
return;
}
state.table.dataSource = Object.keys(dataSource).map((key) => {
return {
key: key,
value: dataSource[key],
};
});
},
{
immediate: true,
}
);
watch(
() => [props.allowEdit, props.allowDelete],
([allowEdit, allowDelete]) => {
if (allowEdit || allowDelete) {
nextTick(() => {
state.table.columns!.push({
width: 220,
title: t('component.extra_property_dictionary.actions.title'),
align: 'center',
dataIndex: 'action',
key: 'action',
fixed: 'right',
});
});
} else {
nextTick(() => {
const columns = state.table.columns ?? [];
const findIndex = columns.findIndex(x => x.key === 'action');
columns.splice(findIndex, 1);
state.table.columns = columns;
});
}
},
{
immediate: true,
}
)
function handleAddNew() {
state.form.model = {};
state.editFlag = false;
state.modal.title = t('component.extra_property_dictionary.actions.create');
state.modal.visible = true;
}
function handleEdit(record) {
state.editFlag = true;
state.form.model = cloneDeep(record);
state.modal.title = t('component.extra_property_dictionary.actions.update');
state.modal.visible = true;
}
function handleClean() {
emits('change', {});
emits('update:value', {});
}
function handleDelete(record) {
const dataSource = state.table.dataSource ?? [];
const findIndex = dataSource.findIndex(x => x.key === record.key);
const changeData: Dictionary<string, string> = {};
dataSource.splice(findIndex, 1);
dataSource.forEach((item) => {
changeData[item.key] = item.value;
});
emits('change', changeData);
emits('update:value', changeData);
}
function handleCancel() {
const form = unref(formRef);
form?.resetFields();
state.modal.visible = false;
}
function handleSubmit() {
const form = unref(formRef);
form?.validate().then(() => {
const dataSource = state.table.dataSource ?? [];
const changeData: Dictionary<string, string> = {};
if (!state.editFlag) {
dataSource.push(state.form.model);
} else {
const findIndex = dataSource.findIndex(x => x.key === state.form.model.key);
dataSource[findIndex] = state.form.model;
}
dataSource.forEach((item) => {
changeData[item.key] = item.value;
});
emits('change', changeData);
emits('update:value', changeData);
form.resetFields();
state.modal.visible = false;
});
}
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-extra-property-dictionary';
.@{prefix-cls} {
&__toolbar {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 8px;
> * {
margin-right: 8px;
}
}
&__action {
display: flex;
align-items: center;
}
&__form {
margin: 10px;
}
}
</style>

3
apps/vue/src/components/Abp/ExtraPropertyDictionary/index.ts

@ -0,0 +1,3 @@
import ExtraPropertyDictionary from './ExtraPropertyDictionary.vue';
export { ExtraPropertyDictionary };

145
apps/vue/src/components/Abp/FeatureModal/hooks/useFeatures.ts

@ -0,0 +1,145 @@
import type { Ref } from 'vue';
import { watch, ref, unref } from 'vue';
import { message } from 'ant-design-vue';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useValidation } from '/@/hooks/abp/useValidation';
import { FeatureGroup } from '/@/api/feature-management/features/model';
import { GetByProvider, UpdateByProvider } from '/@/api/feature-management/features';
import { ReturnInnerMethods } from '/@/components/Modal';
interface UseFeature {
providerName: Ref<string>;
providerKey: Ref<string | null>;
formRel: Ref<any>;
modalMethods: ReturnInnerMethods;
}
export function useFeatures({ providerName, providerKey, formRel, modalMethods }: UseFeature) {
const { L } = useLocalization('AbpFeatureManagement');
const { ruleCreator } = useValidation();
const featureGroup = ref<{ groups: FeatureGroup[] }>({
groups: [],
});
const featureGroupKey = ref(0);
watch(
() => unref(providerKey),
(key) => {
if (key !== undefined) {
const form = unref(formRel);
form.resetFields();
onGroupChange(0);
GetByProvider({
providerName: unref(providerName),
providerKey: key,
}).then((res) => {
featureGroup.value.groups = mapFeatures(res.groups);
});
}
},
);
function getFeatures(groups: FeatureGroup[]) {
const features: { name: string; value: string }[] = [];
groups.forEach((g) => {
g.features.forEach((f) => {
if (f.value !== null) {
features.push({
name: f.name,
value: String(f.value),
});
}
});
});
return features;
}
function mapFeatures(groups: FeatureGroup[]) {
groups.forEach((g) => {
g.features.forEach((f) => {
switch (f.valueType?.validator.name) {
case 'BOOLEAN':
f.value = String(f.value).toLocaleLowerCase() === 'true';
break;
case 'NUMERIC':
f.value = Number(f.value);
break;
}
});
});
return groups;
}
function validator(field: string, validator: Validator) {
const featureRules: { [key: string]: any }[] = new Array<{ [key: string]: any }>();
if (validator.properties) {
switch (validator.name) {
case 'NUMERIC':
featureRules.push(
...ruleCreator.fieldMustBeetWeen({
name: field,
start: Number(validator.properties.MinValue),
end: Number(validator.properties.MaxValue),
trigger: 'change',
}),
);
break;
case 'STRING':
if (
validator.properties.AllowNull &&
validator.properties.AllowNull.toLowerCase() === 'true'
) {
featureRules.push(
...ruleCreator.fieldRequired({
name: field,
trigger: 'blur',
}),
);
}
featureRules.push(
...ruleCreator.fieldMustBeStringWithMinimumLengthAndMaximumLength({
name: field,
minimum: Number(validator.properties.MinValue),
maximum: Number(validator.properties.MaxValue),
trigger: 'blur',
}),
);
break;
default:
break;
}
}
return featureRules;
}
function onGroupChange(activeKey) {
featureGroupKey.value = activeKey;
}
function handleSubmit() {
const form = unref(formRel);
form.validate().then(() => {
UpdateByProvider(
{
providerName: unref(providerName),
providerKey: unref(providerKey),
},
{
features: getFeatures(unref(featureGroup).groups),
},
).then(() => {
modalMethods.closeModal();
message.success(L('Successful'));
});
});
}
return {
featureGroup,
featureGroupKey,
validator,
handleSubmit,
onGroupChange,
};
}

0
apps/vue/src/views/feature/index.ts → apps/vue/src/components/Abp/FeatureModal/index.ts

100
apps/vue/src/components/Abp/FeatureModal/src/FeatureModal.vue

@ -0,0 +1,100 @@
<template>
<BasicModal
v-bind="$attrs"
@register="registerModal"
:title="L('ManageFeatures')"
:width="800"
:min-height="400"
:can-fullscreen="false"
@ok="handleSubmit"
@cancel="onGroupChange(0)"
>
<Form ref="formRel" :model="featureGroup">
<Tabs :class="`${prefixCls}__tabs`" tabPosition="left" v-model:activeKey="featureGroupKey" @change="onGroupChange">
<TabPane v-for="(group, gi) in featureGroup.groups" :key="gi" :tab="group.displayName">
<div v-for="(feature, fi) in group.features" :key="feature.name">
<FormItem
v-if="feature.valueType !== null"
:label="feature.displayName"
:name="['groups', gi, 'features', fi, 'value']"
:rules="validator(feature.displayName, feature.valueType.validator)"
:extra="feature.description"
>
<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"
/>
<BInput v-else v-model:value="feature.value" />
</div>
<Select
v-else-if="feature.valueType.name === 'SelectionStringValueType'"
:allow-clear="true"
v-model:value="feature.value"
>
<Option
v-for="valueItem in (feature.valueType as SelectionStringValueType).itemSource.items"
:key="valueItem.value"
v-model:value="valueItem.value"
:label="Lr(valueItem.displayText.resourceName, valueItem.displayText.name)"
/>
</Select>
</FormItem>
</div>
</TabPane>
</Tabs>
</Form>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { Checkbox, Form, InputNumber, Select, Tabs } from 'ant-design-vue';
import { Input } from '/@/components/Input';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useFeatures } from '../hooks/useFeatures';
const FormItem = Form.Item;
const Option = Select.Option;
const TabPane = Tabs.TabPane;
const BInput = Input!;
const { prefixCls } = useDesign('feature-modal');
const { L, Lr } = useLocalization(['AbpFeatureManagement']);
const formRel = ref(null);
const providerName = ref('');
const providerKey = ref(null);
const [registerModal, modalMethods] = useModalInner((data) => {
providerName.value = data.providerName;
providerKey.value = data.providerKey;
});
const { featureGroup, featureGroupKey, validator, handleSubmit, onGroupChange } = useFeatures({
providerName,
providerKey,
formRel,
modalMethods,
});
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-feature-modal';
.@{prefix-cls} {
&__tabs {
height: 500px;
::v-deep(.ant-tabs-content-holder) {
overflow-y: auto !important;
overflow-x: hidden !important;
}
}
}
</style>

168
apps/vue/src/components/Abp/LocalizableInput/LocalizableInput.vue

@ -0,0 +1,168 @@
<template>
<div style="width: 100%">
<ItemReset>
<InputGroup>
<Row :gutter="4" style="margin-top: 10px;">
<Col :span="8">
<Select
:class="`${prefixCls}__resource`"
:disabled="props.disabled"
:allow-clear="props.allowClear"
:placeholder="t('component.localizable_input.placeholder')"
v-model:value="state.resourceName"
:options="getResources"
@change="handleResourceChange"
/>
</Col>
<Col :span="16">
<Input
v-if="getIsFixed"
:class="`${prefixCls}__name`"
:disabled="props.disabled"
:allow-clear="props.allowClear"
:placeholder="t('component.localizable_input.resources.fiexed.placeholder')"
:value="state.displayName"
@change="(e) => handleDisplayNameChange(e.target.value)"
/>
<Select
v-else
:class="`${prefixCls}__name`"
:disabled="props.disabled"
:allow-clear="props.allowClear"
:placeholder="t('component.localizable_input.resources.localization.placeholder')"
:value="state.displayName"
:options="state.displayNames"
@change="handleDisplayNameChange"
/>
</Col>
</Row>
</InputGroup>
</ItemReset>
</div>
</template>
<script setup lang="ts">
import type { DefaultOptionType } from 'ant-design-vue/lib/select';
import { computed, watch, shallowReactive } from 'vue';
import { Form, Row, Col, Input, Select } from 'ant-design-vue';
import { propTypes } from '/@/utils/propTypes';
import { useI18n } from '/@/hooks/web/useI18n';
import { useDesign } from '/@/hooks/web/useDesign';
import { useLocalizationSerializer } from '/@/hooks/abp/useLocalizationSerializer';
import { useAbpStoreWithOut } from '/@/store/modules/abp';
import { isNullOrWhiteSpace } from '/@/utils/strings';
const ItemReset = Form.ItemRest;
const InputGroup = Input.Group;
interface State {
resourceName?: string,
displayName?: string,
displayNames: DefaultOptionType[],
}
const emits = defineEmits(['update:value', 'change']);
const props = defineProps({
value: propTypes.string,
allowClear: propTypes.bool.def(false),
disabled: propTypes.bool.def(false),
});
const { t } = useI18n();
const abpStore = useAbpStoreWithOut();
const { prefixCls } = useDesign('localizable-input');
const formItemContext = Form.useInjectFormItemContext();
const { values: resources } = abpStore.getApplication.localization;
const { deserialize, serialize } = useLocalizationSerializer();
const state = shallowReactive<State>({
displayNames: [] as any[],
});
const getIsFixed = computed(() => {
return state.resourceName === 'Fixed';
});
const getResources = computed(() => {
const sources = Object.keys(resources).map((key) => {
return {
label: key,
value: key,
}
});
return [
{
label: t('component.localizable_input.resources.fiexed.group'),
value: 'F',
options: [{
label: t('component.localizable_input.resources.fiexed.label'),
value: 'Fixed',
}]
},
{
label: t('component.localizable_input.resources.localization.group'),
value: 'R',
options: sources,
}
];
});
watch(
() => props.value,
(value) => {
const info = deserialize(value);
if (state.resourceName !== info.resourceName) {
state.resourceName = isNullOrWhiteSpace(info.resourceName) ? undefined : info.resourceName;
}
if (state.displayName !== info.name) {
state.displayName = isNullOrWhiteSpace(info.name) ? undefined : info.name;
}
},
{
immediate: true,
}
);
function handleResourceChange(value?: string) {
state.displayNames = [];
if (value && resources[value]) {
state.displayNames = Object.keys(resources[value]).map((key) => {
return {
label: resources[value][key] ? String(resources[value][key]) : key,
value: key,
};
});
}
state.displayName = undefined;
triggerDisplayNameChange(state.displayName);
}
function handleDisplayNameChange(value?: string) {
triggerDisplayNameChange(value);
}
function triggerDisplayNameChange(value?: string) {
const inputValue = value === undefined ? '' : value;
let updateValue = '';
if (getIsFixed.value) {
updateValue = !isNullOrWhiteSpace(value) ? `F:${value}` : 'F:' ;
} else if (!isNullOrWhiteSpace(state.resourceName)) {
const info: LocalizableStringInfo = {
resourceName: state.resourceName ?? '',
name: inputValue,
};
updateValue = serialize(info);
}
emits('change', updateValue);
emits('update:value', updateValue);
formItemContext.onFieldChange();
}
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-localizable-input';
.@{prefix-cls} {
&__resource {
width: 100% !important;
}
&__name {
width: 100% !important;
}
}
</style>

3
apps/vue/src/components/Abp/LocalizableInput/index.ts

@ -0,0 +1,3 @@
import LocalizableInput from './LocalizableInput.vue';
export { LocalizableInput };

9
apps/vue/src/components/Abp/LocalizableInput/props.ts

@ -0,0 +1,9 @@
export interface Props {
resources: string[],
value: string,
}
export interface Resource {
name: string,
value: string,
}

551
apps/vue/src/components/Abp/StringValueType/StringValueTypeInput.vue

@ -0,0 +1,551 @@
<template>
<div style="width: 100%" :class="`${prefixCls}__container`">
<FormItemRest>
<Card>
<template #title>
<div :class="`${prefixCls}__type`">
<Row>
<Col :span="11">
<span>{{ t('component.value_type_nput.type.name') }}</span>
</Col>
<Col :span="2">
<div style="width: 100%"></div>
</Col>
<Col :span="11">
<span>{{ t('component.value_type_nput.validator.name') }}</span>
</Col>
</Row>
<Row>
<Col :span="11">
<Select :value="state.valueType.name" @change="handleValueTypeChange">
<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 style="width: 100%"></div>
</Col>
<Col :span="11">
<Select :value="state.valueType.validator.name" @change="handleValidatorChange">
<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="`${prefixCls}__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 style="width: 100%"></div>
</Col>
<Col :span="11">
<span>{{ t('component.value_type_nput.validator.NUMERIC.maxValue') }}</span>
</Col>
</Row>
<Row>
<Col :span="11">
<InputNumber
style="width: 100%"
v-model:value="(state.valueType.validator as NumericValueValidator).minValue"
/>
</Col>
<Col :span="2">
<div style="width: 100%"></div>
</Col>
<Col :span="11">
<InputNumber
style="width: 100%"
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
style="width: 100%"
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
style="width: 100%"
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 style="width: 100%"></div>
</Col>
<Col :span="11">
<span>{{ t('component.value_type_nput.validator.STRING.maxLength') }}</span>
</Col>
</Row>
<Row>
<Col :span="11">
<InputNumber
style="width: 100%"
v-model:value="(state.valueType.validator as StringValueValidator).minLength"
/>
</Col>
<Col :span="2">
<div style="width: 100%"></div>
</Col>
<Col :span="11">
<InputNumber
style="width: 100%"
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="tableColumns" :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="`${prefixCls}__action`">
<Button v-if="props.allowEdit" type="link" @click="() => handleEdit(record)">
<template #icon>
<EditOutlined />
</template>
{{ t('component.value_type_nput.type.SELECTION.actions.update') }}
</Button>
<Divider v-if="props.allowEdit && props.allowDelete" type="vertical" />
<Button v-if="props.allowDelete" type="link" @click="() => handleDelete(record)" class="ant-btn-error">
<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="`${prefixCls}__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="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 v-model:value="state.selection.form.model.value" />
</FormItem>
</Form>
</Modal>
</div>
</template>
<script setup lang="ts">
import type { ColumnsType } from 'ant-design-vue/lib/table';
import type { RuleObject } from 'ant-design-vue/lib/form';
import { reactive, ref, unref, watch } from 'vue';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
import {
Button,
Card,
Checkbox,
Col,
Divider,
Form,
Input,
InputNumber,
Modal,
Row,
Select,
Table,
} from 'ant-design-vue';
import { useLocalizationSerializer } from '/@/hooks/abp/useLocalizationSerializer';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useDesign } from '/@/hooks/web/useDesign';
import { useI18n } from '/@/hooks/web/useI18n';
import { isNullOrWhiteSpace } from '/@/utils/strings';
import { propTypes } from '/@/utils/propTypes';
import {
AlwaysValidValueValidator,
BooleanValueValidator,
NumericValueValidator,
StringValueValidator,
} from './validator';
import {
StringValueType,
FreeTextStringValueType,
SelectionStringValueType,
ToggleStringValueType,
valueTypeSerializer,
} from './valueType';
import { LocalizableInput } from '../LocalizableInput';
const FormItemRest = Form.ItemRest;
const FormItem = Form.Item;
const Option = Select.Option;
const emits = defineEmits(['change', 'update:value', 'change:valueType', 'change:validator', 'change:selection']);
const props = defineProps({
value: propTypes.string.def('{}'),
allowEdit: propTypes.bool.def(false),
allowDelete: propTypes.bool.def(false),
disabled: propTypes.bool.def(false),
});
const { t } = useI18n();
const { Lr } = useLocalization();
const { deserialize, serialize, validate: validateLocalizer } = useLocalizationSerializer();
const { prefixCls } = useDesign('string-value-type-input');
interface Selection {
modal: {
title?: string,
visible?: boolean,
maskClosable?: boolean,
width?: number,
minHeight?: number,
onOk?: (e: MouseEvent) => void,
onCancel?: (e: MouseEvent) => void,
},
form: {
model: any,
editFlag: boolean,
rules?: Dictionary<string, RuleObject>,
},
}
interface State {
value?: string;
valueType: StringValueType;
selection: Selection;
}
const formRef = ref<any>();
const state = reactive<State>({
valueType: new FreeTextStringValueType(),
selection: {
modal: {
title: t('component.value_type_nput.type.SELECTION.modal.title'),
width: 600,
minHeight: 400,
visible: false,
maskClosable: false,
onOk: handleSubmit,
onCancel: handleCancel,
},
form: {
model: {},
editFlag: false,
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.findIndex(x => serialize(x.displayText) === value) >= 0) {
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.findIndex(x => x.value === value) >= 0) {
return Promise.reject(t('component.value_type_nput.type.SELECTION.duplicateKeyOrValue'));
}
return Promise.resolve();
}
}
},
},
},
});
const tableColumns = reactive<ColumnsType>([{
title: t('component.value_type_nput.type.SELECTION.displayText'),
dataIndex: 'displayText',
key: 'displayText',
align: 'left',
fixed: 'left',
width: 180,
},
{
title: t('component.value_type_nput.type.SELECTION.value'),
dataIndex: 'value',
key: 'value',
align: 'left',
fixed: 'left',
width: 200,
},
{
width: 180,
title: t('component.value_type_nput.type.SELECTION.actions.title'),
align: 'center',
dataIndex: 'action',
key: 'action',
fixed: 'right',
}
]);
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.findIndex((item) => item.value === value) < 0) {
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 'TOGGLE':
case 'ToggleStringValueType':
state.valueType = new ToggleStringValueType();
break;
case 'SELECTION':
case 'SelectionStringValueType':
state.valueType = new SelectionStringValueType();
break;
case 'FREE_TEXT':
case 'FreeTextStringValueType':
default:
state.valueType = new FreeTextStringValueType();
break;
}
}
function handleValidatorChange(validator: string) {
switch (validator) {
case 'NULL':
state.valueType.validator = new AlwaysValidValueValidator();
break;
case 'BOOLEAN':
state.valueType.validator = new BooleanValueValidator();
break;
case 'NUMERIC':
state.valueType.validator = new NumericValueValidator();
break;
case 'STRING':
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) {
state.selection.form.model = {
value: record.value,
displayText: serialize(record.displayText),
};
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) {
items.push({
displayText: deserialize(displayText),
value: state.selection.form.model.value,
});
} else {
const findIndex = items.findIndex(x => serialize(x.displayText) === displayText);
items[findIndex].value = state.selection.form.model.value;
}
form.resetFields();
state.selection.modal.visible = false;
emits('change:selection', items);
});
}
function handleDelete(record) {
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({
validate,
});
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-string-value-type-input';
.@{prefix-cls} {
&__type {
width: 100% !important;
}
&__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>

3
apps/vue/src/components/Abp/StringValueType/index.ts

@ -0,0 +1,3 @@
import StringValueTypeInput from './StringValueTypeInput.vue';
export { StringValueTypeInput };

134
apps/vue/src/components/Abp/StringValueType/validator.ts

@ -0,0 +1,134 @@
import { isBoolean, isNullOrUnDef, isNumber } from "/@/utils/is";
import { isNullOrWhiteSpace } from "/@/utils/strings";
export interface ValueValidator {
name: string;
properties: Dictionary<string, any>;
isValid(value?: any): boolean;
}
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 (value === undefined) 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>;
constructor() {
this.properties = {};
}
get minValue(): number | undefined {
return Number(this.properties['MinValue']);
}
set minValue(value: number) {
this.properties['MinValue'] = value;
}
get maxValue(): number | undefined {
return Number(this.properties['MaxValue']);
}
set maxValue(value: number) {
this.properties['MaxValue'] = value;
}
isValid(value?: any): boolean {
if (!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;
}
_isValidInternal(value: number): boolean {
if (this.minValue && value < this.minValue) return false;
if (this.maxValue && value > this.maxValue) return false;
return true;
}
}
export class StringValueValidator implements ValueValidator {
name = "STRING";
properties: Dictionary<string, any>;
constructor() {
this.properties = {};
}
get allowNull(): boolean {
return String(this.properties['AllowNull'] ?? 'true')?.toLowerCase() === 'true';
}
set allowNull(value: boolean) {
this.properties['AllowNull'] = value;
}
get minLength(): number | undefined {
return Number(this.properties['MinLength']);
}
set minLength(value: number) {
this.properties['MinLength'] = value;
}
get maxLength(): number | undefined {
return Number(this.properties['MaxLength']);
}
set maxLength(value: number) {
this.properties['MaxLength'] = value;
}
get regularExpression(): string {
return String(this.properties['RegularExpression'] ?? '');
}
set regularExpression(value: string) {
this.properties['RegularExpression'] = value
}
isValid(value?: any): boolean {
console.log(value);
console.log(this.allowNull);
if (!this.allowNull && isNullOrUnDef(value)) return false;
const valueString = String(value);
console.log(valueString);
if (!this.allowNull && isNullOrWhiteSpace(valueString.trim())) return false;
console.log(valueString.length);
console.log(this.maxLength);
console.log(this.maxLength);
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 RegExp(this.regularExpression).test(valueString);
}
return true;
}
}

122
apps/vue/src/components/Abp/StringValueType/valueType.ts

@ -0,0 +1,122 @@
import {
ValueValidator,
AlwaysValidValueValidator,
BooleanValueValidator,
NumericValueValidator,
StringValueValidator,
} from "./validator";
export interface StringValueType {
name: string;
properties: Dictionary<string, any>;
validator: ValueValidator;
}
export interface SelectionStringValueItem {
value: string;
displayText: LocalizableStringInfo;
}
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 {
name = "SelectionStringValueType";
properties: Dictionary<string, any>;
validator: ValueValidator;
itemSource: SelectionStringValueItemSource;
constructor(validator?: ValueValidator) {
this.properties = {};
this.itemSource = {
items: [],
};
this.validator = validator ?? new AlwaysValidValueValidator();
}
}
class StringValueTypeSerializer {
serialize(value: StringValueType): string {
// console.log('serialize', value);
const valueTypeString = JSON.stringify(value);
// console.log('deserialize to obj', valueTypeString);
return valueTypeString;
}
deserialize(value: string): StringValueType {
let valueType: StringValueType;
const valueTypeObj = JSON.parse(value);
// console.log('deserialize', value);
// console.log('deserialize to obj', valueTypeObj);
switch (valueTypeObj.name) {
case 'TOGGLE':
case 'ToggleStringValueType':
// console.log('deserialize valueType to TOGGLE', valueTypeObj.name);
valueType = new ToggleStringValueType();
break;
case 'SELECTION':
case 'SelectionStringValueType':
// console.log('deserialize valueType to SELECTION', valueTypeObj.name);
valueType = new SelectionStringValueType();
(valueType as SelectionStringValueType).itemSource = valueTypeObj.itemSource;
break;
default:
case 'FREE_TEXT':
case 'FreeTextStringValueType':
// console.log('deserialize valueType to FREE_TEXT or default', valueTypeObj.name);
valueType = new FreeTextStringValueType();
break;
}
valueType.properties = valueTypeObj.properties;
valueType.validator = this._deserializeValidator(valueTypeObj.validator);
return valueType;
}
_deserializeValidator(validator: any): ValueValidator {
let convertValidator: ValueValidator = new AlwaysValidValueValidator();
if (validator.name) {
switch (validator.name) {
case 'BOOLEAN':
// console.log('deserialize validator to BOOLEAN', validator.name);
convertValidator = new BooleanValueValidator();
break;
case 'NUMERIC':
// console.log('deserialize validator to NUMERIC', validator.name);
convertValidator = new NumericValueValidator();
break;
case 'STRING':
// console.log('deserialize validator to STRING', validator.name);
convertValidator = new StringValueValidator();
break;
case 'NULL':
// console.log('deserialize validator to NULL', validator.name);
convertValidator = new AlwaysValidValueValidator();
break;
}
}
convertValidator.properties = validator.properties;
return convertValidator;
}
}
export const valueTypeSerializer = new StringValueTypeSerializer();

4
apps/vue/src/components/Abp/index.ts

@ -0,0 +1,4 @@
export * from './ExtraPropertyDictionary';
export * from './FeatureModal';
export * from './LocalizableInput';
export * from './StringValueType';

7
apps/vue/src/components/Form/src/componentMap.ts

@ -34,6 +34,10 @@ import { IconPicker } from '/@/components/Icon';
import { CountdownInput } from '/@/components/CountDown'; import { CountdownInput } from '/@/components/CountDown';
import { Input as BInput } from '/@/components/Input'; import { Input as BInput } from '/@/components/Input';
import { CodeEditorX } from '/@/components/CodeEditor'; import { CodeEditorX } from '/@/components/CodeEditor';
import {
ExtraPropertyDictionary,
LocalizableInput
} from '/@/components/Abp';
const componentMap = new Map<ComponentType, Component>(); const componentMap = new Map<ComponentType, Component>();
const customComponentMap = new Map<ComponentType, Component>(); const customComponentMap = new Map<ComponentType, Component>();
@ -76,6 +80,9 @@ componentMap.set('WeekPicker', DatePicker.WeekPicker);
componentMap.set('TimePicker', TimePicker); componentMap.set('TimePicker', TimePicker);
componentMap.set('Divider', Divider); componentMap.set('Divider', Divider);
componentMap.set('ExtraPropertyDictionary', ExtraPropertyDictionary);
componentMap.set('LocalizableInput', LocalizableInput);
customComponentMap.forEach((v, k) => { customComponentMap.forEach((v, k) => {
componentMap.set(k, v); componentMap.set(k, v);
}); });

4
apps/vue/src/components/Form/src/types/index.ts

@ -115,4 +115,6 @@ export type ComponentType =
| 'Rate' | 'Rate'
| 'Divider' | 'Divider'
| 'ApiTransfer' | 'ApiTransfer'
| 'CodeEditorX'; | 'CodeEditorX'
| 'ExtraPropertyDictionary'
| 'LocalizableInput';

34
apps/vue/src/components/Permission/src/PermissionModal.vue

@ -5,23 +5,20 @@
@register="registerModal" @register="registerModal"
:title="getIdentity" :title="getIdentity"
:width="800" :width="800"
:min-height="600" :min-height="400"
:mask-closable="false" :mask-closable="false"
:can-fullscreen="false"
@ok="handleSubmit" @ok="handleSubmit"
@visible-change="handleVisibleChange" @visible-change="handleVisibleChange"
> >
<Row ref="rowRef">
<Col :span="24" ref="preColRef">
<Checkbox <Checkbox
:checked="permissionTreeCheckState.checked" :checked="permissionTreeCheckState.checked"
:indeterminate="permissionTreeCheckState.indeterminate" :indeterminate="permissionTreeCheckState.indeterminate"
@change="handleGrantAllPermission" @change="handleGrantAllPermission"
>{{ L('SelectAllInAllTabs') }}</Checkbox >{{ L('SelectAllInAllTabs') }}</Checkbox
> >
</Col>
<Divider /> <Divider />
<Col :span="24"> <Tabs :class="`${prefixCls}__tabs`" v-model="activeKey" tab-position="left" type="card">
<Tabs v-model="activeKey" tab-position="left" type="card">
<TabPane <TabPane
v-for="permission in permissionTree" v-for="permission in permissionTree"
:key="permission.name" :key="permission.name"
@ -47,21 +44,18 @@
children: 'children', children: 'children',
}" }"
:value="permissionGrantKeys(permission)" :value="permissionGrantKeys(permission)"
@check=" @check="(selectKeys, event) => handlePermissionGranted(permission, selectKeys, event)"
(selectKeys, event) => handlePermissionGranted(permission, selectKeys, event)
"
/> />
</Card> </Card>
</TabPane> </TabPane>
</Tabs> </Tabs>
</Col>
</Row>
</BasicModal> </BasicModal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { Card, Checkbox, Col, Divider, Row, Tabs } from 'ant-design-vue'; import { Card, Checkbox, Divider, Tabs } from 'ant-design-vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { useLocalization } from '/@/hooks/abp/useLocalization'; import { useLocalization } from '/@/hooks/abp/useLocalization';
import { BasicModal, useModalInner } from '/@/components/Modal'; import { BasicModal, useModalInner } from '/@/components/Modal';
@ -78,6 +72,7 @@
identity: '', identity: '',
}; };
const { prefixCls } = useDesign('permission-modal');
const { createMessage } = useMessage(); const { createMessage } = useMessage();
const { L } = useLocalization('AbpPermissionManagement'); const { L } = useLocalization('AbpPermissionManagement');
const activeKey = ref(''); const activeKey = ref('');
@ -125,3 +120,18 @@
}); });
} }
</script> </script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-permission-modal';
.@{prefix-cls} {
&__tabs {
height: 520px;
::v-deep(.ant-tabs-content-holder) {
overflow-y: auto !important;
overflow-x: hidden !important;
}
}
}
</style>

2
apps/vue/src/components/SettingManagement/src/SettingForm.vue

@ -115,7 +115,7 @@
} from 'ant-design-vue'; } from 'ant-design-vue';
import { Input as BInput } from '/@/components/Input'; import { Input as BInput } from '/@/components/Input';
import { formatToDate } from '/@/utils/dateUtil'; import { formatToDate } from '/@/utils/dateUtil';
import { SettingGroup, SettingsUpdate } from '/@/api/settings/model/settingModel'; import { SettingGroup, SettingsUpdate } from '/@/api/settings-management/settings/model';
const CollapsePanel = Collapse.Panel; const CollapsePanel = Collapse.Panel;
const FormItem = Form.Item; const FormItem = Form.Item;

2
apps/vue/src/hooks/abp/useDefineSettings.ts

@ -1,5 +1,5 @@
import { computed, onMounted } from 'vue'; import { computed, onMounted } from 'vue';
import { SettingGroup } from '/@/api/settings/model/settingModel'; import { SettingGroup } from '/@/api/settings-management/settings/model';
import { useSettingManagementStoreWithOut } from '/@/store/modules/settings'; import { useSettingManagementStoreWithOut } from '/@/store/modules/settings';
import { useSettings as useAbpSettings, ISettingProvider } from '/@/hooks/abp/useSettings'; import { useSettings as useAbpSettings, ISettingProvider } from '/@/hooks/abp/useSettings';

32
apps/vue/src/hooks/abp/useFeatures.ts

@ -9,9 +9,10 @@ type FeatureValue = NameValue<string>;
interface IFeatureChecker { interface IFeatureChecker {
/** /**
* *
* @param name * @param featureNames
* @param requiresAll
*/ */
isEnabled(name: string): boolean; isEnabled(featureNames: string | string[], requiresAll?: boolean): boolean;
/** /**
* *
* @param name * @param name
@ -37,13 +38,34 @@ export function useFeatures() {
return getFeatures.value.find((feature) => name === feature.name); return getFeatures.value.find((feature) => name === feature.name);
} }
function _isEnabled(name: string): boolean {
var setting = get(name);
return setting?.value.toLowerCase() === 'true';
}
const featureChecker: IFeatureChecker = { const featureChecker: IFeatureChecker = {
getOrEmpty(name: string) { getOrEmpty(name: string) {
return get(name)?.value ?? ''; return get(name)?.value ?? '';
}, },
isEnabled(name: string) {
var setting = get(name); isEnabled(featureNames: string | string[], requiresAll?: boolean) {
return setting?.value.toLowerCase() === 'true'; if (Array.isArray(featureNames)) {
if (featureNames.length === 0) return true;
if (!requiresAll || requiresAll === true) {
for (let index = 0; index < featureNames.length; index++) {
if (!_isEnabled(featureNames[index])) return false;
}
return true;
}
for (let index = 0; index < featureNames.length; index++) {
if (_isEnabled(featureNames[index])) return true;
}
} else {
return _isEnabled(featureNames);
}
return false;
}, },
}; };

24
apps/vue/src/hooks/abp/useLocalization.ts

@ -1,10 +1,11 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { merge } from 'lodash-es'; import { merge } from 'lodash-es';
import { useAbpStoreWithOut } from '/@/store/modules/abp';
import { format } from '/@/utils/strings'; import { format } from '/@/utils/strings';
import { useAbpStoreWithOut } from '/@/store/modules/abp';
interface IStringLocalizer { interface IStringLocalizer {
L(key: string, args?: Recordable | any[] | undefined): string; L(key: string, args?: Recordable | any[] | undefined): string;
Lr(resource: string, key: string, args?: Recordable | any[] | undefined): string;
} }
export function useLocalization(resourceNames?: string | string[]) { export function useLocalization(resourceNames?: string | string[]) {
@ -29,6 +30,16 @@ export function useLocalization(resourceNames?: string | string[]) {
return resource; return resource;
}); });
const getResourceByName = computed(() => {
return (resource: string): Dictionary<string, string> => {
const abpStore = useAbpStoreWithOut();
const { values } = abpStore.getApplication.localization;
if (Reflect.has(values, resource)) {
return values[resource];
}
return {};
};
});
function L(key: string, args?: Recordable | any[] | undefined) { function L(key: string, args?: Recordable | any[] | undefined) {
if (!key) return ''; if (!key) return '';
@ -37,9 +48,18 @@ export function useLocalization(resourceNames?: string | string[]) {
return format(getResource.value[key], args ?? []); return format(getResource.value[key], args ?? []);
} }
function Lr(resource: string, key: string, args?: Recordable | any[] | undefined) {
if (!key) return '';
const findResource = getResourceByName.value(resource);
if (!findResource) return key;
if (!Reflect.has(findResource, key)) return key;
return format(findResource[key], args ?? []);
}
const localizer: IStringLocalizer = { const localizer: IStringLocalizer = {
L: L, L: L,
Lr: Lr,
}; };
return { L, localizer }; return { L, Lr, localizer };
} }

101
apps/vue/src/hooks/abp/useLocalizationSerializer.ts

@ -0,0 +1,101 @@
import { isNullOrWhiteSpace } from "/@/utils/strings";
interface ValidateOptions {
required?: boolean;
}
interface ILocalizableStringSerializer {
serialize(value?: LocalizableStringInfo): string;
deserialize(value?: string): LocalizableStringInfo;
validate(value?: string, opt?: ValidateOptions): boolean;
}
export function useLocalizationSerializer(): ILocalizableStringSerializer {
function Validate(value?: string, opt?: ValidateOptions): boolean {
if (!value || isNullOrWhiteSpace(value)) {
if (!opt || (opt.required === undefined || opt.required === true)) {
return false;
}
return true;
}
if (value.length < 3 || value[1] !== ':') {
return false;
}
const type = value[0];
switch (type) {
case 'F':
return !isNullOrWhiteSpace(value.substring(2).trim());
case 'L':
const commaPosition = value.indexOf(',', 2);
if (commaPosition == -1) {
return false;
}
const name = value.substring(commaPosition + 1);
if (isNullOrWhiteSpace(name)) {
return false;
}
return true;
default:
return false;
}
}
function Serialize(value?: LocalizableStringInfo): string {
if (!value) return '';
return `L:${value.resourceName},${value.name}`;
}
function Deserialize(value?: string): LocalizableStringInfo {
if (!value || isNullOrWhiteSpace(value)) {
return {
resourceName: '',
name: '',
};
}
if (value.length < 2 || value[1] !== ':') {
return {
resourceName: '',
name: value,
};
}
const type = value[0];
switch (type) {
case 'F':
return {
resourceName: 'Fixed',
name: value.substring(2),
};
case 'L':
const commaPosition = value.indexOf(',', 2);
if (commaPosition == -1) {
return {
resourceName: 'Default',
name: value,
};
}
const resourceName = value.substring(2, commaPosition);
const name = value.substring(commaPosition + 1);
if (isNullOrWhiteSpace(resourceName)) {
return {
resourceName: 'Default',
name: value,
};
}
return {
resourceName: resourceName,
name: name,
};
default:
return {
resourceName: 'Default',
name: value,
};
}
}
return {
validate: Validate,
serialize: Serialize,
deserialize: Deserialize,
}
}

85
apps/vue/src/locales/lang/en/component.ts

@ -150,10 +150,91 @@ export default {
reUploadFailed: 'Re-upload failed files', reUploadFailed: 'Re-upload failed files',
}, },
verify: { verify: {
error: 'verification failed', error: 'verification failed!',
time: 'The verification is successful and it takes {time} seconds', time: 'The verification is successful and it takes {time} seconds!',
redoTip: 'Click the picture to refresh', redoTip: 'Click the picture to refresh',
dragText: 'Hold down the slider and drag', dragText: 'Hold down the slider and drag',
successText: 'Verified', successText: 'Verified',
}, },
localizable_input: {
placeholder: 'Select localized resources',
resources: {
fiexed: {
group: 'Define',
label: 'Fiexed',
placeholder: 'Please enter custom content',
},
localization: {
group: 'Localization',
placeholder: 'Please select a name',
}
}
},
extra_property_dictionary: {
title: 'Extra properties',
key: 'Key',
value: 'Value',
actions: {
title: 'Actions',
create: 'Add',
update: 'Edit',
delete: 'Delete',
clean: 'Clean',
},
validator: {
duplicateKey: 'A key of the same name has been added',
},
},
value_type_nput: {
type: {
name: 'Type',
FREE_TEXT: {
name: 'Free Text',
},
TOGGLE: {
name: 'Toggle',
},
SELECTION: {
name: 'Selection',
displayText: 'Display Text',
displayTextNotBeEmpty: 'Display text cannot be empty',
value: 'value',
duplicateKeyOrValue: 'The name or value of the selection is not allowed to be repeated',
itemsNotBeEmpty: 'Selectable items cannot be empty',
itemsNotFound: 'The selection is not included in the optional list',
actions: {
title: 'Actions',
create: 'Add',
update: 'Edit',
delete: 'Delete',
clean: 'Clean',
},
modal: {
title: 'Selection',
},
},
},
validator: {
name: 'Validator',
isInvalidValue: 'The value failed to pass {0}; check the validator options.',
NULL: {
name: 'None',
},
BOOLEAN: {
name: 'Boolean',
},
NUMERIC: {
name: 'Numeric',
minValue: 'Min Value',
maxValue: 'Max Value',
},
STRING: {
name: 'String',
allowNull: 'Allow Null',
minLength: 'Min Length',
maxLength: 'Max Length',
regularExpression: 'Regular Expression',
},
},
}
}; };

81
apps/vue/src/locales/lang/zh-CN/component.ts

@ -159,4 +159,85 @@ export default {
dragText: '请按住滑块拖动', dragText: '请按住滑块拖动',
successText: '验证通过', successText: '验证通过',
}, },
localizable_input: {
placeholder: '请选择本地化资源',
resources: {
fiexed: {
group: '自定义',
label: '固定内容',
placeholder: '请输入自定义内容',
},
localization: {
group: '本地化',
placeholder: '请选择名称',
}
}
},
extra_property_dictionary: {
title: '扩展属性',
key: '名称',
value: '键值',
actions: {
title: '操作',
create: '新增',
update: '编辑',
delete: '删除',
clean: '清除'
},
validator: {
duplicateKey: '已经添加了一个相同名称的键',
},
},
value_type_nput: {
type: {
name: '类型',
FREE_TEXT: {
name: '自由文本',
},
TOGGLE: {
name: '切换',
},
SELECTION: {
name: '选择',
displayText: '显示名称',
displayTextNotBeEmpty: '显示名称不可为空',
value: '选择项',
duplicateKeyOrValue: '选择项的名称或值不允许重复',
itemsNotBeEmpty: '可选择项列表不能为空',
itemsNotFound: '选择项不包含在可选列表中',
actions: {
title: '操作',
create: '新增',
update: '编辑',
delete: '删除',
clean: '清除',
},
modal: {
title: '选择项',
},
},
},
validator: {
name: '验证器',
isInvalidValue: '值未能通过 {0} 校验, 请检查验证器选项.',
NULL: {
name: '未定义',
},
BOOLEAN: {
name: '布尔类型',
},
NUMERIC: {
name: '数值类型',
minValue: '最小值',
maxValue: '最大值',
},
STRING: {
name: '字符类型',
allowNull: '允许空值',
minLength: '最小长度',
maxLength: '最大长度',
regularExpression: '正则表达式',
},
},
}
}; };

2
apps/vue/src/store/modules/settings.ts

@ -1,7 +1,7 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { store } from '/@/store'; import { store } from '/@/store';
import { createLocalStorage } from '/@/utils/cache'; import { createLocalStorage } from '/@/utils/cache';
import { SettingGroup } from '/@/api/settings/model/settingModel'; import { SettingGroup } from '/@/api/settings-management/settings/model';
const ls = createLocalStorage(); const ls = createLocalStorage();
const SETTING_ID = 'setting-management'; const SETTING_ID = 'setting-management';

23
apps/vue/src/utils/table.ts

@ -0,0 +1,23 @@
import { isDate, isNumber } from "./is";
export function sorter(a: Recordable<any>, b: Recordable<any>, field: string) : number {
if (!a[field] && !b[field]) {
return 0;
}
if (a[field] && !b[field]) {
return 1;
}
if (b[field] && !a[field]) {
return -1;
}
if (isDate(a[field])) {
return a[field] - b[field];
}
if (isNumber(a[field])) {
return a[field] - b[field];
}
if (Array.isArray(a[field])) {
return a[field].length - b[field].length;
}
return String(a[field]).localeCompare(String(b[field]));
}

4
apps/vue/src/views/account/center/Setting.vue

@ -6,8 +6,8 @@
import { useAbpStoreWithOut } from '/@/store/modules/abp'; import { useAbpStoreWithOut } from '/@/store/modules/abp';
import { SettingForm } from '/@/components/SettingManagement'; import { SettingForm } from '/@/components/SettingManagement';
import { SettingGroup } from '/@/api/settings/model/settingModel'; import { SettingGroup } from '/@/api/settings-management/settings/model';
import { getCurrentUserSettings, setCurrentUserSettings } from '/@/api/settings/settings'; import { getCurrentUserSettings, setCurrentUserSettings } from '/@/api/settings-management/settings';
interface ISettingForm { interface ISettingForm {
providerName: string; providerName: string;

433
apps/vue/src/views/feature-management/definitions/features/components/FeatureDefinitionModal.vue

@ -0,0 +1,433 @@
<template>
<BasicModal
@register="registerModal"
:title="L('FeatureDefinitions')"
:can-fullscreen="false"
:width="800"
:height="500"
:close-func="handleBeforeClose"
@ok="handleSubmit"
>
<Form
ref="formRef"
:model="state.entity"
:rules="state.entityRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Tabs v-model:active-key="state.activeTab">
<TabPane key="basic" :tab="L('BasicInfo')">
<FormItem name="groupName" :label="L('DisplayName:GroupName')">
<Select
:disabled="!state.allowedChange"
:allow-clear="true"
v-model:value="state.entity.groupName"
:options="getGroupOptions"
@change="handleGroupChange"
/>
</FormItem>
<FormItem name="parentName" :label="L('DisplayName:ParentName')">
<TreeSelect
:disabled="!state.allowedChange"
:allow-clear="true"
:tree-data="state.availableFeatures"
v-model:value="state.entity.parentName"
:field-names="{
label: 'displayName',
value: 'name',
children: 'children',
}"
@change="handleParentChange"
/>
</FormItem>
<FormItem name="name" :label="L('DisplayName:Name')">
<Input :disabled="state.entityEditFlag && !state.allowedChange" :allow-clear="true" v-model:value="state.entity.name" />
</FormItem>
<FormItem name="displayName" :label="L('DisplayName:DisplayName')">
<LocalizableInput :disabled="!state.allowedChange" :allow-clear="true" v-model:value="state.entity.displayName" />
</FormItem>
<FormItem name="description" :label="L('DisplayName:Description')">
<LocalizableInput :disabled="!state.allowedChange" :allow-clear="true" v-model:value="state.entity.description" />
</FormItem>
<FormItem name="defaultValue" :label="L('DisplayName:DefaultValue')">
<Select
v-if="valueTypeNameRef === 'SelectionStringValueType'"
:disabled="!state.allowedChange"
:allow-clear="true"
v-model:value="state.entity.defaultValue"
:options="state.selectionDataSource"
/>
<TextArea
v-else-if="valueTypeNameRef === 'FreeTextStringValueType' && (validatorNameRef === 'NULL' || validatorNameRef === 'STRING')"
:disabled="state.entityEditFlag && !state.allowedChange"
:allow-clear="true"
:auto-size="{ minRows: 3 }"
v-model:value="state.entity.defaultValue"
/>
<InputNumber
v-else-if="valueTypeNameRef === 'FreeTextStringValueType' && validatorNameRef === 'NUMERIC'"
style="width: 100%;"
:disabled="state.entityEditFlag && !state.allowedChange"
v-model:value="state.entity.defaultValue"
/>
<Checkbox
v-else-if="valueTypeNameRef === 'ToggleStringValueType' && validatorNameRef === 'BOOLEAN'"
:disabled="state.entityEditFlag && !state.allowedChange"
:checked="state.entity.defaultValue === 'true'"
@change="(e) => state.entity.defaultValue = String(e.target.checked).toLowerCase()"
>
{{ L('DisplayName:DefaultValue') }}
</Checkbox>
</FormItem>
<FormItem name="IsVisibleToClients" :label="L('DisplayName:IsVisibleToClients')" :extra="L('Description:IsVisibleToClients')">
<Checkbox
:disabled="!state.allowedChange"
v-model:checked="state.entity.isVisibleToClients"
>{{ L('DisplayName:IsVisibleToClients') }}
</Checkbox>
</FormItem>
<FormItem name="IsAvailableToHost" :label="L('DisplayName:IsAvailableToHost')" :extra="L('Description:IsAvailableToHost')">
<Checkbox
:disabled="!state.allowedChange"
v-model:checked="state.entity.isAvailableToHost"
>{{ L('DisplayName:IsAvailableToHost') }}
</Checkbox>
</FormItem>
</TabPane>
<TabPane key="valueType" :tab="L('ValueValidator')" force-render>
<FormItem name="valueType" label="" :label-col="{ span: 0 }" :wrapper-col="{ span: 24 }">
<StringValueTypeInput
ref="valueTypeRef"
:disabled="!state.allowedChange"
:allow-delete="true"
:allow-edit="true"
v-model:value="state.entity.valueType"
@change:value-type="handleValueTypeNameChange"
@change:validator="handleValidatorNameChange"
@change:selection="handleSelectionChange"
/>
</FormItem>
</TabPane>
<TabPane key="propertites" :tab="L('Properties')" force-render>
<FormItem name="extraProperties" label="" :label-col="{ span: 0 }" :wrapper-col="{ span: 24 }">
<ExtraPropertyDictionary :disabled="!state.allowedChange" :allow-delete="true" :allow-edit="true" v-model:value="state.entity.extraProperties" />
</FormItem>
</TabPane>
</Tabs>
</Form>
</BasicModal>
</template>
<script setup lang="ts">
import type { Rule } from 'ant-design-vue/lib/form';
import { cloneDeep } from 'lodash-es';
import { computed, ref, reactive, unref, nextTick, watch } from 'vue';
import { Checkbox, Form, Input, InputNumber, Select, Tabs, TreeSelect } from 'ant-design-vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { LocalizableInput, ExtraPropertyDictionary, StringValueTypeInput } from '/@/components/Abp';
import { FreeTextStringValueType, SelectionStringValueItem, valueTypeSerializer } from '/@/components/Abp/StringValueType/valueType';
import { useMessage } from '/@/hooks/web/useMessage';
import { ValidationEnum, useValidation } from '/@/hooks/abp/useValidation';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useLocalizationSerializer } from '/@/hooks/abp/useLocalizationSerializer';
import {
GetAsyncByName,
CreateAsyncByInput,
UpdateAsyncByNameAndInput
} from '/@/api/feature-management/definitions/features';
import {
FeatureDefinitionUpdateDto,
FeatureDefinitionCreateDto
} from '/@/api/feature-management/definitions/features/model';
import { GetListAsyncByInput } from '/@/api/feature-management/definitions/features';
import { FeatureGroupDefinitionDto } from '/@/api/feature-management/definitions/groups/model';
import { listToTree } from '/@/utils/helper/treeHelper';
import { isBoolean, isNullOrUnDef } from '/@/utils/is';
import { groupBy } from '/@/utils/array';
const FormItem = Form.Item;
const TabPane = Tabs.TabPane;
const TextArea = Input.TextArea;
interface FeatureSelectData {
label: string;
value: string;
}
interface FeatureTreeData {
name: string;
groupName: string;
displayName: string;
children: FeatureTreeData[];
}
interface State {
activeTab: string,
allowedChange: boolean,
entity: Recordable,
entityRules?: Dictionary<string, Rule>,
entityChanged: boolean,
entityEditFlag: boolean,
defaultGroup?: string,
availableFeatures: FeatureTreeData[],
availableGroups: FeatureGroupDefinitionDto[],
selectionDataSource: FeatureSelectData[],
}
const emits = defineEmits(['register', 'change']);
const { ruleCreator } = useValidation();
const { deserialize, validate } = useLocalizationSerializer();
const { createConfirm, createMessage } = useMessage();
const { L, Lr } = useLocalization(['AbpFeatureManagement', 'AbpUi']);
const formRef = ref<any>();
const valueTypeRef = ref<any>();
const validatorNameRef = ref<string>('NULL');
const valueTypeNameRef = ref<string>('FreeTextStringValueType');
const state = reactive<State>({
activeTab: 'basic',
entity: {},
allowedChange: false,
entityChanged: false,
entityEditFlag: false,
availableFeatures: [],
availableGroups: [],
selectionDataSource: [],
entityRules: {
groupName: ruleCreator.fieldRequired({
name: 'GroupName',
prefix: 'DisplayName',
resourceName: 'WebhooksManagement',
trigger: 'blur',
}),
name: ruleCreator.fieldRequired({
name: 'Name',
prefix: 'DisplayName',
resourceName: 'WebhooksManagement',
trigger: 'blur',
}),
displayName: ruleCreator.defineValidator({
required: true,
trigger: 'blur',
validator(_rule, value) {
if (!validate(value)) {
return Promise.reject(L(ValidationEnum.FieldRequired, [L('DisplayName:DisplayName')]));
}
return Promise.resolve();
},
}),
description: ruleCreator.defineValidator({
trigger: 'blur',
validator(_rule, value) {
if (!validate(value, { required: false })) {
return Promise.reject(L(ValidationEnum.FieldRequired, [L('DisplayName:Description')]));
}
return Promise.resolve();
},
}),
defaultValue: ruleCreator.defineValidator({
trigger: 'blur',
validator(_rule, value) {
const valueType = unref(valueTypeRef);
if (valueType) {
return valueType.validate(value);
}
return Promise.resolve();
},
}),
},
});
const getGroupOptions = computed(() => {
return state.availableGroups.map((group) => {
const info = deserialize(group.displayName);
return {
label: Lr(info.resourceName, info.name),
value: group.name,
};
});
});
watch(
() => state.entity,
() => {
state.entityChanged = true;
},
{
deep: true,
},
);
const [registerModal, { closeModal, changeLoading, changeOkLoading }] = useModalInner((data) => {
state.defaultGroup = data.groupName;
state.availableGroups = data.groups;
nextTick(() => {
fetchFeatures(state.defaultGroup);
fetch(data.record?.name);
});
});
function fetch(name?: string) {
state.activeTab = 'basic';
state.entityEditFlag = false;
if (!name) {
state.entity = {
isVisibleToClients: true,
isAvailableToHost: true,
groupName: state.defaultGroup,
requiredFeatures: [],
valueType: valueTypeSerializer.serialize(new FreeTextStringValueType())
};
state.allowedChange = true;
nextTick(() => state.entityChanged = false);
return;
}
changeLoading(true);
changeOkLoading(true);
GetAsyncByName(name).then((record) => {
state.entity = record;
state.entityEditFlag = true;
state.allowedChange = !record.isStatic;
}).finally(() => {
changeLoading(false);
changeOkLoading(false);
nextTick(() => state.entityChanged = false);
});
}
function fetchFeatures(groupName?: string) {
GetListAsyncByInput({
groupName: groupName,
}).then((res) => {
const featureGroup = groupBy(res.items, 'groupName');
const featureTreeData: FeatureTreeData[] = [];
Object.keys(featureGroup).forEach((gk) => {
const featureTree = listToTree(featureGroup[gk], {
id: 'name',
pid: 'parentName',
});
formatDisplayName(featureTree);
featureTreeData.push(...featureTree);
});
state.availableFeatures = featureTreeData;
});
}
function formatDisplayName(list: any[]) {
if (list && Array.isArray(list)) {
list.forEach((item) => {
if (Reflect.has(item, 'displayName')) {
const info = deserialize(item.displayName);
item.displayName = Lr(info.resourceName, info.name);
}
if (Reflect.has(item, 'children')) {
formatDisplayName(item.children);
}
});
}
}
function handleGroupChange(value?: string) {
state.entity.parentName = undefined;
fetchFeatures(value);
}
function handleParentChange(value?: string) {
if (!value) return;
GetAsyncByName(value).then((res) => {
state.entity.groupName = res.groupName;
const form = unref(formRef);
form?.clearValidate(['groupName']);
});
}
function handleValueTypeNameChange(valueTypeName: string) {
valueTypeNameRef.value = valueTypeName;
switch (valueTypeName) {
case 'ToggleStringValueType':
if (!isBoolean(state.entity.defaultValue)) {
state.entity.defaultValue = false;
}
break;
default:
if (isBoolean(state.entity.defaultValue)) {
state.entity.defaultValue = undefined;
}
break;
}
}
function handleValidatorNameChange(validatorName: string) {
validatorNameRef.value = validatorName;
}
function handleSelectionChange(items: SelectionStringValueItem[]) {
if (items.length === 0) {
state.entity.defaultValue = undefined;
state.selectionDataSource = [];
return;
}
state.selectionDataSource = items.map((item) => {
return {
label: Lr(item.displayText.resourceName, item.displayText.name),
value: item.value,
};
});
if (!items.find(item => item.value === state.entity.defaultValue)) {
state.entity.defaultValue = undefined;
}
}
function handleBeforeClose(): Promise<boolean> {
return new Promise((resolve) => {
if (!state.entityChanged) {
const form = unref(formRef);
form?.resetFields();
return resolve(true);
}
createConfirm({
iconType: 'warning',
title: L('AreYouSure'),
content: L('AreYouSureYouWantToCancelEditingWarningMessage'),
onOk: () => {
const form = unref(formRef);
form?.resetFields();
state.allowedChange = false;
resolve(true);
},
onCancel: () => {
resolve(false);
},
afterClose: () => {
state.defaultGroup = undefined;
},
});
});
}
function handleSubmit() {
if (!state.allowedChange) {
closeModal();
return;
}
const form = unref(formRef);
form?.validate().then(() => {
changeOkLoading(true);
const input = cloneDeep(state.entity);
if (!isNullOrUnDef(input.defaultValue)) {
input.defaultValue = String(input.defaultValue);
}
const api = state.entityEditFlag
? UpdateAsyncByNameAndInput(state.entity.name, input as FeatureDefinitionUpdateDto)
: CreateAsyncByInput(input as FeatureDefinitionCreateDto);
api.then((res) => {
createMessage.success(L('Successful'));
emits('change', res);
form.resetFields();
closeModal();
}).finally(() => {
changeOkLoading(false);
})
});
}
</script>
<style scoped>
</style>

268
apps/vue/src/views/feature-management/definitions/features/components/FeatureDefinitionTable.vue

@ -0,0 +1,268 @@
<template>
<div>
<BasicTable @register="registerTable">
<template #form-groupName="{ model, field }">
<FormItem name="groupName">
<Select v-model:value="model[field]" :options="getGroupOptions" />
</FormItem>
</template>
<template #toolbar>
<Button
v-auth="['FeatureManagement.Definitions.Create']"
type="primary"
@click="handleAddNew"
>
{{ L('FeatureDefinitions:AddNew') }}
</Button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'displayName'">
<span>{{ getGroupDisplayName(record.displayName) }}</span>
</template>
</template>
<template #expandedRowRender="{ record }">
<BasicTable @register="registerSubTable" :data-source="record.features">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'groupName'">
<span>{{ getGroupDisplayName(record.groupName) }}</span>
</template>
<template v-else-if="column.key === 'parentName'">
<span>{{ getDisplayName(record.parentName) }}</span>
</template>
<template v-else-if="column.key === 'displayName'">
<span>{{ getDisplayName(record.displayName) }}</span>
</template>
<template v-else-if="column.key === 'description'">
<span>{{ getDisplayName(record.description) }}</span>
</template>
<template v-else-if="column.key === 'isVisibleToClients'">
<CheckOutlined v-if="record.isVisibleToClients" class="enable" />
<CloseOutlined v-else class="disable" />
</template>
<template v-else-if="column.key === 'isAvailableToHost'">
<CheckOutlined v-if="record.isAvailableToHost" class="enable" />
<CloseOutlined v-else class="disable" />
</template>
<template v-else-if="column.key === 'isStatic'">
<CheckOutlined v-if="record.isStatic" class="enable" />
<CloseOutlined v-else class="disable" />
</template>
<template v-else-if="column.key === 'action'">
<TableAction
:stop-button-propagation="true"
:actions="[
{
auth: 'FeatureManagement.Definitions.Update',
label: L('Edit'),
icon: 'ant-design:edit-outlined',
onClick: handleEdit.bind(null, record),
},
{
auth: 'FeatureManagement.Definitions.Delete',
label: L('Delete'),
color: 'error',
icon: 'ant-design:delete-outlined',
ifShow: !record.isStatic,
onClick: handleDelete.bind(null, record),
},
]"
/>
</template>
</template>
</BasicTable>
</template>
</BasicTable>
<FeatureDefinitionModal @register="registerModal" @change="fetch" />
</div>
</template>
<script lang="ts" setup>
import { cloneDeep } from 'lodash-es';
import { computed, reactive, onMounted } from 'vue';
import { Button, Form, Select } from 'ant-design-vue';
import { CheckOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { BasicTable, TableAction, useTable } from '/@/components/Table';
import { getDataColumns } from '../datas/TableData';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useLocalizationSerializer } from '/@/hooks/abp/useLocalizationSerializer';
import { GetListAsyncByInput as getGroupDefinitions } from '/@/api/feature-management/definitions/groups';
import { FeatureGroupDefinitionDto } from '/@/api/feature-management/definitions/groups/model';
import { GetListAsyncByInput, DeleteAsyncByName } from '/@/api/feature-management/definitions/features';
import { getSearchFormSchemas } from '../datas/ModalData';
import { listToTree } from '/@/utils/helper/treeHelper';
import { groupBy } from '/@/utils/array';
import { sorter } from '/@/utils/table';
import FeatureDefinitionModal from './FeatureDefinitionModal.vue';
const FormItem = Form.Item;
interface Feature {
name: string;
groupName: string;
displayName: string;
parentName?: string;
description?: string;
defaultValue?: string;
valueType: string;
isStatic: boolean;
isVisibleToClients: boolean;
isAvailableToHost: boolean;
allowedProviders: string[];
children: Feature[];
}
interface FeatureGroup {
name: string;
displayName: string;
features: Feature[];
}
interface State {
groups: FeatureGroupDefinitionDto[];
}
const { deserialize } = useLocalizationSerializer();
const { L, Lr } = useLocalization(['AbpFeatureManagement', 'AbpUi']);
const { createConfirm, createMessage } = useMessage();
const [registerModal, { openModal }] = useModal();
const [registerTable, { setLoading, getForm, setTableData }] = useTable({
rowKey: 'name',
title: L('FeatureDefinitions'),
columns: [
{
title: L('DisplayName:DisplayName'),
dataIndex: 'displayName',
align: 'left',
width: 180,
resizable: true,
sorter: (a, b) => sorter(a, b, 'displayName'),
},
],
pagination: true,
striped: false,
useSearchForm: true,
showIndexColumn: false,
showTableSetting: true,
tableSetting: {
redo: false,
},
formConfig: {
labelWidth: 100,
submitFunc: fetch,
schemas: getSearchFormSchemas(),
},
bordered: true,
canResize: true,
immediate: false,
});
const [registerSubTable] = useTable({
rowKey: 'name',
columns: getDataColumns(),
pagination: false,
striped: false,
useSearchForm: false,
showIndexColumn: true,
immediate: false,
scroll: { x: 2000, y: 900 },
actionColumn: {
width: 150,
title: L('Actions'),
dataIndex: 'action',
},
});
const state = reactive<State>({
groups: [],
});
const getGroupOptions = computed(() => {
return state.groups.map((group) => {
const info = deserialize(group.displayName);
return {
label: Lr(info.resourceName, info.name),
value: group.name,
};
});
});
const getGroupDisplayName = computed(() => {
return (groupName: string) => {
const group = state.groups.find(x => x.name === groupName);
if (!group) return groupName;
const info = deserialize(group.displayName);
return Lr(info.resourceName, info.name);
};
});
const getDisplayName = computed(() => {
return (displayName?: string) => {
if (!displayName) return displayName;
const info = deserialize(displayName);
return Lr(info.resourceName, info.name);
};
});
onMounted(() => {
fetch();
fetchGroups();
});
function fetch() {
const form = getForm();
return form.validate().then(() => {
setLoading(true);
setTableData([]);
var input = form.getFieldsValue();
GetListAsyncByInput(input).then((res) => {
const featureGroup = groupBy(res.items, 'groupName');
const featureGroupData: FeatureGroup[] = [];
Object.keys(featureGroup).forEach((gk) => {
const groupData: FeatureGroup = {
name: gk,
displayName: gk,
features: [],
};
const featureTree = listToTree(featureGroup[gk], {
id: 'name',
pid: 'parentName',
});
featureTree.forEach((tk) => {
groupData.features.push(tk);
});
featureGroupData.push(groupData);
});
setTableData(featureGroupData);
}).finally(() => {
setLoading(false);
});
});
}
function fetchGroups() {
getGroupDefinitions({}).then((res) => {
state.groups = res.items;
});
}
function handleAddNew() {
openModal(true, {
groups: cloneDeep(state.groups),
});
}
function handleEdit(record) {
openModal(true, {
record: record,
groups: cloneDeep(state.groups),
});
}
function handleDelete(record) {
createConfirm({
iconType: 'warning',
title: L('AreYouSure'),
content: L('ItemWillBeDeleteOrRestoreMessage'),
onOk: () => {
return DeleteAsyncByName(record.name).then(() => {
createMessage.success(L('Successful'));
fetch();
});
},
});
}
</script>

22
apps/vue/src/views/feature-management/definitions/features/datas/ModalData.ts

@ -0,0 +1,22 @@
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { FormSchema } from '/@/components/Form';
const { L } = useLocalization(['AbpFeatureManagement', 'AbpUi']);
export function getSearchFormSchemas():FormSchema[] {
return [
{
field: 'groupName',
component: 'ApiSelect',
label: L('DisplayName:GroupName'),
colProps: { span: 6 },
slot: 'groupName',
},
{
field: 'filter',
component: 'Input',
label: L('Search'),
colProps: { span: 18 },
},
];
}

69
apps/vue/src/views/feature-management/definitions/features/datas/TableData.ts

@ -0,0 +1,69 @@
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { BasicColumn } from '/@/components/Table';
import { sorter } from '/@/utils/table';
const { L } = useLocalization(['AbpFeatureManagement']);
export function getDataColumns(): BasicColumn[] {
return [
{
title: L('DisplayName:Name'),
dataIndex: 'name',
align: 'left',
width: 180,
resizable: true,
sorter: (a, b) => sorter(a, b, 'name'),
},
{
title: L('DisplayName:DisplayName'),
dataIndex: 'displayName',
align: 'left',
width: 180,
resizable: true,
sorter: (a, b) => sorter(a, b, 'displayName'),
},
{
title: L('DisplayName:Description'),
dataIndex: 'description',
align: 'left',
width: 250,
resizable: true,
sorter: (a, b) => sorter(a, b, 'description'),
},
{
title: L('DisplayName:DefaultValue'),
dataIndex: 'defaultValue',
align: 'left',
width: 150,
resizable: true,
sorter: (a, b) => sorter(a, b, 'defaultValue'),
},
{
title: L('DisplayName:IsVisibleToClients'),
dataIndex: 'isVisibleToClients',
align: 'center',
width: 150,
resizable: true,
defaultHidden: true,
sorter: (a, b) => sorter(a, b, 'isVisibleToClients'),
},
{
title: L('DisplayName:IsAvailableToHost'),
dataIndex: 'isAvailableToHost',
align: 'center',
width: 150,
resizable: true,
defaultHidden: true,
sorter: (a, b) => sorter(a, b, 'isAvailableToHost'),
},
{
title: L('DisplayName:IsStatic'),
dataIndex: 'isStatic',
align: 'center',
width: 150,
resizable: true,
defaultHidden: true,
sorter: (a, b) => sorter(a, b, 'isStatic'),
},
];
}

16
apps/vue/src/views/feature-management/definitions/features/index.vue

@ -0,0 +1,16 @@
<template>
<FeatureDefinitionTable />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import FeatureDefinitionTable from './components/FeatureDefinitionTable.vue';
export default defineComponent({
name: 'FeaturesDefinitions',
components: {
FeatureDefinitionTable,
},
setup() {},
});
</script>

199
apps/vue/src/views/feature-management/definitions/groups/components/GroupDefinitionModal.vue

@ -0,0 +1,199 @@
<template>
<BasicModal
@register="registerModal"
:title="L('GroupDefinitions')"
:can-fullscreen="false"
:width="800"
:height="500"
:close-func="handleBeforeClose"
@ok="handleSubmit"
>
<Form
ref="formRef"
:model="state.entity"
:rules="state.entityRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Tabs v-model:active-key="state.activeTab">
<TabPane key="basic" :tab="L('BasicInfo')">
<FormItem name="name" :label="L('DisplayName:Name')">
<Input :disabled="state.entityEditFlag && !state.allowedChange" :allow-clear="true" v-model:value="state.entity.name" />
</FormItem>
<FormItem name="displayName" :label="L('DisplayName:DisplayName')">
<LocalizableInput :disabled="!state.allowedChange" :allow-clear="true" v-model:value="state.entity.displayName" />
</FormItem>
</TabPane>
<TabPane key="propertites" :tab="L('Properties')">
<FormItem name="extraProperties" label="" :label-col="{ span: 0 }" :wrapper-col="{ span: 24 }">
<ExtraPropertyDictionary :disabled="!state.allowedChange" :allow-delete="true" :allow-edit="true" v-model:value="state.entity.extraProperties" />
</FormItem>
</TabPane>
</Tabs>
</Form>
</BasicModal>
</template>
<script setup lang="ts">
import type { Rule } from 'ant-design-vue/lib/form';
import { cloneDeep } from 'lodash-es';
import { ref, reactive, unref, nextTick, watch } from 'vue';
import { Form, Input, Tabs } from 'ant-design-vue';
import { ExtraPropertyDictionary } from '/@/components/Abp';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { LocalizableInput } from '/@/components/Abp';
import { useMessage } from '/@/hooks/web/useMessage';
import { ValidationEnum, useValidation } from '/@/hooks/abp/useValidation';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useLocalizationSerializer } from '/@/hooks/abp/useLocalizationSerializer';
import {
GetAsyncByName,
CreateAsyncByInput,
UpdateAsyncByNameAndInput
} from '/@/api/feature-management/definitions/groups';
import {
FeatureGroupDefinitionUpdateDto,
FeatureGroupDefinitionCreateDto
} from '/@/api/feature-management/definitions/groups/model';
const FormItem = Form.Item;
const TabPane = Tabs.TabPane;
interface State {
activeTab: string,
allowedChange: boolean,
entity: Recordable,
entityRules?: Dictionary<string, Rule>,
entityChanged: boolean,
entityEditFlag: boolean,
}
const emits = defineEmits(['register', 'change']);
const { ruleCreator } = useValidation();
const { validate } = useLocalizationSerializer();
const { createConfirm, createMessage } = useMessage();
const { L } = useLocalization(['AbpFeatureManagement', 'AbpUi']);
const formRef = ref<any>();
const state = reactive<State>({
activeTab: 'basic',
entity: {},
allowedChange: false,
entityChanged: false,
entityEditFlag: false,
entityRules: {
name: ruleCreator.fieldRequired({
name: 'Name',
prefix: 'DisplayName',
resourceName: 'AbpFeatureManagement',
trigger: 'blur',
}),
displayName: ruleCreator.defineValidator({
required: true,
trigger: 'blur',
validator(_rule, value) {
if (!validate(value)) {
return Promise.reject(L(ValidationEnum.FieldRequired, [L('DisplayName:DisplayName')]));
}
return Promise.resolve();
},
}),
description: ruleCreator.defineValidator({
trigger: 'blur',
validator(_rule, value) {
if (!validate(value, { required: false })) {
return Promise.reject(L(ValidationEnum.FieldRequired, [L('DisplayName:Description')]));
}
return Promise.resolve();
},
}),
},
});
watch(
() => state.entity,
() => {
state.entityChanged = true;
},
{
deep: true,
},
);
const [registerModal, { closeModal, changeLoading, changeOkLoading }] = useModalInner((record) => {
nextTick(() => fetch(record.name));
});
function fetch(name?: string) {
state.activeTab = 'basic';
state.entityEditFlag = false;
if (!name) {
state.entity = {};
state.allowedChange = true;
nextTick(() => state.entityChanged = false);
return;
}
changeLoading(true);
changeOkLoading(true);
GetAsyncByName(name).then((record) => {
state.entity = record;
state.entityEditFlag = true;
state.allowedChange = !record.isStatic;
}).finally(() => {
changeLoading(false);
changeOkLoading(false);
nextTick(() => state.entityChanged = false);
});
}
function handleBeforeClose(): Promise<boolean> {
return new Promise((resolve) => {
if (!state.entityChanged) {
const form = unref(formRef);
form?.resetFields();
return resolve(true);
}
createConfirm({
iconType: 'warning',
title: L('AreYouSure'),
content: L('AreYouSureYouWantToCancelEditingWarningMessage'),
onOk: () => {
const form = unref(formRef);
form?.resetFields();
resolve(true);
},
onCancel: () => {
resolve(false);
},
afterClose: () => {
state.allowedChange = false;
},
});
});
}
function handleSubmit() {
if (!state.allowedChange) {
closeModal();
return;
}
const form = unref(formRef);
form?.validate().then(() => {
changeOkLoading(true);
const api = state.entityEditFlag
? UpdateAsyncByNameAndInput(state.entity.name, cloneDeep(state.entity) as FeatureGroupDefinitionUpdateDto)
: CreateAsyncByInput(cloneDeep(state.entity) as FeatureGroupDefinitionCreateDto);
api.then((res) => {
createMessage.success(L('Successful'));
emits('change', res);
form.resetFields();
closeModal();
}).finally(() => {
changeOkLoading(false);
})
});
}
</script>
<style scoped>
</style>

159
apps/vue/src/views/feature-management/definitions/groups/components/GroupDefinitionTable.vue

@ -0,0 +1,159 @@
<template>
<div>
<BasicTable @register="registerTable">
<template #toolbar>
<Button
v-auth="['FeatureManagement.GroupDefinitions.Create']"
type="primary"
@click="handleAddNew"
>
{{ L('GroupDefinitions:AddNew') }}
</Button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'displayName'">
<span>{{ getDisplayName(record.displayName) }}</span>
</template>
<template v-else-if="column.key === 'action'">
<TableAction
:stop-button-propagation="true"
:actions="[
{
auth: 'FeatureManagement.GroupDefinitions.Update',
label: L('Edit'),
icon: 'ant-design:edit-outlined',
onClick: handleEdit.bind(null, record),
},
{
auth: 'FeatureManagement.GroupDefinitions.Delete',
label: L('Delete'),
color: 'error',
icon: 'ant-design:delete-outlined',
ifShow: !record.isStatic,
onClick: handleDelete.bind(null, record),
},
]"
:dropDownActions="[
{
auth: 'FeatureManagement.Definitions.Create',
label: L('FeatureDefinitions:AddNew'),
icon: 'ant-design:edit-outlined',
onClick: handleAddFeature.bind(null, record),
},
]"
/>
</template>
</template>
</BasicTable>
<GroupDefinitionModal @register="registerModal" @change="fetch" />
<FeatureDefinitionModal @register="registerFeatureModal" />
</div>
</template>
<script lang="ts" setup>
import { cloneDeep } from 'lodash-es';
import { computed, reactive, onMounted } from 'vue';
import { Button } from 'ant-design-vue';
import { BasicTable, TableAction, useTable } from '/@/components/Table';
import { getDataColumns } from '../datas/TableData';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useLocalizationSerializer } from '/@/hooks/abp/useLocalizationSerializer';
import { FeatureGroupDefinitionDto } from '/@/api/feature-management/definitions/groups/model';
import { GetListAsyncByInput, DeleteAsyncByName } from '/@/api/feature-management/definitions/groups';
import { getSearchFormSchemas } from '../datas/ModalData';
import GroupDefinitionModal from './GroupDefinitionModal.vue';
import FeatureDefinitionModal from '../../features/components/FeatureDefinitionModal.vue';
interface State {
groups: FeatureGroupDefinitionDto[];
}
const state = reactive<State>({
groups: [],
});
const { deserialize } = useLocalizationSerializer();
const { L, Lr } = useLocalization(['AbpFeatureManagement', 'AbpUi']);
const { createConfirm, createMessage } = useMessage();
const [registerModal, { openModal }] = useModal();
const [registerFeatureModal, { openModal: openFeatureModal }] = useModal();
const [registerTable, { setLoading, getForm, setTableData }] = useTable({
rowKey: 'name',
title: L('GroupDefinitions'),
columns: getDataColumns(),
pagination: true,
striped: false,
useSearchForm: true,
showIndexColumn: false,
showTableSetting: true,
tableSetting: {
redo: false,
},
formConfig: {
labelWidth: 100,
submitFunc: fetch,
schemas: getSearchFormSchemas(),
},
bordered: true,
canResize: true,
immediate: false,
actionColumn: {
width: 120,
title: L('Actions'),
dataIndex: 'action',
},
});
const getDisplayName = computed(() => {
return (displayName?: string) => {
if (!displayName) return displayName;
const info = deserialize(displayName);
return Lr(info.resourceName, info.name);
};
});
onMounted(fetch);
function fetch() {
const form = getForm();
return form.validate().then(() => {
setLoading(true);
state.groups = [];
var input = form.getFieldsValue();
GetListAsyncByInput(input).then((res) => {
state.groups = res.items;
}).finally(() => {
setTableData(state.groups);
setLoading(false);
});
});
}
function handleAddNew() {
openModal(true, {});
}
function handleEdit(record) {
openModal(true, record);
}
function handleAddFeature(record) {
openFeatureModal(true, {
groupName: record.name,
groups: cloneDeep(state.groups),
});
}
function handleDelete(record) {
createConfirm({
iconType: 'warning',
title: L('AreYouSure'),
content: L('ItemWillBeDeleteOrRestoreMessage'),
onOk: () => {
return DeleteAsyncByName(record.name).then(() => {
createMessage.success(L('Successful'));
fetch();
});
},
});
}
</script>

15
apps/vue/src/views/feature-management/definitions/groups/datas/ModalData.ts

@ -0,0 +1,15 @@
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { FormSchema } from '/@/components/Form';
const { L } = useLocalization(['AbpUi']);
export function getSearchFormSchemas():FormSchema[] {
return [
{
field: 'filter',
component: 'Input',
label: L('Search'),
colProps: { span: 24 },
},
];
}

35
apps/vue/src/views/feature-management/definitions/groups/datas/TableData.ts

@ -0,0 +1,35 @@
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { BasicColumn } from '/@/components/Table';
import { sorter } from '/@/utils/table';
const { L } = useLocalization(['AbpFeatureManagement']);
export function getDataColumns(): BasicColumn[] {
return [
{
title: L('DisplayName:Name'),
dataIndex: 'name',
align: 'left',
width: 350,
resizable: true,
sorter: (a, b) => sorter(a, b, 'name'),
},
{
title: L('DisplayName:DisplayName'),
dataIndex: 'displayName',
align: 'left',
width: 180,
resizable: true,
sorter: (a, b) => sorter(a, b, 'displayName'),
},
{
title: L('DisplayName:IsStatic'),
dataIndex: 'isStatic',
align: 'center',
width: 150,
resizable: true,
defaultHidden: true,
sorter: (a, b) => sorter(a, b, 'isStatic'),
},
];
}

16
apps/vue/src/views/feature-management/definitions/groups/index.vue

@ -0,0 +1,16 @@
<template>
<GroupDefinitionTable />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import GroupDefinitionTable from './components/GroupDefinitionTable.vue';
export default defineComponent({
name: 'FeatruesGroupDefinitions',
components: {
GroupDefinitionTable,
},
setup() {},
});
</script>

145
apps/vue/src/views/feature/hooks/useFeature.ts

@ -1,145 +0,0 @@
import type { Ref } from 'vue';
import { watch, ref, unref } from 'vue';
import { message } from 'ant-design-vue';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useValidation } from '/@/hooks/abp/useValidation';
import { FeatureGroup } from '/@/api/feature/model/featureModel';
import { get, update } from '/@/api/feature/feature';
import { ReturnInnerMethods } from '/@/components/Modal';
interface UseFeature {
providerName: Ref<string>;
providerKey: Ref<string | null>;
formRel: Ref<any>;
modalMethods: ReturnInnerMethods;
}
export function useFeature({ providerName, providerKey, formRel, modalMethods }: UseFeature) {
const { L } = useLocalization('AbpFeatureManagement');
const { ruleCreator } = useValidation();
const featureGroup = ref<{ groups: FeatureGroup[] }>({
groups: [],
});
const featureGroupKey = ref(0);
watch(
() => unref(providerKey),
(key) => {
if (key !== undefined) {
const form = unref(formRel);
form.resetFields();
onGroupChange(0);
get({
providerName: unref(providerName),
providerKey: key,
}).then((res) => {
featureGroup.value.groups = mapFeatures(res.groups);
});
}
},
);
function getFeatures(groups: FeatureGroup[]) {
const features: { name: string; value: string }[] = [];
groups.forEach((g) => {
g.features.forEach((f) => {
if (f.value !== null) {
features.push({
name: f.name,
value: String(f.value),
});
}
});
});
return features;
}
function mapFeatures(groups: FeatureGroup[]) {
groups.forEach((g) => {
g.features.forEach((f) => {
switch (f.valueType?.validator.name) {
case 'BOOLEAN':
f.value = String(f.value).toLocaleLowerCase() === 'true';
break;
case 'NUMERIC':
f.value = Number(f.value);
break;
}
});
});
return groups;
}
function validator(field: string, validator: Validator) {
const featureRules: { [key: string]: any }[] = new Array<{ [key: string]: any }>();
if (validator.properties) {
switch (validator.name) {
case 'NUMERIC':
featureRules.push(
...ruleCreator.fieldMustBeetWeen({
name: field,
start: Number(validator.properties.MinValue),
end: Number(validator.properties.MaxValue),
trigger: 'change',
}),
);
break;
case 'STRING':
if (
validator.properties.AllowNull &&
validator.properties.AllowNull.toLowerCase() === 'true'
) {
featureRules.push(
...ruleCreator.fieldRequired({
name: field,
trigger: 'blur',
}),
);
}
featureRules.push(
...ruleCreator.fieldMustBeStringWithMinimumLengthAndMaximumLength({
name: field,
minimum: Number(validator.properties.MinValue),
maximum: Number(validator.properties.MaxValue),
trigger: 'blur',
}),
);
break;
default:
break;
}
}
return featureRules;
}
function onGroupChange(activeKey) {
featureGroupKey.value = activeKey;
}
function handleSubmit() {
const form = unref(formRel);
form.validate().then(() => {
update(
{
providerName: unref(providerName),
providerKey: unref(providerKey),
},
{
features: getFeatures(unref(featureGroup).groups),
},
).then(() => {
modalMethods.closeModal();
message.success(L('Successful'));
});
});
}
return {
featureGroup,
featureGroupKey,
validator,
handleSubmit,
onGroupChange,
};
}

89
apps/vue/src/views/feature/src/FeatureModal.vue

@ -1,89 +0,0 @@
<template>
<BasicModal
v-bind="$attrs"
@register="registerModal"
:title="L('ManageFeatures')"
:width="800"
:height="400"
:min-height="400"
@ok="handleSubmit"
@cancel="onGroupChange(0)"
>
<Form ref="formRel" :model="featureGroup">
<Tabs tabPosition="left" v-model:activeKey="featureGroupKey" @change="onGroupChange">
<TabPane v-for="(group, gi) in featureGroup.groups" :key="gi" :tab="group.displayName">
<div v-for="(feature, fi) in group.features" :key="feature.name">
<FormItem
v-if="feature.valueType !== null"
:label="feature.displayName"
:name="['groups', gi, 'features', fi, 'value']"
:rules="validator(feature.displayName, feature.valueType.validator)"
:extra="feature.description"
>
<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"
/>
<BInput v-else v-model:value="feature.value" />
</div>
<Select
v-else-if="feature.valueType.name === 'SelectionStringValueType'"
v-model="feature.value"
>
<Option
v-for="valueItem in (feature.valueType as SelectionStringValueType).itemSource.items"
:key="valueItem.value"
v-model:value="valueItem.value"
:label="localizer(valueItem.displayText.resourceName, valueItem.displayText.name)"
/>
</Select>
</FormItem>
</div>
</TabPane>
</Tabs>
</Form>
</BasicModal>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { Checkbox, Form, InputNumber, Select, Tabs } from 'ant-design-vue';
import { Input } from '/@/components/Input';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useFeature } from '../hooks/useFeature';
const FormItem = Form.Item;
const Option = Select.Option;
const TabPane = Tabs.TabPane;
const BInput = Input!;
const { L } = useLocalization('AbpFeatureManagement');
// TODO: ?
const localizer = computed(() => {
return (resourceName: string, key: string) => {
const { L: RL } = useLocalization(resourceName);
return RL(key);
};
});
const formRel = ref(null);
const providerName = ref('');
const providerKey = ref(null);
const [registerModal, modalMethods] = useModalInner((data) => {
providerName.value = data.providerName;
providerKey.value = data.providerKey;
});
const { featureGroup, featureGroupKey, validator, handleSubmit, onGroupChange } = useFeature({
providerName,
providerKey,
formRel,
modalMethods,
});
</script>

6
apps/vue/src/views/localization/languages/components/LanguageTable.vue

@ -53,7 +53,7 @@
const { createConfirm, createMessage } = useMessage(); const { createConfirm, createMessage } = useMessage();
const { L } = useLocalization(['LocalizationManagement', 'AbpLocalization', 'AbpUi']); const { L } = useLocalization(['LocalizationManagement', 'AbpLocalization', 'AbpUi']);
const [registerModal, { openModal }] = useModal(); const [registerModal, { openModal }] = useModal();
const [registerTable, { setTableData, getForm }] = useTable({ const [registerTable, { setLoading, setTableData, getForm }] = useTable({
rowKey: 'cultureName', rowKey: 'cultureName',
title: L('Languages'), title: L('Languages'),
columns: getDataColumns(), columns: getDataColumns(),
@ -90,8 +90,12 @@
function fetchLanguages() { function fetchLanguages() {
const form = getForm(); const form = getForm();
return form.validate().then((input) => { return form.validate().then((input) => {
setLoading(true);
setTableData([]);
return getList(input).then((res) => { return getList(input).then((res) => {
setTableData(res.items); setTableData(res.items);
}).finally(() => {
setLoading(false);
}); });
}); });
} }

6
apps/vue/src/views/localization/resources/components/ResourceTable.vue

@ -53,7 +53,7 @@
const { createConfirm, createMessage } = useMessage(); const { createConfirm, createMessage } = useMessage();
const { L } = useLocalization(['LocalizationManagement', 'AbpLocalization', 'AbpUi']); const { L } = useLocalization(['LocalizationManagement', 'AbpLocalization', 'AbpUi']);
const [registerModal, { openModal }] = useModal(); const [registerModal, { openModal }] = useModal();
const [registerTable, { setTableData, getForm }] = useTable({ const [registerTable, { setLoading, setTableData, getForm }] = useTable({
rowKey: 'name', rowKey: 'name',
title: L('Resources'), title: L('Resources'),
columns: getDataColumns(), columns: getDataColumns(),
@ -89,8 +89,12 @@
function fetchResources() { function fetchResources() {
const form = getForm(); const form = getForm();
return form.validate().then((input) => { return form.validate().then((input) => {
setLoading(true);
setTableData([]);
return getList(input).then((res) => { return getList(input).then((res) => {
setTableData(res.items); setTableData(res.items);
}).finally(() => {
setLoading(false);
}); });
}); });
} }

4
apps/vue/src/views/localization/texts/components/ModalData.ts

@ -58,12 +58,12 @@ export function getSearchFormSchemas(submitFunc?: () => Promise<void>): Partial<
{ {
key: L('DisplayName:Any'), key: L('DisplayName:Any'),
label: L('DisplayName:Any'), label: L('DisplayName:Any'),
value: 0, value: false,
}, },
{ {
key: L('DisplayName:OnlyNull'), key: L('DisplayName:OnlyNull'),
label: L('DisplayName:OnlyNull'), label: L('DisplayName:OnlyNull'),
value: 1, value: true,
}, },
], ],
}, },

7
apps/vue/src/views/localization/texts/components/TextTable.vue

@ -43,7 +43,7 @@
const { L } = useLocalization(['LocalizationManagement', 'AbpUi']); const { L } = useLocalization(['LocalizationManagement', 'AbpUi']);
const { hasPermission } = usePermission(); const { hasPermission } = usePermission();
const [registerModal, { openModal }] = useModal(); const [registerModal, { openModal }] = useModal();
const [registerTable, { setTableData, setPagination, getForm }] = useTable({ const [registerTable, { setLoading, setTableData, setPagination, getForm }] = useTable({
rowKey: 'key', rowKey: 'key',
title: L('Texts'), title: L('Texts'),
columns: getDataColumns(), columns: getDataColumns(),
@ -70,9 +70,12 @@
const form = getForm(); const form = getForm();
return form.validate().then((input) => { return form.validate().then((input) => {
const request = cloneDeep(input); const request = cloneDeep(input);
request.onlyNull = input.onlyNull === 1; setLoading(true);
setTableData([]);
return getList(request).then((res) => { return getList(request).then((res) => {
return setTableData(res.items); return setTableData(res.items);
}).finally(() => {
setLoading(false);
}); });
}) })
} }

4
apps/vue/src/views/openiddict/applications/components/ApplicationModal.vue

@ -391,6 +391,8 @@
function handleBeforeClose(): Promise<boolean> { function handleBeforeClose(): Promise<boolean> {
return new Promise((resolve) => { return new Promise((resolve) => {
if (!state.entityChanged) { if (!state.entityChanged) {
const form = unref(formRef);
form?.resetFields();
return resolve(true); return resolve(true);
} }
createConfirm({ createConfirm({
@ -398,6 +400,8 @@
title: L('AreYouSure'), title: L('AreYouSure'),
content: L('AreYouSureYouWantToCancelEditingWarningMessage'), content: L('AreYouSureYouWantToCancelEditingWarningMessage'),
onOk: () => { onOk: () => {
const form = unref(formRef);
form?.resetFields();
resolve(true); resolve(true);
}, },
onCancel: () => { onCancel: () => {

22
apps/vue/src/views/openiddict/applications/components/ApplicationTable.vue

@ -33,10 +33,15 @@
{ {
auth: 'AbpOpenIddict.Applications.ManagePermissions', auth: 'AbpOpenIddict.Applications.ManagePermissions',
label: L('ManagePermissions'), label: L('ManagePermissions'),
onClick: handlePermission.bind(null, record), onClick: handleManagePermissions.bind(null, record),
}, },
{ {
//auth: 'AbpOpenIddict.Applications.ManageSecret', auth: 'AbpOpenIddict.Applications.ManageFeatures',
label: L('ManageFeatures'),
onClick: handleManageFeatures.bind(null, record),
},
{
auth: 'AbpOpenIddict.Applications.ManageSecret',
label: L('GenerateSecret'), label: L('GenerateSecret'),
ifShow: getShowSecret(record), ifShow: getShowSecret(record),
onClick: handleGenerateSecret.bind(null, record), onClick: handleGenerateSecret.bind(null, record),
@ -49,6 +54,7 @@
<ApplicationModal @register="registerModal" @change="reload" /> <ApplicationModal @register="registerModal" @change="reload" />
<ApplicationSecretModal @register="registerSecretModal" @change="reload" /> <ApplicationSecretModal @register="registerSecretModal" @change="reload" />
<PermissionModal @register="registerPermissionModal" /> <PermissionModal @register="registerPermissionModal" />
<FeatureModal @register="registerFeatureModal" />
</div> </div>
</template> </template>
@ -58,6 +64,7 @@
import { BasicTable, TableAction, useTable } from '/@/components/Table'; import { BasicTable, TableAction, useTable } from '/@/components/Table';
import { getDataColumns } from '../datas/TableData'; import { getDataColumns } from '../datas/TableData';
import { useModal } from '/@/components/Modal'; import { useModal } from '/@/components/Modal';
import { FeatureModal } from '/@/components/Abp';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { useLocalization } from '/@/hooks/abp/useLocalization'; import { useLocalization } from '/@/hooks/abp/useLocalization';
import { GetListAsyncByInput, DeleteAsyncById } from '/@/api/openiddict/open-iddict-application'; import { GetListAsyncByInput, DeleteAsyncById } from '/@/api/openiddict/open-iddict-application';
@ -70,6 +77,7 @@
const { createConfirm, createMessage } = useMessage(); const { createConfirm, createMessage } = useMessage();
const [registerModal, { openModal }] = useModal(); const [registerModal, { openModal }] = useModal();
const [registerSecretModal, { openModal: openSecretModal }] = useModal(); const [registerSecretModal, { openModal: openSecretModal }] = useModal();
const [registerFeatureModal, { openModal: openFeatureModal }] = useModal();
const [registerPermissionModal, { openModal: openPermissionModal }] = useModal(); const [registerPermissionModal, { openModal: openPermissionModal }] = useModal();
const [registerTable, { reload }] = useTable({ const [registerTable, { reload }] = useTable({
rowKey: 'id', rowKey: 'id',
@ -120,7 +128,7 @@
openSecretModal(true, record); openSecretModal(true, record);
} }
function handlePermission(record) { function handleManagePermissions(record) {
const props = { const props = {
providerName: 'C', providerName: 'C',
providerKey: record.clientId, providerKey: record.clientId,
@ -128,6 +136,14 @@
openPermissionModal(true, props, true); openPermissionModal(true, props, true);
} }
function handleManageFeatures(record) {
const props = {
providerName: 'C',
providerKey: record.clientId,
};
openFeatureModal(true, props, true);
}
function handleDelete(record) { function handleDelete(record) {
createConfirm({ createConfirm({
iconType: 'warning', iconType: 'warning',

49
apps/vue/src/views/oss-management/objects/components/FileList.vue

@ -46,7 +46,7 @@
</template> </template>
</template> </template>
</BasicTable> </BasicTable>
<OssUploadModal @register="registerUploadModal" /> <OssUploadModal @register="registerUploadModal" @file:uploaded="handleUploaded" />
<OssPreviewModal @register="registerPreviewModal" /> <OssPreviewModal @register="registerPreviewModal" />
</div> </div>
</template> </template>
@ -65,6 +65,7 @@
import OssUploadModal from './OssUploadModal.vue'; import OssUploadModal from './OssUploadModal.vue';
import OssPreviewModal from './OssPreviewModal.vue'; import OssPreviewModal from './OssPreviewModal.vue';
const emits = defineEmits(['file:delete', 'file:upload', 'folder:delete', 'oss:delete']);
const props = defineProps({ const props = defineProps({
bucket: { bucket: {
type: String, type: String,
@ -158,21 +159,29 @@
}); });
} }
function handleUploaded(bucket: string, path: string, name: string) {
reload();
emits('file:upload', bucket, path, name);
}
function handleBulkDelete() { function handleBulkDelete() {
createConfirm({ createConfirm({
iconType: 'warning', iconType: 'warning',
title: L('AreYouSure'), title: L('AreYouSure'),
content: L('Objects:WillBeBulkDeletedMessage'), content: L('Objects:WillBeBulkDeletedMessage'),
okCancel: true, okCancel: true,
onOk: () => { onOk: async () => {
return bulkDeleteObject({ const bucket = props.bucket;
bucket: props.bucket, const path = props.path;
path: props.path, const objects = getSelectRowKeys();
objects: getSelectRowKeys(), await bulkDeleteObject({
}).then(() => { bucket: bucket,
createMessage.success(L('SuccessfullyDeleted')); path: path,
reload(); objects: objects,
}); });
createMessage.success(L('SuccessfullyDeleted'));
await reload();
emits('oss:delete', bucket, path, objects);
}, },
}); });
} }
@ -190,15 +199,18 @@
title: L('AreYouSure'), title: L('AreYouSure'),
content: L('ItemWillBeDeletedMessage'), content: L('ItemWillBeDeletedMessage'),
okCancel: true, okCancel: true,
onOk: () => { onOk: async () => {
return deleteObject({ const bucket = props.bucket;
bucket: props.bucket, const path = props.path;
path: props.path, const object = record.name;
object: record.name, await deleteObject({
}).then(() => { bucket: bucket,
createMessage.success(L('SuccessfullyDeleted')); path: path,
reload(); object: object,
}); });
createMessage.success(L('SuccessfullyDeleted'));
await reload();
emits(record.isFolder ? 'folder:delete' : 'file:delete', bucket, path, object);
}, },
}); });
} }
@ -212,4 +224,7 @@
link.click(); link.click();
} }
defineExpose({
refresh: reload,
});
</script> </script>

45
apps/vue/src/views/oss-management/objects/components/FolderTree.vue

@ -10,14 +10,19 @@
@click="handleNewFolder">{{ L('Objects:CreateFolder') }}</Button> @click="handleNewFolder">{{ L('Objects:CreateFolder') }}</Button>
</Col> </Col>
<Col :span="24"> <Col :span="24">
<div class="folder-tree">
<DirectoryTree <DirectoryTree
v-if="enabledNewFolder" v-if="enabledNewFolder"
ref="folderTreeRef"
v-model:expandedKeys="expandedKeys" v-model:expandedKeys="expandedKeys"
v-model:selectedKeys="selectedKeys" v-model:selectedKeys="selectedKeys"
:loadedKeys="loadedKeys"
:tree-data="folders" :tree-data="folders"
:load-data="fetchChildren" :load-data="fetchChildren"
@select="handleSelectChange" @select="handleSelectChange"
@expand="handleFolderExpand"
/> />
</div>
</Col> </Col>
</Row> </Row>
<OssFolderModal @register="registerFolderModal" @change="handleFolderChange" /> <OssFolderModal @register="registerFolderModal" @change="handleFolderChange" />
@ -38,7 +43,7 @@
const DirectoryTree = Tree.DirectoryTree; const DirectoryTree = Tree.DirectoryTree;
const emits = defineEmits(['select']); const emits = defineEmits(['select', 'folder:created']);
const props = defineProps({ const props = defineProps({
bucket: { bucket: {
type: String, type: String,
@ -51,6 +56,8 @@
const enabledNewFolder = computed(() => { const enabledNewFolder = computed(() => {
return hasPermission('AbpOssManagement.OssObject.Create') && (props.bucket ? true : false); return hasPermission('AbpOssManagement.OssObject.Create') && (props.bucket ? true : false);
}); });
const folderTreeRef = ref<any>();
const loadedKeys = ref<string[]>([]);
const expandedKeys = ref<string[]>([]); const expandedKeys = ref<string[]>([]);
const selectedKeys = ref<string[]>([]); const selectedKeys = ref<string[]>([]);
const folders = ref<Folder[]>([ const folders = ref<Folder[]>([
@ -102,6 +109,7 @@
fetchFolders(props.bucket, path).then((fs) => { fetchFolders(props.bucket, path).then((fs) => {
treeNode.dataRef!.children = fs; treeNode.dataRef!.children = fs;
folders.value = [...folders.value]; folders.value = [...folders.value];
loadedKeys.value = [...loadedKeys.value, treeNode.key.toString()];
resolve(); resolve();
}).catch(() => { }).catch(() => {
resolve(); resolve();
@ -109,6 +117,15 @@
}); });
}; };
function handleFolderExpand(_, e: any) {
if (!e.expanded) {
const keys = loadedKeys.value;
const findIndex = keys.findLastIndex(key => key === e.node.key);
findIndex >= 0 && keys.splice(findIndex);
loadedKeys.value = keys;
}
}
function fetchFolders(bucket: string, path?: string): Promise<Folder[]> { function fetchFolders(bucket: string, path?: string): Promise<Folder[]> {
return new Promise((resolve) => { return new Promise((resolve) => {
getObjects({ getObjects({
@ -153,14 +170,34 @@
}); });
} }
function handleFolderChange() { function handleFolderChange(bucket: string, path: string, name: string) {
console.log(folders.value); refresh(path);
// TODO: emits('folder:created', bucket, path, name);
}
function refresh(path: string) {
//
//
const keys = expandedKeys.value;
const findIndex = keys.findLastIndex(key => key === path);
if (findIndex >= 0) {
keys.splice(findIndex);
expandedKeys.value = keys;
loadedKeys.value = [];
} }
}
defineExpose({
refresh,
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.folder-tree-wrap { .folder-tree-wrap {
height: 100%; height: 100%;
} }
.folder-tree {
overflow-x: auto;
}
</style> </style>

2
apps/vue/src/views/oss-management/objects/components/OssFolderModal.vue

@ -51,7 +51,7 @@
}).then(() => { }).then(() => {
createMessage.success(L('Successful')); createMessage.success(L('Successful'));
closeModal(); closeModal();
emits('change', name); emits('change', unref(bucket), unref(path), name);
}).finally(() => { }).finally(() => {
changeOkLoading(false); changeOkLoading(false);
}); });

25
apps/vue/src/views/oss-management/objects/components/OssManagePage.vue

@ -16,19 +16,19 @@
/> />
</template> </template>
<template #description> <template #description>
<FolderTree :bucket="currentBucket" @select="handlePathChange" /> <FolderTree ref="folderTreeRef" :bucket="currentBucket" @select="handlePathChange" @folder:created="handlePathCreated" />
</template> </template>
</CardMeta> </CardMeta>
</CardGrid> </CardGrid>
<CardGrid style="width: 75%;"> <CardGrid style="width: 75%;">
<FileList :bucket="currentBucket" :path="currentPath" /> <FileList ref="fileListRef" :bucket="currentBucket" :path="currentPath" @folder:delete="handlePathDeleted" />
</CardGrid> </CardGrid>
</Card> </Card>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted } from 'vue'; import { ref, unref, onMounted } from 'vue';
import { Card, Select } from 'ant-design-vue'; import { Card, Select } from 'ant-design-vue';
import { useLocalization } from '/@/hooks/abp/useLocalization'; import { useLocalization } from '/@/hooks/abp/useLocalization';
import { getContainers } from '/@/api/oss-management/oss'; import { getContainers } from '/@/api/oss-management/oss';
@ -40,6 +40,8 @@
const CardMeta = Card.Meta; const CardMeta = Card.Meta;
const { L } = useLocalization(['AbpOssManagement', 'AbpUi']); const { L } = useLocalization(['AbpOssManagement', 'AbpUi']);
const fileListRef = ref<any>();
const folderTreeRef = ref<any>();
const currentPath = ref(''); const currentPath = ref('');
const currentBucket = ref(''); const currentBucket = ref('');
const bucketList = ref<OssContainer[]>([]); const bucketList = ref<OssContainer[]>([]);
@ -58,11 +60,24 @@
}); });
} }
function handleBucketChange(bucket) { function handleBucketChange(bucket: string) {
currentBucket.value = bucket; currentBucket.value = bucket;
} }
function handlePathChange(path) { function handlePathChange(path: string) {
currentPath.value = path; currentPath.value = path;
} }
function handlePathCreated() {
const fileList = unref(fileListRef);
fileList?.refresh();
}
function handlePathDeleted(_bucket: string, path: string) {
console.log(_bucket);
console.log(path);
console.log(name);
const folderTree = unref(folderTreeRef);
folderTree?.refresh(path);
}
</script> </script>

8
apps/vue/src/views/oss-management/objects/components/OssUploadModal.vue

@ -60,6 +60,8 @@
import { useUserStoreWithOut } from '/@/store/modules/user'; import { useUserStoreWithOut } from '/@/store/modules/user';
import Uploader from 'simple-uploader.js'; import Uploader from 'simple-uploader.js';
const emits = defineEmits(['file:uploaded', 'register']);
let uploader: any = null; let uploader: any = null;
const { L } = useLocalization(['AbpOssManagement', 'AbpUi']); const { L } = useLocalization(['AbpOssManagement', 'AbpUi']);
const bucket = ref(''); const bucket = ref('');
@ -140,6 +142,7 @@
// uploader.on('fileSuccess', _fileSuccess); // uploader.on('fileSuccess', _fileSuccess);
uploader.on('filesSubmitted', _filesSubmitted); uploader.on('filesSubmitted', _filesSubmitted);
uploader.on('fileError', _fileError); uploader.on('fileError', _fileError);
uploader.on('fileSuccess', _fileSuccess);
uploader.on('fileProgress', _fileProgress); uploader.on('fileProgress', _fileProgress);
}); });
@ -147,6 +150,7 @@
// uploader.off('fileSuccess', _fileSuccess); // uploader.off('fileSuccess', _fileSuccess);
uploader.off('filesSubmitted', _filesSubmitted); uploader.off('filesSubmitted', _filesSubmitted);
uploader.off('fileError', _fileError); uploader.off('fileError', _fileError);
uploader.off('fileSuccess', _fileSuccess);
uploader.off('fileProgress', _fileProgress); uploader.off('fileProgress', _fileProgress);
uploader = null; uploader = null;
}); });
@ -180,6 +184,10 @@
} }
} }
function _fileSuccess(_, file) {
emits('file:uploaded', unref(bucket), unref(path), file.name);
}
function handleSelect() { function handleSelect() {
unref(btnRef)?.click(); unref(btnRef)?.click();
} }

2
apps/vue/src/views/oss-management/objects/index.vue

@ -7,7 +7,7 @@
import OssManagePage from './components/OssManagePage.vue'; import OssManagePage from './components/OssManagePage.vue';
export default defineComponent({ export default defineComponent({
name: 'Oss', name: 'Objects',
components: { components: {
OssManagePage, OssManagePage,
}, },

2
apps/vue/src/views/saas/editions/components/EditionTable.vue

@ -48,9 +48,9 @@
import { usePermission } from '/@/hooks/web/usePermission'; import { usePermission } from '/@/hooks/web/usePermission';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { useModal } from '/@/components/Modal'; import { useModal } from '/@/components/Modal';
import { FeatureModal } from '/@/components/Abp';
import { BasicTable, TableAction, useTable } from '/@/components/Table'; import { BasicTable, TableAction, useTable } from '/@/components/Table';
import { useFeatureModal } from '../hooks/useFeatureModal'; import { useFeatureModal } from '../hooks/useFeatureModal';
import { FeatureModal } from '../../../feature';
import { DeleteAsyncById, GetListAsyncByInput } from '/@/api/saas/edition'; import { DeleteAsyncById, GetListAsyncByInput } from '/@/api/saas/edition';
import { getDataColumns } from '../datas/TableData'; import { getDataColumns } from '../datas/TableData';
import { getSearchFormSchemas } from '../datas//ModalData'; import { getSearchFormSchemas } from '../datas//ModalData';

2
apps/vue/src/views/saas/tenant/components/TenantTable.vue

@ -65,11 +65,11 @@
import { useLocalization } from '/@/hooks/abp/useLocalization'; import { useLocalization } from '/@/hooks/abp/useLocalization';
import { usePermission } from '/@/hooks/web/usePermission'; import { usePermission } from '/@/hooks/web/usePermission';
import { useModal } from '/@/components/Modal'; import { useModal } from '/@/components/Modal';
import { FeatureModal } from '/@/components/Abp';
import { BasicTable, TableAction, TableActionType } from '/@/components/Table'; import { BasicTable, TableAction, TableActionType } from '/@/components/Table';
import { useTenantTable } from '../hooks/useTenantTable'; import { useTenantTable } from '../hooks/useTenantTable';
import { useTenantModal } from '../hooks/useTenantModal'; import { useTenantModal } from '../hooks/useTenantModal';
import { useFeatureModal } from '../hooks/useFeatureModal'; import { useFeatureModal } from '../hooks/useFeatureModal';
import { FeatureModal } from '../../../feature';
import TenantModal from './TenantModal.vue'; import TenantModal from './TenantModal.vue';
import ConnectionTableModal from './ConnectionTableModal.vue'; import ConnectionTableModal from './ConnectionTableModal.vue';

2
apps/vue/src/views/saas/tenant/hooks/useTenantTable.ts

@ -5,7 +5,7 @@ import { useMessage } from '/@/hooks/web/useMessage';
import { useLocalization } from '/@/hooks/abp/useLocalization'; import { useLocalization } from '/@/hooks/abp/useLocalization';
import { TableActionType, useTable } from '/@/components/Table'; import { TableActionType, useTable } from '/@/components/Table';
import { getDataColumns } from '../datas/TableData'; import { getDataColumns } from '../datas/TableData';
import { getSearchFormSchemas } from '../datas//ModalData'; import { getSearchFormSchemas } from '../datas/ModalData';
import { DeleteAsyncById, GetListAsyncByInput } from '/@/api/saas/tenant'; import { DeleteAsyncById, GetListAsyncByInput } from '/@/api/saas/tenant';
import { formatPagedRequest } from '/@/utils/http/abp/helper'; import { formatPagedRequest } from '/@/utils/http/abp/helper';

230
apps/vue/src/views/settings-management/definitions/components/SettingDefinitionModal.vue

@ -0,0 +1,230 @@
<template>
<BasicModal
@register="registerModal"
:title="L('Settings')"
:can-fullscreen="false"
:width="800"
:height="500"
:close-func="handleBeforeClose"
@ok="handleSubmit"
>
<Form
ref="formRef"
:model="state.entity"
:rules="state.entityRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Tabs v-model:active-key="state.activeTab">
<TabPane key="basic" :tab="L('BasicInfo')">
<FormItem name="name" :label="L('DisplayName:Name')">
<Input :disabled="state.entityEditFlag && !state.allowedChange" :allow-clear="true" v-model:value="state.entity.name" />
</FormItem>
<FormItem name="defaultValue" :label="L('DisplayName:DefaultValue')">
<TextArea
:disabled="state.entityEditFlag && !state.allowedChange"
:allow-clear="true"
:auto-size="{ minRows: 3 }"
v-model:value="state.entity.defaultValue"
/>
</FormItem>
<FormItem name="displayName" :label="L('DisplayName:DisplayName')">
<LocalizableInput :disabled="!state.allowedChange" :allow-clear="true" v-model:value="state.entity.displayName" />
</FormItem>
<FormItem name="description" :label="L('DisplayName:Description')">
<LocalizableInput :disabled="!state.allowedChange" :allow-clear="true" v-model:value="state.entity.description" />
</FormItem>
<FormItem name="providers" :label="L('DisplayName:Providers')" :extra="L('Description:Providers')">
<Select mode="multiple" :disabled="!state.allowedChange" :allow-clear="true" v-model:value="state.entity.providers" :options="providers" />
</FormItem>
<FormItem name="isInherited" :label="L('DisplayName:IsInherited')" :extra="L('Description:IsInherited')">
<Checkbox
:disabled="!state.allowedChange"
v-model:checked="state.entity.isInherited"
>{{ L('DisplayName:IsInherited') }}
</Checkbox>
</FormItem>
<FormItem name="isEncrypted" :label="L('DisplayName:IsEncrypted')" :extra="L('Description:IsEncrypted')">
<Checkbox
:disabled="!state.allowedChange"
v-model:checked="state.entity.isEncrypted"
>{{ L('DisplayName:IsEncrypted') }}
</Checkbox>
</FormItem>
<FormItem name="isVisibleToClients" :label="L('DisplayName:IsVisibleToClients')" :extra="L('Description:IsVisibleToClients')">
<Checkbox
:disabled="!state.allowedChange"
v-model:checked="state.entity.isVisibleToClients"
>{{ L('DisplayName:IsVisibleToClients') }}
</Checkbox>
</FormItem>
</TabPane>
<TabPane key="propertites" :tab="L('Properties')">
<FormItem name="extraProperties" label="" :label-col="{ span: 0 }" :wrapper-col="{ span: 24 }">
<ExtraPropertyDictionary :disabled="!state.allowedChange" :allow-delete="true" :allow-edit="true" v-model:value="state.entity.extraProperties" />
</FormItem>
</TabPane>
</Tabs>
</Form>
</BasicModal>
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash-es';
import { ref, reactive, unref, nextTick, watch } from 'vue';
import { Checkbox, Form, Input, Select, Tabs } from 'ant-design-vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { LocalizableInput, ExtraPropertyDictionary } from '/@/components/Abp';
import { ModalState } from '../types/props';
import { useMessage } from '/@/hooks/web/useMessage';
import { ValidationEnum, useValidation } from '/@/hooks/abp/useValidation';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useLocalizationSerializer } from '/@/hooks/abp/useLocalizationSerializer';
import { GetAsyncByName, CreateAsyncByInput, UpdateAsyncByNameAndInput } from '/@/api/settings-management/definitions';
import { SettingDefinitionDto, SettingDefinitionUpdateDto, SettingDefinitionCreateDto } from '/@/api/settings-management/definitions/model';
const FormItem = Form.Item;
const TabPane = Tabs.TabPane;
const TextArea = Input.TextArea;
const emits = defineEmits(['register', 'change']);
const { ruleCreator } = useValidation();
const { validate } = useLocalizationSerializer();
const { createConfirm, createMessage } = useMessage();
const { L } = useLocalization(['AbpSettingManagement', 'AbpUi']);
const formRef = ref<any>();
const providers = reactive([
{ label: L('Providers:Default'), value: 'D' },
{ label: L('Providers:Configuration'), value: 'C' },
{ label: L('Providers:Global'), value: 'G' },
{ label: L('Providers:Tenant'), value: 'T' },
{ label: L('Providers:User'), value: 'U' },
]);
const state = reactive<ModalState>({
activeTab: 'basic',
entity: {
isInherited: true,
} as SettingDefinitionDto,
allowedChange: false,
entityChanged: false,
entityEditFlag: false,
entityRules: {
name: ruleCreator.fieldRequired({
name: 'Name',
prefix: 'DisplayName',
resourceName: 'AbpSettingManagement',
trigger: 'blur',
}),
displayName: ruleCreator.defineValidator({
required: true,
trigger: 'blur',
validator(_rule, value) {
if (!validate(value)) {
return Promise.reject(L(ValidationEnum.FieldRequired, [L('DisplayName:DisplayName')]));
}
return Promise.resolve();
},
}),
description: ruleCreator.defineValidator({
trigger: 'blur',
validator(_rule, value) {
if (!validate(value, { required: false })) {
return Promise.reject(L(ValidationEnum.FieldRequired, [L('DisplayName:Description')]));
}
return Promise.resolve();
},
}),
},
});
watch(
() => state.entity,
() => {
state.entityChanged = true;
},
{
deep: true,
},
);
const [registerModal, { closeModal, changeLoading, changeOkLoading }] = useModalInner((record) => {
nextTick(() => fetch(record.name));
});
function fetch(name?: string) {
state.activeTab = 'basic';
state.entityEditFlag = false;
if (!name) {
state.entity = {
isInherited: true,
};
state.allowedChange = true;
nextTick(() => state.entityChanged = false);
return;
}
changeLoading(true);
changeOkLoading(true);
GetAsyncByName(name).then((record) => {
state.entity = record;
state.entityEditFlag = true;
state.allowedChange = !record.isStatic;
}).finally(() => {
changeLoading(false);
changeOkLoading(false);
nextTick(() => state.entityChanged = false);
});
}
function handleBeforeClose(): Promise<boolean> {
return new Promise((resolve) => {
if (!state.entityChanged) {
const form = unref(formRef);
form?.resetFields();
return resolve(true);
}
createConfirm({
iconType: 'warning',
title: L('AreYouSure'),
content: L('AreYouSureYouWantToCancelEditingWarningMessage'),
onOk: () => {
const form = unref(formRef);
form?.resetFields();
resolve(true);
},
onCancel: () => {
resolve(false);
},
afterClose: () => {
state.allowedChange = false;
},
});
});
}
function handleSubmit() {
if (!state.allowedChange) {
closeModal();
return;
}
const form = unref(formRef);
form?.validate().then(() => {
changeOkLoading(true);
const api = state.entityEditFlag
? UpdateAsyncByNameAndInput(state.entity.name, cloneDeep(state.entity) as SettingDefinitionUpdateDto)
: CreateAsyncByInput(cloneDeep(state.entity) as SettingDefinitionCreateDto);
api.then((res) => {
createMessage.success(L('Successful'));
emits('change', res);
form.resetFields();
closeModal();
}).finally(() => {
changeOkLoading(false);
})
});
}
</script>
<style scoped>
</style>

155
apps/vue/src/views/settings-management/definitions/components/SettingDefinitionTable.vue

@ -0,0 +1,155 @@
<template>
<div>
<BasicTable @register="registerTable">
<template #toolbar>
<Button
v-auth="['SettingManagement.Definition.Create']"
type="primary"
@click="handleAddNew"
>
{{ L('Definition:AddNew') }}
</Button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'displayName'">
<span>{{ getDisplayName(record.displayName) }}</span>
</template>
<template v-else-if="column.key === 'description'">
<span>{{ getDisplayName(record.description) }}</span>
</template>
<template v-else-if="column.key === 'isVisibleToClients'">
<CheckOutlined v-if="record.isVisibleToClients" class="enable" />
<CloseOutlined v-else class="disable" />
</template>
<template v-else-if="column.key === 'isInherited'">
<CheckOutlined v-if="record.isInherited" class="enable" />
<CloseOutlined v-else class="disable" />
</template>
<template v-else-if="column.key === 'isEncrypted'">
<CheckOutlined v-if="record.isEncrypted" class="enable" />
<CloseOutlined v-else class="disable" />
</template>
<template v-else-if="column.key === 'isStatic'">
<CheckOutlined v-if="record.isStatic" class="enable" />
<CloseOutlined v-else class="disable" />
</template>
<template v-else-if="column.key === 'providers'">
<Tag v-for="provider in record.providers" style="margin-left: 5px" color="blue">{{ provider }}</Tag>
</template>
<template v-else-if="column.key === 'action'">
<TableAction
:stop-button-propagation="true"
:actions="[
{
auth: 'SettingManagement.Definition.Update',
label: L('Edit'),
icon: 'ant-design:edit-outlined',
onClick: handleEdit.bind(null, record),
},
{
auth: 'SettingManagement.Definition.DeleteOrRestore',
label: L('DeleteOrRestore'),
color: 'error',
icon: 'ant-design:delete-outlined',
ifShow: !record.isStatic,
onClick: handleDelete.bind(null, record),
},
]"
/>
</template>
</template>
</BasicTable>
<SettingDefinitionModal @register="registerModal" @change="fetch" />
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted } from 'vue';
import { Button, Tag } from 'ant-design-vue';
import { CheckOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { BasicTable, TableAction, useTable } from '/@/components/Table';
import { getDataColumns } from '../datas/TableData';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useLocalizationSerializer } from '/@/hooks/abp/useLocalizationSerializer';
import { GetListAsyncByInput, DeleteOrRestoreAsyncByName } from '/@/api/settings-management/definitions';
import { getSearchFormSchemas } from '../datas/ModalData';
import SettingDefinitionModal from './SettingDefinitionModal.vue';
const { deserialize } = useLocalizationSerializer();
const { L, Lr } = useLocalization(['AbpSettingManagement', 'AbpUi']);
const { createConfirm, createMessage } = useMessage();
const [registerModal, { openModal }] = useModal();
const [registerTable, { setLoading, getForm, setTableData }] = useTable({
rowKey: 'name',
title: L('Settings'),
columns: getDataColumns(),
pagination: true,
striped: false,
useSearchForm: true,
showIndexColumn: false,
showTableSetting: true,
tableSetting: {
redo: false,
},
formConfig: {
labelWidth: 100,
submitFunc: fetch,
schemas: getSearchFormSchemas(),
},
bordered: true,
canResize: true,
immediate: false,
actionColumn: {
width: 200,
title: L('Actions'),
dataIndex: 'action',
},
});
const getDisplayName = computed(() => {
return (displayName?: string) => {
if (!displayName) return displayName;
const info = deserialize(displayName);
return Lr(info.resourceName, info.name);
};
});
onMounted(fetch);
function fetch() {
const form = getForm();
return form.validate().then(() => {
setLoading(true);
setTableData([]);
var input = form.getFieldsValue();
GetListAsyncByInput(input).then((res) => {
setTableData(res.items);
}).finally(() => {
setLoading(false);
});
});
}
function handleAddNew() {
openModal(true, {});
}
function handleEdit(record) {
openModal(true, record);
}
function handleDelete(record) {
createConfirm({
iconType: 'warning',
title: L('AreYouSure'),
content: L('ItemWillBeDeleteOrRestoreMessage'),
onOk: () => {
return DeleteOrRestoreAsyncByName(record.name).then(() => {
createMessage.success(L('Successful'));
fetch();
});
},
});
}
</script>

30
apps/vue/src/views/settings-management/definitions/datas/ModalData.ts

@ -0,0 +1,30 @@
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { FormSchema } from '/@/components/Form';
const { L } = useLocalization(['AbpSettingManagement', 'AbpUi']);
export function getSearchFormSchemas():FormSchema[] {
return [
{
field: 'providerName',
component: 'Select',
label: L('DisplayName:Providers'),
colProps: { span: 6 },
componentProps: {
options: [
{ label: L('Providers:Default'), value: 'D' },
{ label: L('Providers:Configuration'), value: 'C' },
{ label: L('Providers:Global'), value: 'G' },
{ label: L('Providers:Tenant'), value: 'T' },
{ label: L('Providers:User'), value: 'U' },
],
}
},
{
field: 'filter',
component: 'Input',
label: L('Search'),
colProps: { span: 18 },
},
];
}

83
apps/vue/src/views/settings-management/definitions/datas/TableData.ts

@ -0,0 +1,83 @@
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { BasicColumn } from '/@/components/Table';
import { sorter } from '/@/utils/table';
const { L } = useLocalization(['AbpSettingManagement']);
export function getDataColumns(): BasicColumn[] {
return [
{
title: L('DisplayName:Name'),
dataIndex: 'name',
align: 'left',
width: 350,
resizable: true,
sorter: (a, b) => sorter(a, b, 'name'),
},
{
title: L('DisplayName:DisplayName'),
dataIndex: 'displayName',
align: 'left',
width: 180,
resizable: true,
sorter: (a, b) => sorter(a, b, 'displayName'),
},
{
title: L('DisplayName:Description'),
dataIndex: 'description',
align: 'left',
width: 250,
resizable: true,
sorter: (a, b) => sorter(a, b, 'description'),
},
{
title: L('DisplayName:DefaultValue'),
dataIndex: 'defaultValue',
align: 'left',
width: 180,
resizable: true,
sorter: (a, b) => sorter(a, b, 'defaultValue'),
},
{
title: L('DisplayName:IsVisibleToClients'),
dataIndex: 'isVisibleToClients',
align: 'center',
width: 150,
resizable: true,
sorter: (a, b) => sorter(a, b, 'isVisibleToClients'),
},
{
title: L('DisplayName:IsInherited'),
dataIndex: 'isInherited',
align: 'center',
width: 160,
resizable: true,
sorter: (a, b) => sorter(a, b, 'isInherited'),
},
{
title: L('DisplayName:IsEncrypted'),
dataIndex: 'isEncrypted',
align: 'center',
width: 150,
resizable: true,
sorter: (a, b) => sorter(a, b, 'isEncrypted'),
},
{
title: L('DisplayName:IsStatic'),
dataIndex: 'isStatic',
align: 'center',
width: 150,
resizable: true,
defaultHidden: true,
sorter: (a, b) => sorter(a, b, 'isStatic'),
},
{
title: L('DisplayName:Providers'),
dataIndex: 'providers',
align: 'left',
width: 160,
resizable: true,
sorter: (a, b) => sorter(a, b, 'providers'),
},
];
}

16
apps/vue/src/views/settings-management/definitions/index.vue

@ -0,0 +1,16 @@
<template>
<SettingDefinitionTable />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import SettingDefinitionTable from './components/SettingDefinitionTable.vue';
export default defineComponent({
name: 'SettingDefinitions',
components: {
SettingDefinitionTable,
},
setup() {},
});
</script>

10
apps/vue/src/views/settings-management/definitions/types/props.ts

@ -0,0 +1,10 @@
import { Rule } from 'ant-design-vue/lib/form';
export interface ModalState {
activeTab: string,
allowedChange: boolean,
entity: Recordable,
entityRules?: Dictionary<string, Rule>,
entityChanged: boolean,
entityEditFlag: boolean,
}

93
apps/vue/src/views/settings-management/settings/index.vue

@ -0,0 +1,93 @@
<template>
<SettingForm :save-api="settingFormRef.saveApi" :setting-groups="group">
<template #send-test-email="{ detail }">
<FormItem name="testEmail" :label="detail.displayName" :extra="detail.description">
<SearchInput
:placeholder="L('TargetEmailAddress')"
v-model:value="detail.value"
@search="handleSendTestEmail"
:loading="sendingEmail"
>
<template #enterButton>
<Button type="primary">{{ L('Send') }}</Button>
</template>
</SearchInput>
</FormItem>
</template>
</SettingForm>
</template>
<script lang="ts">
export default {
name: 'SystemSettings',
}
</script>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { Button, Form, Input } from 'ant-design-vue';
import { useAbpStoreWithOut } from '/@/store/modules/abp';
import { useMessage } from '/@/hooks/web/useMessage';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { SettingForm } from '/@/components/SettingManagement';
import { SettingGroup } from '/@/api/settings-management/settings/model';
import {
getCurrentTenantSettings,
getGlobalSettings,
setGlobalSettings,
setCurrentTenantSettings,
sendTestEmail,
} from '/@/api/settings-management/settings';
import { isEmail } from '/@/utils/is';
interface ISettingForm {
providerName: string;
providerKey?: string;
saveApi: (...args: any) => Promise<any>;
}
const FormItem = Form.Item;
const SearchInput = Input.Search;
const sendingEmail = ref(false);
const group = ref<SettingGroup[]>([]);
const settingFormRef = ref<ISettingForm>({
providerName: 'G',
providerKey: '',
saveApi: setGlobalSettings,
});
const abpStore = useAbpStoreWithOut();
const { createWarningModal, createMessage } = useMessage();
const { L } = useLocalization(['AbpSettingManagement']);
onMounted(() => {
if (abpStore.getApplication.currentTenant.id) {
settingFormRef.value = {
providerName: 'T',
providerKey: abpStore.getApplication.currentTenant.id,
saveApi: setCurrentTenantSettings,
};
getCurrentTenantSettings().then((res) => {
group.value = res.items;
});
} else {
getGlobalSettings().then((res) => {
group.value = res.items;
});
}
});
function handleSendTestEmail(emailAddress: string) {
if (!isEmail(emailAddress)) {
createWarningModal({
title: L('ValidationErrorMessage'),
content: L('ThisFieldIsNotAValidEmailAddress.')
});
return;
}
sendingEmail.value = true;
sendTestEmail(emailAddress).then(() => {
createMessage.success(L('SuccessfullySent'));
}).finally(() => {
sendingEmail.value = false;
});
}
</script>

6
apps/vue/src/views/sys/settings/index.vue

@ -5,7 +5,7 @@
<SearchInput <SearchInput
:placeholder="L('TargetEmailAddress')" :placeholder="L('TargetEmailAddress')"
v-model:value="detail.value" v-model:value="detail.value"
@search="handleSendTestEmail(detail.value)" @search="handleSendTestEmail"
:loading="sendingEmail" :loading="sendingEmail"
> >
<template #enterButton> <template #enterButton>
@ -23,14 +23,14 @@
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { useLocalization } from '/@/hooks/abp/useLocalization'; import { useLocalization } from '/@/hooks/abp/useLocalization';
import { SettingForm } from '/@/components/SettingManagement'; import { SettingForm } from '/@/components/SettingManagement';
import { SettingGroup } from '/@/api/settings/model/settingModel'; import { SettingGroup } from '/@/api/settings-management/settings/model';
import { import {
getCurrentTenantSettings, getCurrentTenantSettings,
getGlobalSettings, getGlobalSettings,
setGlobalSettings, setGlobalSettings,
setCurrentTenantSettings, setCurrentTenantSettings,
sendTestEmail, sendTestEmail,
} from '/@/api/settings/settings'; } from '/@/api/settings-management/settings';
import { isEmail } from '/@/utils/is'; import { isEmail } from '/@/utils/is';
interface ISettingForm { interface ISettingForm {

252
apps/vue/src/views/text-templating/templates/components/TemplateDefinitionModal.vue

@ -4,87 +4,251 @@
:title="L('TextTemplates')" :title="L('TextTemplates')"
:width="800" :width="800"
:min-height="400" :min-height="400"
:close-func="handleBeforeClose"
@ok="handleSubmit" @ok="handleSubmit"
> >
<BasicForm @register="registerForm" /> <Form
ref="formRef"
:model="state.entity"
:rules="state.entityRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Tabs v-model:active-key="state.activeTab">
<TabPane key="basic" :tab="L('BasicInfo')">
<FormItem hidden name="concurrencyStamp" label="concurrencyStamp">
<Input v-model:value="state.entity.concurrencyStamp" />
</FormItem>
<FormItem hidden name="localizationResourceName" label="L('DisplayName:LocalizationResourceName')">
<Input v-model:value="state.entity.localizationResourceName" />
</FormItem>
<FormItem name="isInlineLocalized" :label="L('DisplayName:Name')">
<Checkbox
:disabled="state.entityEditFlag && !state.allowedChange"
v-model:checked="state.entity.isInlineLocalized"
>{{ L('DisplayName:IsInlineLocalized') }}
</Checkbox>
</FormItem>
<FormItem name="defaultCultureName" :label="L('DisplayName:DefaultCultureName')">
<Select :disabled="!state.allowedChange" :allow-clear="true" v-model:value="state.entity.defaultCultureName" :options="state.languages" />
</FormItem>
<FormItem name="isLayout" :label="L('DisplayName:IsLayout')">
<Checkbox
:disabled="state.entityEditFlag && !state.allowedChange"
v-model:checked="state.entity.isLayout"
>{{ L('DisplayName:IsLayout') }}
</Checkbox>
</FormItem>
<FormItem :hidden="state.entity.isLayout" name="layout" :label="L('DisplayName:Layout')">
<Select :disabled="!state.allowedChange" :allow-clear="true" v-model:value="state.entity.layout" :options="state.layouts" />
</FormItem>
<FormItem name="name" :label="L('DisplayName:Name')">
<Input :disabled="!state.allowedChange" v-model:value="state.entity.name" />
</FormItem>
<FormItem name="displayName" :label="L('DisplayName:DisplayName')">
<LocalizableInput :disabled="!state.allowedChange" v-model:value="state.entity.displayName" />
</FormItem>
</TabPane>
<TabPane key="propertites" :tab="L('Properties')">
<FormItem name="extraProperties" label="" :label-col="{ span: 0 }" :wrapper-col="{ span: 24 }">
<ExtraPropertyDictionary :disabled="!state.allowedChange" :allow-delete="true" :allow-edit="true" v-model:value="state.entity.extraProperties" />
</FormItem>
</TabPane>
</Tabs>
</Form>
</BasicModal> </BasicModal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, nextTick } from 'vue'; import type { Rule } from 'ant-design-vue/lib/form';
import { BasicForm, useForm } from '/@/components/Form'; import type { DefaultOptionType } from 'ant-design-vue/lib/select';
import { reactive, ref, unref, nextTick, onMounted, watch, watchEffect } from 'vue';
import { Checkbox, Form, Input, Select, Tabs } from 'ant-design-vue';
import { BasicModal, useModalInner } from '/@/components/Modal'; import { BasicModal, useModalInner } from '/@/components/Modal';
import { LocalizableInput, ExtraPropertyDictionary } from '/@/components/Abp';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { useLocalization } from '/@/hooks/abp/useLocalization'; import { useLocalization } from '/@/hooks/abp/useLocalization';
import { GetByNameAsyncByName, CreateAsyncByInput, UpdateAsyncByNameAndInput } from '/@/api/text-templating/definitions'; import { ValidationEnum, useValidation } from '/@/hooks/abp/useValidation';
import { getModalFormSchemas } from '../datas/ModalData'; import { useLocalizationSerializer } from '/@/hooks/abp/useLocalizationSerializer';
import {
GetByNameAsyncByName,
CreateAsyncByInput,
UpdateAsyncByNameAndInput,
GetListAsyncByInput,
} from '/@/api/text-templating/definitions';
import { getList as getLanguages } from '/@/api/localization/languages';
const FormItem = Form.Item;
const TabPane = Tabs.TabPane;
const emits = defineEmits(['register', 'change']); const emits = defineEmits(['register', 'change']);
const state = reactive({ interface State {
isEdit: false activeTab: string,
}); allowedChange: boolean,
const { createMessage } = useMessage(); entity: Recordable,
const { L } = useLocalization(['AbpTextTemplating']); entityRules?: Dictionary<string, Rule>,
const [registerForm, { resetFields, setFieldsValue, validate, updateSchema }] = useForm({ entityChanged: boolean,
labelWidth: 150, entityEditFlag: boolean,
showActionButtonGroup: false, layouts: DefaultOptionType[],
schemas: getModalFormSchemas(), languages: DefaultOptionType[],
}); }
const { ruleCreator } = useValidation();
const { createConfirm, createMessage } = useMessage();
const { deserialize, validate } = useLocalizationSerializer();
const { L, Lr } = useLocalization(['AbpTextTemplating']);
const [registerModal, { changeLoading, changeOkLoading, closeModal }] = useModalInner((data) => { const [registerModal, { changeLoading, changeOkLoading, closeModal }] = useModalInner((data) => {
nextTick(() => { nextTick(() => fetch(data?.name));
fetch(data?.name); });
const formRef = ref<any>();
const state = reactive<State>({
activeTab: 'basic',
allowedChange: true,
entityChanged: false,
entityEditFlag: false,
entity: {},
entityRules: {
name: ruleCreator.fieldRequired({
name: 'Name',
prefix: 'DisplayName',
resourceName: 'AbpTextTemplating',
trigger: 'blur',
}),
displayName: ruleCreator.defineValidator({
required: true,
trigger: 'blur',
validator(_rule, value) {
if (!validate(value)) {
return Promise.reject(Lr('AbpValidation', ValidationEnum.FieldRequired, [L('DisplayName:DisplayName')]));
}
return Promise.resolve();
},
}),
description: ruleCreator.defineValidator({
trigger: 'blur',
validator(_rule, value) {
if (!validate(value, { required: false })) {
return Promise.reject(L(ValidationEnum.FieldRequired, [L('DisplayName:Description')]));
}
return Promise.resolve();
},
}),
},
layouts: [],
languages: [],
}); });
watch(
() => state.entity,
() => {
state.entityChanged = true;
},
{
deep: true,
},
);
watchEffect(watchLayoutChange);
onMounted(() => {
fetchLayouts();
fetchLanguages();
}); });
function watchLayoutChange() {
if (state.entity.isLayout) {
state.entity.layout = '';
}
}
function fetch(name?: string) { function fetch(name?: string) {
state.isEdit = false; state.activeTab = 'basic';
resetFields(); state.entityEditFlag = false;
if (!name) { if (!name) {
updateSchema({ state.entity = {
field: 'name', isInherited: true,
dynamicDisabled: state.isEdit, };
}); state.allowedChange = true;
nextTick(() => state.entityChanged = false);
return; return;
} }
changeLoading(true); changeLoading(true);
changeOkLoading(true); changeOkLoading(true);
GetByNameAsyncByName(name).then((res) => { GetByNameAsyncByName(name).then((record) => {
state.isEdit = true; state.entity = record;
updateSchema({ state.entityEditFlag = true;
field: 'name', state.allowedChange = !record.isStatic;
dynamicDisabled: state.isEdit, }).finally(() => {
changeLoading(false);
changeOkLoading(false);
nextTick(() => state.entityChanged = false);
});
}
function fetchLanguages() {
getLanguages({}).then((res) => {
state.languages = res.items
.map((item) => {
return {
label: item.displayName,
value: item.cultureName,
};
}); });
setFieldsValue(res);
if (res.formatedDisplayName) {
// L:XXX,YYY
const splitChars = res.formatedDisplayName.split(',');
if (splitChars.length >= 2 && splitChars[0].startsWith('L:')) {
const resource = splitChars[0].substring(2);
setFieldsValue({
resource: resource,
text: splitChars[1],
}); });
} }
function fetchLayouts() {
GetListAsyncByInput({
isLayout: true,
}).then((res) => {
state.layouts = res.items
.map((item) => {
const info = deserialize(item.displayName);
return {
label: Lr(info.resourceName, info.name),
value: item.name,
};
});
});
}
function handleBeforeClose(): Promise<boolean> {
return new Promise((resolve) => {
if (!state.entityChanged) {
const form = unref(formRef);
form?.resetFields();
return resolve(true);
}
createConfirm({
iconType: 'warning',
title: L('AreYouSure'),
content: L('AreYouSureYouWantToCancelEditingWarningMessage'),
onOk: () => {
const form = unref(formRef);
form?.resetFields();
resolve(true);
},
onCancel: () => {
resolve(false);
} }
}).finally(() => { });
changeLoading(false);
changeOkLoading(false);
}); });
} }
function handleSubmit() { function handleSubmit() {
validate().then((input) => { if (!state.allowedChange) {
input.displayName = `L:${input.resource},${input.text}`; closeModal();
return;
}
const form = unref(formRef);
form?.validate().then((input) => {
changeLoading(true); changeLoading(true);
changeOkLoading(true); changeOkLoading(true);
const submitApi = state.isEdit const submitApi = state.entityEditFlag
? UpdateAsyncByNameAndInput(input.name, input) ? UpdateAsyncByNameAndInput(input.name, input)
: CreateAsyncByInput(input); : CreateAsyncByInput(input);
submitApi.then((res) => { submitApi.then((res) => {
setFieldsValue(res);
createMessage.success(L('Successful')); createMessage.success(L('Successful'));
emits('change', res); emits('change', res);
form.resetFields();
closeModal(); closeModal();
}).finally(() => { }).finally(() => {
changeLoading(false); changeLoading(false);

65
apps/vue/src/views/text-templating/templates/components/TemplateDefinitionTable.vue

@ -10,7 +10,10 @@
</Button> </Button>
</template> </template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'isStatic'"> <template v-if="column.key === 'displayName'">
<span>{{ getDisplayName(record.displayName) }}</span>
</template>
<template v-else-if="column.key === 'isStatic'">
<CheckOutlined v-if="record.isStatic" class="enable" /> <CheckOutlined v-if="record.isStatic" class="enable" />
<CloseOutlined v-else class="disable" /> <CloseOutlined v-else class="disable" />
</template> </template>
@ -37,6 +40,7 @@
onClick: handleEdit.bind(null, record), onClick: handleEdit.bind(null, record),
}, },
{ {
ifShow: !record.isStatic,
auth: 'AbpTextTemplating.TextTemplateDefinitions.Delete', auth: 'AbpTextTemplating.TextTemplateDefinitions.Delete',
label: L('Delete'), label: L('Delete'),
color: 'error', color: 'error',
@ -57,50 +61,89 @@
</template> </template>
</BasicTable> </BasicTable>
<TemplateContentModal @register="registerContentModal" /> <TemplateContentModal @register="registerContentModal" />
<TemplateDefinitionModal @register="registerEditModal" @change="reload" /> <TemplateDefinitionModal @register="registerEditModal" @change="fetch" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted } from 'vue';
import { Button } from 'ant-design-vue'; import { Button } from 'ant-design-vue';
import { CheckOutlined, CloseOutlined } from '@ant-design/icons-vue'; import { CheckOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { useModal } from '/@/components/Modal'; import { useModal } from '/@/components/Modal';
import { BasicTable, TableAction, useTable } from '/@/components/Table'; import { BasicTable, TableAction, useTable } from '/@/components/Table';
import { getDataColumns } from '../datas/TableData'; import { getDataColumns } from '../datas/TableData';
import { getSearchFormSchemas } from '../datas/ModalData';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { useLocalization } from '/@/hooks/abp/useLocalization'; import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useLocalizationSerializer } from '/@/hooks/abp/useLocalizationSerializer';
import { GetListAsyncByInput, DeleteAsyncByName } from '/@/api/text-templating/definitions'; import { GetListAsyncByInput, DeleteAsyncByName } from '/@/api/text-templating/definitions';
import { TextTemplateDefinitionDto } from '/@/api/text-templating/definitions/model'; import { TextTemplateDefinitionDto } from '/@/api/text-templating/definitions/model';
import { formatPagedRequest } from '/@/utils/http/abp/helper';
import TemplateContentModal from './TemplateContentModal.vue'; import TemplateContentModal from './TemplateContentModal.vue';
import TemplateDefinitionModal from './TemplateDefinitionModal.vue'; import TemplateDefinitionModal from './TemplateDefinitionModal.vue';
const { L } = useLocalization(['AbpTextTemplating', 'AbpUi']); const { deserialize } = useLocalizationSerializer();
const { L, Lr } = useLocalization(['AbpTextTemplating', 'AbpUi']);
const { createConfirm, createMessage } = useMessage(); const { createConfirm, createMessage } = useMessage();
const [registerEditModal, { openModal: openEditModal }] = useModal(); const [registerEditModal, { openModal: openEditModal }] = useModal();
const [registerContentModal, { openModal: openContentModal }] = useModal(); const [registerContentModal, { openModal: openContentModal }] = useModal();
const [registerTable, { reload }] = useTable({ const [registerTable, { getForm, setLoading, setTableData }] = useTable({
rowKey: 'name', rowKey: 'name',
title: L('TextTemplates'), title: L('TextTemplates'),
columns: getDataColumns(), columns: getDataColumns(),
api: GetListAsyncByInput,
beforeFetch: formatPagedRequest,
pagination: true, pagination: true,
striped: false, striped: false,
useSearchForm: true, useSearchForm: true,
formConfig: getSearchFormSchemas(), formConfig: {
labelWidth: 100,
schemas: [
{
field: 'filter',
component: 'Input',
label: L('Search'),
colProps: { span: 18 },
},
{
field: 'isLayout',
component: 'Checkbox',
label: L('DisplayName:IsLayout'),
colProps: { span: 6 },
renderComponentContent: L('DisplayName:IsLayout'),
},
],
submitFunc: fetch,
},
showIndexColumn: false, showIndexColumn: false,
showTableSetting: true, showTableSetting: true,
bordered: true, bordered: true,
canResize: true, canResize: true,
immediate: true, immediate: false,
actionColumn: { actionColumn: {
width: 150, width: 150,
title: L('Actions'), title: L('Actions'),
dataIndex: 'action', dataIndex: 'action',
}, },
}); });
const getDisplayName = computed(() => {
return (displayName?: string) => {
if (!displayName) return displayName;
const info = deserialize(displayName);
return Lr(info.resourceName, info.name);
};
});
onMounted(fetch);
async function fetch() {
try {
const form = getForm();
const input = await form.validate();
setLoading(true);
setTableData([]);
const { items } = await GetListAsyncByInput(input);
setTableData(items);
} finally {
setLoading(false);
}
}
function handleAddNew() { function handleAddNew() {
openEditModal(true, {}); openEditModal(true, {});
@ -122,7 +165,7 @@
onOk: () => { onOk: () => {
return DeleteAsyncByName(record.name).then(() => { return DeleteAsyncByName(record.name).then(() => {
createMessage.success(record.isStatic ? L('TemplateUpdated') : L('SuccessfullyDeleted')); createMessage.success(record.isStatic ? L('TemplateUpdated') : L('SuccessfullyDeleted'));
reload(); fetch();
}); });
}, },
}); });

173
apps/vue/src/views/text-templating/templates/datas/ModalData.ts

@ -1,173 +0,0 @@
import { useAbpStoreWithOut } from '/@/store/modules/abp';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { FormProps, FormSchema } from '/@/components/Form';
import { getList as getResources } from '/@/api/localization/resources';
import { getList as getLanguages } from '/@/api/localization/languages';
import { getList as getTexts } from '/@/api/localization/texts';
import { GetListAsyncByInput as getTemplateDefintions } from '/@/api/text-templating/definitions';
const abpStore = useAbpStoreWithOut();
const { L } = useLocalization(['AbpTextTemplating', 'AbpLocalization', 'AbpUi']);
export function getSearchFormSchemas(): Partial<FormProps> {
return {
labelWidth: 100,
schemas: [
{
field: 'filter',
component: 'Input',
label: L('Search'),
colProps: { span: 24 },
},
],
};
}
export function getModalFormSchemas(): FormSchema[] {
return [
{
field: 'concurrencyStamp',
component: 'Input',
label: 'concurrencyStamp',
colProps: { span: 24 },
show: false,
},
{
field: 'isInlineLocalized',
component: 'Checkbox',
label: L('DisplayName:IsInlineLocalized'),
colProps: { span: 24 },
defaultValue: false,
renderComponentContent: L('DisplayName:IsInlineLocalized'),
},
{
field: 'defaultCultureName',
component: 'ApiSelect',
label: L('DisplayName:DefaultCultureName'),
colProps: { span: 24 },
componentProps: {
api: getLanguages,
params: {
input: {
skipCount: 0,
maxResultCount: 100,
},
},
resultField: 'items',
labelField: 'displayName',
valueField: 'cultureName',
},
},
{
field: 'isLayout',
component: 'Checkbox',
label: L('DisplayName:IsLayout'),
colProps: { span: 24 },
defaultValue: false,
renderComponentContent: L('DisplayName:IsLayout'),
componentProps: ({ formActionType }) => {
return {
onChange: (e) => {
if (e.target.checked) {
formActionType.setFieldsValue({
layout: '',
});
}
}
}
},
},
{
field: 'layout',
component: 'ApiSelect',
label: L('DisplayName:Layout'),
colProps: { span: 24 },
ifShow: ({ values }) => {
return values.isLayout === false;
},
componentProps: {
api: getTemplateDefintions,
params: {
input: {
skipCount: 0,
maxResultCount: 100,
isLayout: true,
},
},
resultField: 'items',
labelField: 'displayName',
valueField: 'name',
},
},
{
field: 'name',
component: 'Input',
label: L('DisplayName:Name'),
colProps: { span: 24 },
required: true,
},
{
field: 'resource',
component: 'ApiSelect',
label: L('DisplayName:ResourceName'),
colProps: { span: 12 },
required: true,
componentProps: ({ formActionType }) => {
return {
api: getResources,
resultField: 'items',
labelField: 'displayName',
valueField: 'name',
showSearch: true,
filterOption: (input: string, option: any) => {
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0;
},
onChange: (val) => {
formActionType.setFieldsValue({
text: '',
});
getTexts({
cultureName: 'en',
targetCultureName: abpStore.getApplication.localization.currentCulture.cultureName,
resourceName: val,
filter: '',
onlyNull: false,
}).then((res) => {
formActionType.updateSchema({
field: 'text',
componentProps: {
options: res.items.map((item) => {
return {
label: item.targetValue,
value: item.key,
}
}),
}
});
});
},
getPopupContainer: (triggerNode) => triggerNode.parentNode,
style: {
width: '100%',
},
}
},
},
{
field: 'text',
component: 'Select',
label: L('DisplayName:DisplayName'),
colProps: { span: 12 },
required: true,
componentProps: {
showSearch: true,
filterOption: (input: string, option: any) => {
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0;
},
style: {
width: '100%',
},
},
},
];
}

10
apps/vue/src/views/text-templating/templates/datas/TableData.ts

@ -69,6 +69,16 @@ export function getDataColumns(): BasicColumn[] {
ellipsis: true, ellipsis: true,
resizable: true, resizable: true,
}, },
{
title: L('DisplayName:LocalizationResourceName'),
dataIndex: 'localizationResourceName',
align: 'left',
width: 150,
sorter: true,
ellipsis: true,
resizable: true,
defaultHidden: true,
},
{ {
title: L('DisplayName:RenderEngine'), title: L('DisplayName:RenderEngine'),
dataIndex: 'renderEngine', dataIndex: 'renderEngine',

201
apps/vue/src/views/webhooks/definitions/groups/components/GroupDefinitionModal.vue

@ -0,0 +1,201 @@
<template>
<BasicModal
@register="registerModal"
:title="L('GroupDefinitions')"
:can-fullscreen="false"
:width="600"
:height="500"
:close-func="handleBeforeClose"
@ok="handleSubmit"
>
<Form
ref="formRef"
:model="state.entity"
:rules="state.entityRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Tabs v-model:active-key="state.activeTab">
<TabPane key="basic" :tab="L('BasicInfo')">
<FormItem name="name" :label="L('DisplayName:Name')">
<Input :disabled="state.entityEditFlag && !state.allowedChange" :allow-clear="true" v-model:value="state.entity.name" />
</FormItem>
<FormItem name="displayName" :label="L('DisplayName:DisplayName')">
<LocalizableInput :disabled="!state.allowedChange" :allow-clear="true" v-model:value="state.entity.displayName" />
</FormItem>
</TabPane>
<TabPane key="propertites" :tab="L('Properties')">
<FormItem name="extraProperties" label="" :label-col="{ span: 0 }" :wrapper-col="{ span: 24 }">
<ExtraPropertyDictionary :disabled="!state.allowedChange" :allow-delete="true" :allow-edit="true" v-model:value="state.entity.extraProperties" />
</FormItem>
</TabPane>
</Tabs>
</Form>
</BasicModal>
</template>
<script setup lang="ts">
import type { Rule } from 'ant-design-vue/lib/form';
import { cloneDeep } from 'lodash-es';
import { ref, reactive, unref, nextTick, watch } from 'vue';
import { Form, Input, Tabs } from 'ant-design-vue';
import { ExtraPropertyDictionary } from '/@/components/Abp';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { LocalizableInput } from '/@/components/Abp';
import { useMessage } from '/@/hooks/web/useMessage';
import { ValidationEnum, useValidation } from '/@/hooks/abp/useValidation';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useLocalizationSerializer } from '/@/hooks/abp/useLocalizationSerializer';
import {
GetAsyncByName,
CreateAsyncByInput,
UpdateAsyncByNameAndInput
} from '/@/api/webhooks/definitions/groups';
import {
WebhookGroupDefinitionUpdateDto,
WebhookGroupDefinitionCreateDto
} from '/@/api/webhooks/definitions/groups/model';
const FormItem = Form.Item;
const TabPane = Tabs.TabPane;
interface State {
activeTab: string,
allowedChange: boolean,
entity: Recordable,
entityRules?: Dictionary<string, Rule>,
entityChanged: boolean,
entityEditFlag: boolean,
}
const emits = defineEmits(['register', 'change']);
const { ruleCreator } = useValidation();
const { validate } = useLocalizationSerializer();
const { createConfirm, createMessage } = useMessage();
const { L } = useLocalization(['WebhooksManagement', 'AbpUi']);
const formRef = ref<any>();
const state = reactive<State>({
activeTab: 'basic',
entity: {},
allowedChange: false,
entityChanged: false,
entityEditFlag: false,
entityRules: {
name: ruleCreator.fieldRequired({
name: 'Name',
prefix: 'DisplayName',
resourceName: 'WebhooksManagement',
trigger: 'blur',
}),
displayName: ruleCreator.defineValidator({
required: true,
trigger: 'blur',
validator(_rule, value) {
if (!validate(value)) {
return Promise.reject(L(ValidationEnum.FieldRequired, [L('DisplayName:DisplayName')]));
}
return Promise.resolve();
},
}),
description: ruleCreator.defineValidator({
trigger: 'blur',
validator(_rule, value) {
if (!validate(value, { required: false })) {
return Promise.reject(L(ValidationEnum.FieldRequired, [L('DisplayName:Description')]));
}
return Promise.resolve();
},
}),
},
});
watch(
() => state.entity,
() => {
state.entityChanged = true;
},
{
deep: true,
},
);
const [registerModal, { closeModal, changeLoading, changeOkLoading }] = useModalInner((record) => {
nextTick(() => fetch(record.name));
});
function fetch(name?: string) {
state.activeTab = 'basic';
state.entityEditFlag = false;
if (!name) {
state.entity = {
isInherited: true,
};
state.allowedChange = true;
nextTick(() => state.entityChanged = false);
return;
}
changeLoading(true);
changeOkLoading(true);
GetAsyncByName(name).then((record) => {
state.entity = record;
state.entityEditFlag = true;
state.allowedChange = !record.isStatic;
}).finally(() => {
changeLoading(false);
changeOkLoading(false);
nextTick(() => state.entityChanged = false);
});
}
function handleBeforeClose(): Promise<boolean> {
return new Promise((resolve) => {
if (!state.entityChanged) {
const form = unref(formRef);
form?.resetFields();
return resolve(true);
}
createConfirm({
iconType: 'warning',
title: L('AreYouSure'),
content: L('AreYouSureYouWantToCancelEditingWarningMessage'),
onOk: () => {
const form = unref(formRef);
form?.resetFields();
resolve(true);
},
onCancel: () => {
resolve(false);
},
afterClose: () => {
state.allowedChange = false;
},
});
});
}
function handleSubmit() {
if (!state.allowedChange) {
closeModal();
return;
}
const form = unref(formRef);
form?.validate().then(() => {
changeOkLoading(true);
const api = state.entityEditFlag
? UpdateAsyncByNameAndInput(state.entity.name, cloneDeep(state.entity) as WebhookGroupDefinitionUpdateDto)
: CreateAsyncByInput(cloneDeep(state.entity) as WebhookGroupDefinitionCreateDto);
api.then((res) => {
createMessage.success(L('Successful'));
emits('change', res);
form.resetFields();
closeModal();
}).finally(() => {
changeOkLoading(false);
})
});
}
</script>
<style scoped>
</style>

147
apps/vue/src/views/webhooks/definitions/groups/components/GroupDefinitionTable.vue

@ -0,0 +1,147 @@
<template>
<div>
<BasicTable @register="registerTable">
<template #toolbar>
<Button
v-auth="['AbpWebhooks.GroupDefinitions.Create']"
type="primary"
@click="handleAddNew"
>
{{ L('GroupDefinitions:AddNew') }}
</Button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'displayName'">
<span>{{ getDisplayName(record.displayName) }}</span>
</template>
<template v-else-if="column.key === 'action'">
<TableAction
:stop-button-propagation="true"
:actions="[
{
auth: 'AbpWebhooks.GroupDefinitions.Update',
label: L('Edit'),
icon: 'ant-design:edit-outlined',
onClick: handleEdit.bind(null, record),
},
{
auth: 'AbpWebhooks.GroupDefinitions.Delete',
label: L('Delete'),
color: 'error',
icon: 'ant-design:delete-outlined',
ifShow: !record.isStatic,
onClick: handleDelete.bind(null, record),
},
]"
:dropDownActions="[
{
auth: 'AbpWebhooks.Definitions.Create',
label: L('Webhooks:AddNew'),
icon: 'ant-design:edit-outlined',
onClick: handleAddWebhook.bind(null, record),
},
]"
/>
</template>
</template>
</BasicTable>
<GroupDefinitionModal @register="registerModal" @change="fetch" />
<WebhookDefinitionModal @register="registerWebhookModal" />
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted } from 'vue';
import { Button } from 'ant-design-vue';
import { BasicTable, TableAction, useTable } from '/@/components/Table';
import { getDataColumns } from '../datas/TableData';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { useLocalizationSerializer } from '/@/hooks/abp/useLocalizationSerializer';
import { GetListAsyncByInput, DeleteAsyncByName } from '/@/api/webhooks/definitions/groups';
import { getSearchFormSchemas } from '../datas/ModalData';
import GroupDefinitionModal from './GroupDefinitionModal.vue';
import WebhookDefinitionModal from '../../webhooks/components/WebhookDefinitionModal.vue';
const { deserialize } = useLocalizationSerializer();
const { L, Lr } = useLocalization(['WebhooksManagement', 'AbpUi']);
const { createConfirm, createMessage } = useMessage();
const [registerModal, { openModal }] = useModal();
const [registerWebhookModal, { openModal: openWebhookModal }] = useModal();
const [registerTable, { setLoading, getForm, setTableData }] = useTable({
rowKey: 'name',
title: L('GroupDefinitions'),
columns: getDataColumns(),
pagination: true,
striped: false,
useSearchForm: true,
showIndexColumn: false,
showTableSetting: true,
tableSetting: {
redo: false,
},
formConfig: {
labelWidth: 100,
submitFunc: fetch,
schemas: getSearchFormSchemas(),
},
bordered: true,
canResize: true,
immediate: false,
actionColumn: {
width: 120,
title: L('Actions'),
dataIndex: 'action',
},
});
const getDisplayName = computed(() => {
return (displayName?: string) => {
if (!displayName) return displayName;
const info = deserialize(displayName);
return Lr(info.resourceName, info.name);
};
});
onMounted(fetch);
function fetch() {
const form = getForm();
return form.validate().then(() => {
setLoading(true);
setTableData([]);
var input = form.getFieldsValue();
GetListAsyncByInput(input).then((res) => {
setTableData(res.items);
}).finally(() => {
setLoading(false);
});
});
}
function handleAddNew() {
openModal(true, {});
}
function handleEdit(record) {
openModal(true, record);
}
function handleAddWebhook(record) {
openWebhookModal(true, { groupName: record.name });
}
function handleDelete(record) {
createConfirm({
iconType: 'warning',
title: L('AreYouSure'),
content: L('ItemWillBeDeleteOrRestoreMessage'),
onOk: () => {
return DeleteAsyncByName(record.name).then(() => {
createMessage.success(L('Successful'));
fetch();
});
},
});
}
</script>

15
apps/vue/src/views/webhooks/definitions/groups/datas/ModalData.ts

@ -0,0 +1,15 @@
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { FormSchema } from '/@/components/Form';
const { L } = useLocalization(['AbpUi']);
export function getSearchFormSchemas():FormSchema[] {
return [
{
field: 'filter',
component: 'Input',
label: L('Search'),
colProps: { span: 24 },
},
];
}

35
apps/vue/src/views/webhooks/definitions/groups/datas/TableData.ts

@ -0,0 +1,35 @@
import { useLocalization } from '/@/hooks/abp/useLocalization';
import { BasicColumn } from '/@/components/Table';
import { sorter } from '/@/utils/table';
const { L } = useLocalization(['WebhooksManagement']);
export function getDataColumns(): BasicColumn[] {
return [
{
title: L('DisplayName:Name'),
dataIndex: 'name',
align: 'left',
width: 350,
resizable: true,
sorter: (a, b) => sorter(a, b, 'name'),
},
{
title: L('DisplayName:DisplayName'),
dataIndex: 'displayName',
align: 'left',
width: 180,
resizable: true,
sorter: (a, b) => sorter(a, b, 'displayName'),
},
{
title: L('DisplayName:IsStatic'),
dataIndex: 'isStatic',
align: 'center',
width: 150,
resizable: true,
defaultHidden: true,
sorter: (a, b) => sorter(a, b, 'isStatic'),
},
];
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save