diff --git a/apps/react-admin/.env.development b/apps/react-admin/.env.development index 8a1166a28..458a620c8 100644 --- a/apps/react-admin/.env.development +++ b/apps/react-admin/.env.development @@ -1,12 +1,25 @@ VITE_APP_BASE_API= VITE_APP_HOMEPAGE=/dashboard/workbench VITE_APP_BASE_PATH=/ + + +# ----------------------- +# 1 +# VITE_PROXY_API=http://192.168.31.24:30001 # milo # VITE_GLOB_CLIENT_ID=react-admin-client # VITE_GLOB_CLIENT_SECRET='' # VITE_GLOB_SCOPE="openid email address phone profile offline_access miwen-abp-application" -# VITE_PROXY_API=http://192.168.31.246:30001 + +# 2 +# VITE_PROXY_API=http://192.168.31.24:30000 # collin + +VITE_PROXY_API=http://124.223.5.95:30001/ #yun VITE_GLOB_CLIENT_ID=vue-admin-client VITE_GLOB_CLIENT_SECRET=1q2w3e* VITE_GLOB_SCOPE="openid email address phone profile offline_access lingyun-abp-application" -VITE_PROXY_API=http://124.223.5.95:30001 \ No newline at end of file + +# ----------------------- + +VITE_EXTERNAL_LOGIN_ADDRESS=http://localhost:30001/connect/external/login +VITE_REGISTER_ADDRESS=http://localhost:3100/ \ No newline at end of file diff --git a/apps/react-admin/.gitignore b/apps/react-admin/.gitignore index 6df0aabe7..4acd58200 100644 --- a/apps/react-admin/.gitignore +++ b/apps/react-admin/.gitignore @@ -24,5 +24,6 @@ dist-ssr # vite 打包分析产物 stats.html + # 取消上层忽略 !.vscode/ \ No newline at end of file diff --git a/apps/react-admin/biome.json b/apps/react-admin/biome.json index 47ef3986f..6e7b987bd 100644 --- a/apps/react-admin/biome.json +++ b/apps/react-admin/biome.json @@ -23,7 +23,8 @@ "rules": { "recommended": true, "suspicious": { - "noExplicitAny": "off" + "noExplicitAny": "off", + "noAssignInExpressions": "off" }, "a11y": { "useKeyWithClickEvents": "off" diff --git a/apps/react-admin/package.json b/apps/react-admin/package.json index 47f2243e5..6966950d9 100644 --- a/apps/react-admin/package.json +++ b/apps/react-admin/package.json @@ -46,6 +46,10 @@ "i18next": "^23.5.1", "i18next-browser-languagedetector": "^7.1.0", "json-edit-react": "^1.19.2", + "lodash.debounce": "^4.0.8", + "lodash.isdate": "^4.0.1", + "lodash.isnumber": "^3.0.3", + "lodash.orderby": "^4.6.0", "nprogress": "^0.2.0", "numeral": "^2.0.6", "ramda": "^0.29.1", @@ -78,9 +82,12 @@ "@commitlint/cli": "^17.7.2", "@commitlint/config-conventional": "^17.7.0", "@faker-js/faker": "^8.1.0", - "@hey-api/openapi-ts": "^0.59.1", "@types/autosuggest-highlight": "^3.2.0", "@types/color": "^3.0.4", + "@types/lodash.debounce": "^4.0.9", + "@types/lodash.isdate": "^4.0.9", + "@types/lodash.isnumber": "^3.0.9", + "@types/lodash.orderby": "^4.6.9", "@types/nprogress": "^0.2.1", "@types/numeral": "^2.0.3", "@types/ramda": "^0.29.6", diff --git a/apps/react-admin/src/api/account/account.ts b/apps/react-admin/src/api/account/account.ts index 8710dd7f1..f6b3b57ff 100644 --- a/apps/react-admin/src/api/account/account.ts +++ b/apps/react-admin/src/api/account/account.ts @@ -4,6 +4,8 @@ import type { TwoFactorProvider, SendEmailSigninCodeDto, SendPhoneSigninCodeDto, + ExternalSignUpApiDto, + PhoneResetPasswordDto, } from "#/account/account"; import requestClient from "@/api/request"; @@ -26,3 +28,9 @@ export const sendEmailSigninCodeApi = (input: SendEmailSigninCodeDto) => */ export const sendPhoneSigninCodeApi = (input: SendPhoneSigninCodeDto) => requestClient.post("/api/account/phone/send-signin-code", input); + +export const externalSignUpApi = (input: ExternalSignUpApiDto) => + requestClient.post("/api/account/external/register", input, { withCredentials: true }); + +export const resetPasswordApi = (input: PhoneResetPasswordDto) => + requestClient.put("/api/account/phone/reset-password", input); diff --git a/apps/react-admin/src/api/account/profile.ts b/apps/react-admin/src/api/account/profile.ts index 221bb3761..76f4092ba 100644 --- a/apps/react-admin/src/api/account/profile.ts +++ b/apps/react-admin/src/api/account/profile.ts @@ -8,6 +8,9 @@ import type { AuthenticatorRecoveryCodeDto, SendEmailConfirmCodeDto, ConfirmEmailInput, + SendChangePhoneNumberCodeInput, + ChangePhoneNumberInput, + ChangePictureInput, } from "#/account/profile"; import requestClient from "@/api/request"; @@ -27,6 +30,40 @@ export const updateApi = (input: UpdateProfileDto) => requestClient.put requestClient.post("/api/account/my-profile/change-password", input); +/** + * 发送修改手机号验证码 + * @param input 参数 + */ +export const sendChangePhoneNumberCodeApi = (input: SendChangePhoneNumberCodeInput) => + requestClient.post("/api/account/my-profile/send-phone-number-change-code", input); +/** + * 修改手机号 + * @param input 参数 + */ +export const changePhoneNumberApi = (input: ChangePhoneNumberInput) => + requestClient.put("/api/account/my-profile/change-phone-number", input); + +/** + * 修改头像 + * @param input 参数 + */ +export const changePictureApi = (input: ChangePictureInput) => { + requestClient.post("/api/account/my-profile/picture", input, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); +}; + +/** + * 获取头像 + * @returns 头像文件流 + */ +export const getPictureApi = () => + requestClient.get("/api/account/my-profile/picture", { + responseType: "blob", + }); + /** * Get two-factor authentication status */ diff --git a/apps/react-admin/src/api/account/scan-Qrcode.ts b/apps/react-admin/src/api/account/scan-Qrcode.ts new file mode 100644 index 000000000..043464cf9 --- /dev/null +++ b/apps/react-admin/src/api/account/scan-Qrcode.ts @@ -0,0 +1,20 @@ +import type { GenerateQrCodeResult, QrCodeUserInfoResult } from "#/account/qrcode"; + +import requestClient from "@/api/request"; + +/** + * 生成登录二维码 + * @returns 二维码信息 + */ +export function generateApi(): Promise { + return requestClient.post("/api/account/qrcode/generate"); +} + +/** + * 检查二维码状态 + * @param key 二维码Key + * @returns 二维码信息 + */ +export function checkCodeApi(key: string): Promise { + return requestClient.get(`/api/account/qrcode/${key}/check`); +} diff --git a/apps/react-admin/src/api/account/token.ts b/apps/react-admin/src/api/account/token.ts index 7cc52222f..01d2ca5b4 100644 --- a/apps/react-admin/src/api/account/token.ts +++ b/apps/react-admin/src/api/account/token.ts @@ -1,4 +1,10 @@ -import type { OAuthTokenResult, PasswordTokenRequestModel, RefreshTokenRequestModel, TokenResult } from "#/account"; +import type { + OAuthTokenResult, + PasswordTokenRequestModel, + RefreshTokenRequestModel, + SignInRedirectResult, + TokenResult, +} from "#/account"; import requestClient from "../request"; /** @@ -35,6 +41,11 @@ export async function loginApi(request: PasswordTokenRequestModel): Promise { const clientId = import.meta.env.VITE_GLOB_CLIENT_ID; const clientSecret = import.meta.env.VITE_GLOB_CLIENT_SECRET; @@ -61,3 +72,48 @@ export async function refreshToken(request: RefreshTokenRequestModel): Promise { + const clientId = import.meta.env.VITE_GLOB_CLIENT_ID; + const clientSecret = import.meta.env.VITE_GLOB_CLIENT_SECRET; + const scope = import.meta.env.VITE_GLOB_SCOPE; + const registerAddress = import.meta.env.VITE_REGISTER_ADDRESS; + + //import: https://stackoverflow.com/questions/61345366/axios-302-responses + const res = await fetch("/connect/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + credentials: "include", // 让请求带上 Cookie + body: new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + grant_type: "ExternalLogin", + scope: scope, + register_address: registerAddress, + }), + }); + + if (res.redirected) { + const redirectUrl = new URL(res.url); + const searchParams = new URLSearchParams(redirectUrl.search); + const isExternalLogin = searchParams.get("isExternalLogin") === "true"; + const needRegister = searchParams.get("needRegister") === "true"; + return { + isExternalLogin, + needRegister, + // redirectUrl //这个react项目这里不需要 + }; + } + const result = await res.json(); + return { + accessToken: result.access_token, + expiresIn: result.expires_in, + refreshToken: result.refresh_token, + tokenType: result.token_type, + }; +} diff --git a/apps/react-admin/src/api/management/auditing/loggings.ts b/apps/react-admin/src/api/management/auditing/loggings.ts new file mode 100644 index 000000000..2e01a8107 --- /dev/null +++ b/apps/react-admin/src/api/management/auditing/loggings.ts @@ -0,0 +1,25 @@ +import type { PagedResultDto } from "#/abp-core"; + +import type { LogDto, LogGetListInput } from "#/management/auditing"; + +import requestClient from "../../request"; + +/** + * 获取系统日志 + * @param id 日志id + */ +export function getApi(id: string): Promise { + return requestClient.get(`/api/auditing/logging/${id}`, { + method: "GET", + }); +} + +/** + * 获取系统日志分页列表 + * @param input 参数 + */ +export function getPagedListApi(input: LogGetListInput): Promise> { + return requestClient.get>("/api/auditing/logging", { + params: input, + }); +} diff --git a/apps/react-admin/src/api/management/features/feature-definitions.ts b/apps/react-admin/src/api/management/features/feature-definitions.ts new file mode 100644 index 000000000..f068e6508 --- /dev/null +++ b/apps/react-admin/src/api/management/features/feature-definitions.ts @@ -0,0 +1,57 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { + FeatureDefinitionCreateDto, + FeatureDefinitionDto, + FeatureDefinitionGetListInput, + FeatureDefinitionUpdateDto, +} from "#/management/features/definitions"; + +import requestClient from "../../request"; + +/** + * 删除功能定义 + * @param name 功能名称 + */ +export function deleteApi(name: string): Promise { + return requestClient.delete(`/api/feature-management/definitions/${name}`); +} + +/** + * 查询功能定义 + * @param name 功能名称 + * @returns 功能定义数据传输对象 + */ +export function getApi(name: string): Promise { + return requestClient.get(`/api/feature-management/definitions/${name}`); +} + +/** + * 查询功能定义列表 + * @param input 功能过滤条件 + * @returns 功能定义数据传输对象列表 + */ +export function getListApi(input?: FeatureDefinitionGetListInput): Promise> { + return requestClient.get>("/api/feature-management/definitions", { + params: input, + }); +} + +/** + * 创建功能定义 + * @param input 功能定义参数 + * @returns 功能定义数据传输对象 + */ +export function createApi(input: FeatureDefinitionCreateDto): Promise { + return requestClient.post("/api/feature-management/definitions", input); +} + +/** + * 更新功能定义 + * @param name 功能名称 + * @param input 功能定义参数 + * @returns 功能定义数据传输对象 + */ +export function updateApi(name: string, input: FeatureDefinitionUpdateDto): Promise { + return requestClient.put(`/api/feature-management/definitions/${name}`, input); +} diff --git a/apps/react-admin/src/api/management/features/feature-group-definitions.ts b/apps/react-admin/src/api/management/features/feature-group-definitions.ts new file mode 100644 index 000000000..279df7da7 --- /dev/null +++ b/apps/react-admin/src/api/management/features/feature-group-definitions.ts @@ -0,0 +1,59 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { + FeatureGroupDefinitionCreateDto, + FeatureGroupDefinitionDto, + FeatureGroupDefinitionGetListInput, + FeatureGroupDefinitionUpdateDto, +} from "#/management/features/groups"; + +import requestClient from "../../request"; + +/** + * 删除功能定义 + * @param name 功能名称 + */ +export function deleteApi(name: string): Promise { + return requestClient.delete(`/api/feature-management/definitions/groups/${name}`); +} + +/** + * 查询功能定义 + * @param name 功能名称 + * @returns 功能定义数据传输对象 + */ +export function getApi(name: string): Promise { + return requestClient.get(`/api/feature-management/definitions/groups/${name}`); +} + +/** + * 查询功能定义列表 + * @param input 功能过滤条件 + * @returns 功能定义数据传输对象列表 + */ +export function getListApi( + input?: FeatureGroupDefinitionGetListInput, +): Promise> { + return requestClient.get>("/api/feature-management/definitions/groups", { + params: input, + }); +} + +/** + * 创建功能定义 + * @param input 功能定义参数 + * @returns 功能定义数据传输对象 + */ +export function createApi(input: FeatureGroupDefinitionCreateDto): Promise { + return requestClient.post("/api/feature-management/definitions/groups", input); +} + +/** + * 更新功能定义 + * @param name 功能名称 + * @param input 功能定义参数 + * @returns 功能定义数据传输对象 + */ +export function updateApi(name: string, input: FeatureGroupDefinitionUpdateDto): Promise { + return requestClient.put(`/api/feature-management/definitions/groups/${name}`, input); +} diff --git a/apps/react-admin/src/api/management/features/features.ts b/apps/react-admin/src/api/management/features/features.ts new file mode 100644 index 000000000..9b763b961 --- /dev/null +++ b/apps/react-admin/src/api/management/features/features.ts @@ -0,0 +1,37 @@ +import type { FeatureProvider, GetFeatureListResultDto, UpdateFeaturesDto } from "#/management/features/features"; + +import requestClient from "../../request"; + +/** + * 删除功能 + * @param {FeatureProvider} provider 参数 + * @returns {Promise} + */ +export function deleteApi(provider: FeatureProvider): Promise { + return requestClient.delete("/api/feature-management/features", { + params: provider, + }); +} + +/** + * 查询功能 + * @param {FeatureProvider} provider 参数 + * @returns {Promise} 功能实体数据传输对象 + */ +export function getApi(provider: FeatureProvider): Promise { + return requestClient.get("/api/feature-management/features", { + params: provider, + }); +} + +/** + * 更新功能 + * @param {FeatureProvider} provider + * @param {UpdateFeaturesDto} input 参数 + * @returns {Promise} + */ +export function updateApi(provider: FeatureProvider, input: UpdateFeaturesDto): Promise { + return requestClient.put("/api/feature-management/features", input, { + params: provider, + }); +} diff --git a/apps/react-admin/src/api/management/localization/languages.ts b/apps/react-admin/src/api/management/localization/languages.ts new file mode 100644 index 000000000..06b4a43a2 --- /dev/null +++ b/apps/react-admin/src/api/management/localization/languages.ts @@ -0,0 +1,57 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { + LanguageCreateDto, + LanguageDto, + LanguageGetListInput, + LanguageUpdateDto, +} from "#/management/localization/languages"; + +import requestClient from "../../request"; + +/** + * 查询语言列表 + * @param input 参数 + * @returns 语言列表 + */ +export function getListApi(input?: LanguageGetListInput): Promise> { + return requestClient.get>("/api/abp/localization/languages", { + params: input, + }); +} + +/** + * 查询语言 + * @param name 语言名称 + * @returns 查询的语言 + */ +export function getApi(name: string): Promise { + return requestClient.get(`/api/localization/languages/${name}`); +} + +/** + * 删除语言 + * @param name 语言名称 + */ +export function deleteApi(name: string): Promise { + return requestClient.delete(`/api/localization/languages/${name}`); +} + +/** + * 创建语言 + * @param input 参数 + * @returns 创建的语言 + */ +export function createApi(input: LanguageCreateDto): Promise { + return requestClient.post("/api/localization/languagesr", input); +} + +/** + * 编辑语言 + * @param name 语言名称 + * @param input 参数 + * @returns 编辑的语言 + */ +export function updateApi(name: string, input: LanguageUpdateDto): Promise { + return requestClient.put(`/api/localization/languages/${name}`, input); +} diff --git a/apps/react-admin/src/api/management/localization/localizations.ts b/apps/react-admin/src/api/management/localization/localizations.ts new file mode 100644 index 000000000..37e787f23 --- /dev/null +++ b/apps/react-admin/src/api/management/localization/localizations.ts @@ -0,0 +1,16 @@ +import type { ApplicationLocalizationDto } from "#/abp-core"; + +import requestClient from "../../request"; + +/** + * 获取应用程序语言 + * @returns 本地化配置 + */ +export function getLocalizationApi(options: { + cultureName: string; + onlyDynamics?: boolean; +}): Promise { + return requestClient.get("/api/abp/application-localization", { + params: options, + }); +} diff --git a/apps/react-admin/src/api/management/localization/resources.ts b/apps/react-admin/src/api/management/localization/resources.ts new file mode 100644 index 000000000..b399f1417 --- /dev/null +++ b/apps/react-admin/src/api/management/localization/resources.ts @@ -0,0 +1,57 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { + ResourceCreateDto, + ResourceDto, + ResourceGetListInput, + ResourceUpdateDto, +} from "#/management/localization/resources"; + +import requestClient from "../../request"; + +/** + * 查询资源列表 + * @param input 参数 + * @returns 资源列表 + */ +export function getListApi(input?: ResourceGetListInput): Promise> { + return requestClient.get>("/api/abp/localization/resources", { + params: input, + }); +} + +/** + * 查询资源 + * @param name 资源名称 + * @returns 查询的资源 + */ +export function getApi(name: string): Promise { + return requestClient.get(`/api/localization/resources/${name}`); +} + +/** + * 删除资源 + * @param name 资源名称 + */ +export function deleteApi(name: string): Promise { + return requestClient.delete(`/api/localization/resources/${name}`); +} + +/** + * 创建资源 + * @param input 参数 + * @returns 创建的资源 + */ +export function createApi(input: ResourceCreateDto): Promise { + return requestClient.post("/api/localization/resources", input); +} + +/** + * 编辑资源 + * @param name 资源名称 + * @param input 参数 + * @returns 编辑的资源 + */ +export function updateApi(name: string, input: ResourceUpdateDto): Promise { + return requestClient.put(`/api/localization/resources/${name}`, input); +} diff --git a/apps/react-admin/src/api/management/localization/texts.ts b/apps/react-admin/src/api/management/localization/texts.ts new file mode 100644 index 000000000..b6d5346d0 --- /dev/null +++ b/apps/react-admin/src/api/management/localization/texts.ts @@ -0,0 +1,41 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { + GetTextByKeyInput, + GetTextsInput, + SetTextInput, + TextDifferenceDto, + TextDto, +} from "#/management/localization/texts"; + +import requestClient from "../../request"; + +/** + * 查询文本列表 + * @param input 参数 + * @returns 文本列表 + */ +export function getListApi(input: GetTextsInput): Promise> { + return requestClient.get>("/api/abp/localization/texts", { + params: input, + }); +} + +/** + * 查询文本 + * @param input 参数 + * @returns 查询的文本 + */ +export function getApi(input: GetTextByKeyInput): Promise { + return requestClient.get("/api/abp/localization/texts/by-culture-key", { + params: input, + }); +} + +/** + * 设置文本 + * @param input 参数 + */ +export function setApi(input: SetTextInput): Promise { + return requestClient.put("/api/localization/texts", input); +} diff --git a/apps/react-admin/src/api/management/notifications/my-subscribes.ts b/apps/react-admin/src/api/management/notifications/my-subscribes.ts new file mode 100644 index 000000000..1aad9d1f6 --- /dev/null +++ b/apps/react-admin/src/api/management/notifications/my-subscribes.ts @@ -0,0 +1,31 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { UserSubscreNotification } from "#/notifications/subscribes"; + +import requestClient from "../../request"; + +/** + * 获取我的所有订阅通知 + * @returns 订阅通知列表 + */ +export function getMySubscribesApi(): Promise> { + return requestClient.get>("/api/notifications/my-subscribes/all"); +} + +/** + * 订阅通知 + * @param name 通知名称 + */ +export function subscribeApi(name: string): Promise { + return requestClient.post("/api/notifications/my-subscribes", { + name, + }); +} + +/** + * 取消订阅通知 + * @param name 通知名称 + */ +export function unSubscribeApi(name: string): Promise { + return requestClient.delete(`/api/notifications/my-subscribes?name=${name}`); +} diff --git a/apps/react-admin/src/api/management/notifications/notification-definitions.ts b/apps/react-admin/src/api/management/notifications/notification-definitions.ts new file mode 100644 index 000000000..ab1d49829 --- /dev/null +++ b/apps/react-admin/src/api/management/notifications/notification-definitions.ts @@ -0,0 +1,59 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { + NotificationDefinitionCreateDto, + NotificationDefinitionDto, + NotificationDefinitionGetListInput, + NotificationDefinitionUpdateDto, +} from "#/notifications/definitions"; + +import requestClient from "../../request"; + +/** + * 删除通知定义 + * @param name 通知名称 + */ +export function deleteApi(name: string): Promise { + return requestClient.delete(`/api/notifications/definitions/notifications/${name}`); +} + +/** + * 查询通知定义 + * @param name 通知名称 + * @returns 通知定义数据传输对象 + */ +export function getApi(name: string): Promise { + return requestClient.get(`/api/notifications/definitions/notifications/${name}`); +} + +/** + * 查询通知定义列表 + * @param input 通知过滤条件 + * @returns 通知定义数据传输对象列表 + */ +export function getListApi( + input?: NotificationDefinitionGetListInput, +): Promise> { + return requestClient.get>("/api/notifications/definitions/notifications", { + params: input, + }); +} + +/** + * 创建通知定义 + * @param input 通知定义参数 + * @returns 通知定义数据传输对象 + */ +export function createApi(input: NotificationDefinitionCreateDto): Promise { + return requestClient.post("/api/notifications/definitions/notifications", input); +} + +/** + * 更新通知定义 + * @param name 通知名称 + * @param input 通知定义参数 + * @returns 通知定义数据传输对象 + */ +export function updateApi(name: string, input: NotificationDefinitionUpdateDto): Promise { + return requestClient.put(`/api/notifications/definitions/notifications/${name}`, input); +} diff --git a/apps/react-admin/src/api/management/notifications/notification-group-definitions.ts b/apps/react-admin/src/api/management/notifications/notification-group-definitions.ts new file mode 100644 index 000000000..65be29041 --- /dev/null +++ b/apps/react-admin/src/api/management/notifications/notification-group-definitions.ts @@ -0,0 +1,62 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { + NotificationGroupDefinitionCreateDto, + NotificationGroupDefinitionDto, + NotificationGroupDefinitionGetListInput, + NotificationGroupDefinitionUpdateDto, +} from "#/notifications/groups"; + +import requestClient from "../../request"; + +/** + * 删除通知分组定义 + * @param name 通知分组名称 + */ +export function deleteApi(name: string): Promise { + return requestClient.delete(`/api/notifications/definitions/groups/${name}`); +} + +/** + * 查询通知分组定义 + * @param name 通知分组名称 + * @returns 通知分组定义数据传输对象 + */ +export function getApi(name: string): Promise { + return requestClient.get(`/api/notifications/definitions/groups/${name}`); +} + +/** + * 查询通知分组定义列表 + * @param input 通知分组过滤条件 + * @returns 通知分组定义数据传输对象列表 + */ +export function getListApi( + input?: NotificationGroupDefinitionGetListInput, +): Promise> { + return requestClient.get>("/api/notifications/definitions/groups", { + params: input, + }); +} + +/** + * 创建通知分组定义 + * @param input 通知分组定义参数 + * @returns 通知分组定义数据传输对象 + */ +export function createApi(input: NotificationGroupDefinitionCreateDto): Promise { + return requestClient.post("/api/notifications/definitions/groups", input); +} + +/** + * 更新通知分组定义 + * @param name 通知分组名称 + * @param input 通知分组定义参数 + * @returns 通知分组定义数据传输对象 + */ +export function updateApi( + name: string, + input: NotificationGroupDefinitionUpdateDto, +): Promise { + return requestClient.put(`/api/notifications/definitions/groups/${name}`, input); +} diff --git a/apps/react-admin/src/api/management/notifications/notifications.ts b/apps/react-admin/src/api/management/notifications/notifications.ts index a0121fe4f..72ca6a5d9 100644 --- a/apps/react-admin/src/api/management/notifications/notifications.ts +++ b/apps/react-admin/src/api/management/notifications/notifications.ts @@ -1,10 +1,22 @@ import type { ListResultDto } from "#/abp-core"; -import type { NotificationGroupDto, NotificationTemplateDto } from "#/notifications/definitions"; +import type { + NotificationGroupDto, + NotificationProviderDto, + NotificationTemplateDto, +} from "#/notifications/definitions"; import type { NotificationSendInput, NotificationTemplateSendInput } from "#/notifications/notifications"; import requestClient from "@/api/request"; +/** + * 获取可用通知提供者列表 + * @returns {Promise>} 可用通知提供者列表 + */ +export function getAssignableProvidersApi(): Promise> { + return requestClient.get>("/api/notifications/assignable-providers"); +} + /** * 获取可用通知列表 * @returns {Promise>} 可用通知列表 diff --git a/apps/react-admin/src/api/oss/containes.ts b/apps/react-admin/src/api/oss/containes.ts new file mode 100644 index 000000000..fa96a1c2c --- /dev/null +++ b/apps/react-admin/src/api/oss/containes.ts @@ -0,0 +1,33 @@ +import type { + GetOssContainersInput, + GetOssObjectsInput, + OssContainerDto, + OssContainersResultDto, +} from "#/oss/containes"; +import type { OssObjectsResultDto } from "#/oss/objects"; + +import requestClient from "../request"; + +export function deleteApi(name: string): Promise { + return requestClient.delete(`/api/oss-management/containes/${name}`); +} + +export function getApi(name: string): Promise { + return requestClient.get(`/api/oss-management/containes/${name}`); +} + +export function getListApi(input?: GetOssContainersInput): Promise { + return requestClient.get("/api/oss-management/containes", { + params: input, + }); +} + +export function getObjectsApi(input: GetOssObjectsInput): Promise { + return requestClient.get("/api/oss-management/containes/objects", { + params: input, + }); +} + +export function createApi(name: string): Promise { + return requestClient.post(`/api/oss-management/containes/${name}`); +} diff --git a/apps/react-admin/src/api/oss/objects.ts b/apps/react-admin/src/api/oss/objects.ts new file mode 100644 index 000000000..40284abc8 --- /dev/null +++ b/apps/react-admin/src/api/oss/objects.ts @@ -0,0 +1,32 @@ +import type { BulkDeleteOssObjectInput, CreateOssObjectInput, GetOssObjectInput, OssObjectDto } from "#/oss/objects"; + +import requestClient from "../request"; + +export function createApi(input: CreateOssObjectInput): Promise { + const formData = new window.FormData(); + formData.append("bucket", input.bucket); + formData.append("fileName", input.fileName); + formData.append("overwrite", String(input.overwrite)); + input.expirationTime && formData.append("expirationTime", input.expirationTime.toString()); + input.path && formData.append("path", input.path); + input.file && formData.append("file", input.file); + + return requestClient.post("/api/oss-management/objects", formData, { + headers: { + "Content-Type": "multipart/form-data;charset=utf-8", + }, + }); +} + +export function generateUrlApi(input: GetOssObjectInput): Promise { + // return requestClient.get("/api/oss-management/objects/generate-url", { TODO update + return requestClient.get("/api/oss-management/objects/download", { + params: input, + }); +} + +export function deleteApi(input: BulkDeleteOssObjectInput): Promise { + return requestClient.delete("/api/oss-management/objects", { + params: input, + }); +} diff --git a/apps/react-admin/src/api/platform/data-dictionaries.ts b/apps/react-admin/src/api/platform/data-dictionaries.ts new file mode 100644 index 000000000..650bee96a --- /dev/null +++ b/apps/react-admin/src/api/platform/data-dictionaries.ts @@ -0,0 +1,59 @@ +import type { ListResultDto, PagedResultDto } from "#/abp-core"; + +import type { + DataCreateDto, + DataDto, + DataItemCreateDto, + DataItemUpdateDto, + DataMoveDto, + DataUpdateDto, + GetDataListInput, +} from "#/platform/data-dictionaries"; + +import requestClient from "../request"; + +export function createApi(input: DataCreateDto): Promise { + return requestClient.post("/api/platform/datas", input); +} + +export function deleteApi(id: string): Promise { + return requestClient.delete(`/api/platform/datas/${id}`); +} + +export function createItemApi(id: string, input: DataItemCreateDto): Promise { + return requestClient.post(`/api/platform/datas/${id}/items`, input); +} + +export function deleteItemApi(id: string, name: string): Promise { + return requestClient.delete(`/api/platform/datas/${id}/items/${name}`); +} + +export function getApi(id: string): Promise { + return requestClient.get(`/api/platform/datas/${id}`); +} + +export function getByNameApi(name: string): Promise { + return requestClient.get(`/api/platform/datas/by-name/${name}`); +} + +export function getAllApi(): Promise> { + return requestClient.get>("/api/platform/datas/all"); +} + +export function getPagedListApi(input?: GetDataListInput): Promise> { + return requestClient.get>("/api/platform/datas", { + params: input, + }); +} + +export function moveApi(id: string, input: DataMoveDto): Promise { + return requestClient.put(`/api/platform/datas/${id}/move`, input); +} + +export function updateApi(id: string, input: DataUpdateDto): Promise { + return requestClient.put(`/api/platform/datas/${id}`, input); +} + +export function updateItemApi(id: string, name: string, input: DataItemUpdateDto): Promise { + return requestClient.put(`/api/platform/datas/${id}/items/${name}`, input); +} diff --git a/apps/react-admin/src/api/platform/email-messages.ts b/apps/react-admin/src/api/platform/email-messages.ts new file mode 100644 index 000000000..c6ac50e7e --- /dev/null +++ b/apps/react-admin/src/api/platform/email-messages.ts @@ -0,0 +1,43 @@ +import type { PagedResultDto } from "#/abp-core"; + +import type { EmailMessageDto, EmailMessageGetListInput } from "#/platform/messages"; + +import requestClient from "../request"; + +/** + * 获取邮件消息分页列表 + * @param {EmailMessageGetListInput} input 参数 + * @returns {Promise>} 邮件消息列表 + */ +export function getPagedListApi(input?: EmailMessageGetListInput): Promise> { + return requestClient.get>("/api/platform/messages/email", { + params: input, + }); +} + +/** + * 获取邮件消息 + * @param id Id + * @returns {EmailMessageDto} 邮件消息 + */ +export function getApi(id: string): Promise { + return requestClient.get(`/api/platform/messages/email/${id}`); +} + +/** + * 删除邮件消息 + * @param id Id + * @returns {void} + */ +export function deleteApi(id: string): Promise { + return requestClient.delete(`/api/platform/messages/email/${id}`); +} + +/** + * 发送邮件消息 + * @param id Id + * @returns {void} + */ +export function sendApi(id: string): Promise { + return requestClient.post(`/api/platform/messages/email/${id}/send`); +} diff --git a/apps/react-admin/src/api/platform/layouts.ts b/apps/react-admin/src/api/platform/layouts.ts new file mode 100644 index 000000000..af5662ff3 --- /dev/null +++ b/apps/react-admin/src/api/platform/layouts.ts @@ -0,0 +1,27 @@ +import type { PagedResultDto } from "#/abp-core"; + +import type { LayoutCreateDto, LayoutDto, LayoutGetPagedListInput, LayoutUpdateDto } from "#/platform/layouts"; + +import requestClient from "../request"; + +export function createApi(input: LayoutCreateDto): Promise { + return requestClient.post("/api/platform/layouts", input); +} + +export function getPagedListApi(input?: LayoutGetPagedListInput): Promise> { + return requestClient.get>("/api/platform/layouts", { + params: input, + }); +} + +export function getApi(id: string): Promise { + return requestClient.get(`/api/platform/layouts/${id}`); +} + +export function deleteApi(id: string): Promise { + return requestClient.delete(`/api/platform/layouts/${id}`); +} + +export function updateApi(id: string, input: LayoutUpdateDto): Promise { + return requestClient.put(`/api/platform/layouts/${id}`, input); +} diff --git a/apps/react-admin/src/api/platform/menus.ts b/apps/react-admin/src/api/platform/menus.ts new file mode 100644 index 000000000..8c3aa790d --- /dev/null +++ b/apps/react-admin/src/api/platform/menus.ts @@ -0,0 +1,27 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { MenuCreateDto, MenuDto, MenuGetAllInput, MenuUpdateDto } from "#/platform/menus"; + +import requestClient from "../request"; + +export function createApi(input: MenuCreateDto): Promise { + return requestClient.post("/api/platform/menus", input); +} + +export function getAllApi(input?: MenuGetAllInput): Promise> { + return requestClient.get>("/api/platform/menus/all", { + params: input, + }); +} + +export function getApi(id: string): Promise { + return requestClient.get(`/api/platform/menus/${id}`); +} + +export function deleteApi(id: string): Promise { + return requestClient.delete(`/api/platform/menus/${id}`); +} + +export function updateApi(id: string, input: MenuUpdateDto): Promise { + return requestClient.put(`/api/platform/menus/${id}`, input); +} diff --git a/apps/react-admin/src/api/platform/my-favorite-menus.ts b/apps/react-admin/src/api/platform/my-favorite-menus.ts new file mode 100644 index 000000000..e8f35765d --- /dev/null +++ b/apps/react-admin/src/api/platform/my-favorite-menus.ts @@ -0,0 +1,33 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { UserFavoriteMenuCreateDto, UserFavoriteMenuDto } from "#/platform/favorites"; + +import requestClient from "../request"; + +/** + * 新增常用菜单 + * @param input 参数 + * @returns 常用菜单 + */ +export function createApi(input: UserFavoriteMenuCreateDto): Promise { + return requestClient.post("/api/platform/menus/favorites/my-favorite-menus", input); +} + +/** + * 删除常用菜单 + * @param id 菜单Id + */ +export function deleteApi(id: string): Promise { + return requestClient.delete(`/api/platform/menus/favorites/my-favorite-menus/${id}`); +} + +/** + * 获取常用菜单列表 + * @param framework ui框架 + * @returns 菜单列表 + */ +export function getListApi(framework?: string): Promise> { + return requestClient.get>("/api/platform/menus/favorites/my-favorite-menus", { + params: { framework }, + }); +} diff --git a/apps/react-admin/src/api/platform/my-menus.ts b/apps/react-admin/src/api/platform/my-menus.ts new file mode 100644 index 000000000..34a69fe8a --- /dev/null +++ b/apps/react-admin/src/api/platform/my-menus.ts @@ -0,0 +1,11 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { MenuDto, MenuGetInput } from "#/platform/menus"; + +import requestClient from "../request"; + +export function getAllApi(input?: MenuGetInput): Promise> { + return requestClient.get>("/api/platform/menus/by-current-user", { + params: input, + }); +} diff --git a/apps/react-admin/src/api/platform/role-menus.ts b/apps/react-admin/src/api/platform/role-menus.ts new file mode 100644 index 000000000..3e2141941 --- /dev/null +++ b/apps/react-admin/src/api/platform/role-menus.ts @@ -0,0 +1,19 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { MenuDto, MenuGetByRoleInput, SetRoleMenuInput, SetRoleMenuStartupInput } from "#/platform/menus"; + +import requestClient from "../request"; + +export function getAllApi(input: MenuGetByRoleInput): Promise> { + return requestClient.get>(`/api/platform/menus/by-role/${input.role}/${input.framework}`, { + params: input, + }); +} + +export function setMenusApi(input: SetRoleMenuInput): Promise { + return requestClient.put("/api/platform/menus/by-role", input); +} + +export function setStartupMenuApi(meudId: string, input: SetRoleMenuStartupInput): Promise { + return requestClient.put(`/api/platform/menus/startup/${meudId}/by-role`, input); +} diff --git a/apps/react-admin/src/api/platform/sms-messages.ts b/apps/react-admin/src/api/platform/sms-messages.ts new file mode 100644 index 000000000..40db0badf --- /dev/null +++ b/apps/react-admin/src/api/platform/sms-messages.ts @@ -0,0 +1,43 @@ +import type { PagedResultDto } from "#/abp-core"; + +import type { SmsMessageDto, SmsMessageGetListInput } from "#/platform/messages"; + +import requestClient from "../request"; + +/** + * 获取短信消息分页列表 + * @param {EmailMessageGetListInput} input 参数 + * @returns {Promise>} 短信消息列表 + */ +export function getPagedListApi(input?: SmsMessageGetListInput): Promise> { + return requestClient.get>("/api/platform/messages/sms", { + params: input, + }); +} + +/** + * 获取短信消息 + * @param id Id + * @returns {SmsMessageDto} 短信消息 + */ +export function getApi(id: string): Promise { + return requestClient.get(`/api/platform/messages/sms/${id}`); +} + +/** + * 删除短信消息 + * @param id Id + * @returns {void} + */ +export function deleteApi(id: string): Promise { + return requestClient.delete(`/api/platform/messages/sms/${id}`); +} + +/** + * 发送短信消息 + * @param id Id + * @returns {void} + */ +export function sendApi(id: string): Promise { + return requestClient.post(`/api/platform/messages/sms/${id}/send`); +} diff --git a/apps/react-admin/src/api/platform/user-menus.ts b/apps/react-admin/src/api/platform/user-menus.ts new file mode 100644 index 000000000..3fa8db123 --- /dev/null +++ b/apps/react-admin/src/api/platform/user-menus.ts @@ -0,0 +1,19 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { MenuDto, MenuGetByUserInput, SetUserMenuInput, SetUserMenuStartupInput } from "#/platform/menus"; + +import requestClient from "../request"; + +export function getAllApi(input: MenuGetByUserInput): Promise> { + return requestClient.get>(`/api/platform/menus/by-user/${input.userId}/${input.framework}`, { + params: input, + }); +} + +export function setMenusApi(input: SetUserMenuInput): Promise { + return requestClient.put("/api/platform/menus/by-user", input); +} + +export function setStartupMenuApi(meudId: string, input: SetUserMenuStartupInput): Promise { + return requestClient.put(`/api/platform/menus/startup/${meudId}/by-user`, input); +} diff --git a/apps/react-admin/src/api/request.ts b/apps/react-admin/src/api/request.ts index 2e3fb250f..e23adcb32 100644 --- a/apps/react-admin/src/api/request.ts +++ b/apps/react-admin/src/api/request.ts @@ -7,6 +7,7 @@ import { toast } from "sonner"; import { refreshToken } from "./account/token"; import { wrapperResult } from "@/utils/abp/request"; import { handleOAuthError } from "@/utils/abp/handleOAuthError"; +import useAbpStore from "@/store/abpCoreStore"; const requestClient = new RequestClient({ baseURL: import.meta.env.VITE_APP_BASE_API, @@ -64,12 +65,18 @@ function formatToken(token: null | string) { requestClient.addRequestInterceptor({ fulfilled: async (config) => { const { userToken } = useUserStore.getState(); + const { tenantId, xsrfToken } = useAbpStore.getState(); if (userToken.accessToken) { config.headers.Authorization = `${userToken.accessToken}`; } const { locale } = useLocaleStore.getState(); config.headers["Accept-Language"] = mapLocaleToAbpLanguageFormat(locale); - config.headers["X-Request-From"] = "slash-admin"; + // config.headers["X-Request-From"] = "slash-admin"; + config.headers["X-Request-From"] = "vben"; + + config.headers.__tenant = tenantId; + config.headers.RequestVerificationToken = xsrfToken; + return config; }, }); diff --git a/apps/react-admin/src/api/saas/editions.ts b/apps/react-admin/src/api/saas/editions.ts new file mode 100644 index 000000000..5dcb8e063 --- /dev/null +++ b/apps/react-admin/src/api/saas/editions.ts @@ -0,0 +1,53 @@ +import type { PagedResultDto } from "#/abp-core"; + +import type { EditionCreateDto, EditionDto, EditionUpdateDto, GetEditionPagedListInput } from "#/saas/editions"; + +import requestClient from "../request"; + +/** + * 创建版本 + * @param {EditionCreateDto} input 参数 + * @returns 创建的版本 + */ +export function createApi(input: EditionCreateDto): Promise { + return requestClient.post("/api/saas/editions", input); +} + +/** + * 编辑版本 + * @param {string} id 参数 + * @param {EditionUpdateDto} input 参数 + * @returns 编辑的版本 + */ +export function updateApi(id: string, input: EditionUpdateDto): Promise { + return requestClient.put(`/api/saas/editions/${id}`, input); +} + +/** + * 查询版本 + * @param {string} id Id + * @returns 查询的版本 + */ +export function getApi(id: string): Promise { + return requestClient.get(`/api/saas/editions/${id}`); +} + +/** + * 删除版本 + * @param {string} id Id + * @returns {void} + */ +export function deleteApi(id: string): Promise { + return requestClient.delete(`/api/saas/editions/${id}`); +} + +/** + * 查询版本分页列表 + * @param {GetEditionPagedListInput} input 参数 + * @returns {void} + */ +export function getPagedListApi(input?: GetEditionPagedListInput): Promise> { + return requestClient.get>("/api/saas/editions", { + params: input, + }); +} diff --git a/apps/react-admin/src/api/saas/multi-tenancy.ts b/apps/react-admin/src/api/saas/multi-tenancy.ts new file mode 100644 index 000000000..8bc531a64 --- /dev/null +++ b/apps/react-admin/src/api/saas/multi-tenancy.ts @@ -0,0 +1,10 @@ +import type { FindTenantResultDto } from "#/saas"; +import requestClient from "../request"; + +export function findTenantByNameApi(name: string): Promise { + return requestClient.get(`/api/abp/multi-tenancy/tenants/by-name/${name}`); +} + +export function findTenantByIdApi(id: string): Promise { + return requestClient.get(`/api/abp/multi-tenancy/tenants/by-id/${id}`); +} diff --git a/apps/react-admin/src/api/saas/tenants.ts b/apps/react-admin/src/api/saas/tenants.ts new file mode 100644 index 000000000..84541c3e3 --- /dev/null +++ b/apps/react-admin/src/api/saas/tenants.ts @@ -0,0 +1,111 @@ +import type { ListResultDto, PagedResultDto } from "#/abp-core"; + +import type { + GetTenantPagedListInput, + TenantConnectionStringCheckInput, + TenantConnectionStringDto, + TenantConnectionStringSetInput, + TenantCreateDto, + TenantDto, + TenantUpdateDto, +} from "#/saas/tenants"; + +import requestClient from "../request"; + +/** + * 创建租户 + * @param {TenantCreateDto} input 参数 + * @returns 创建的租户 + */ +export function createApi(input: TenantCreateDto): Promise { + return requestClient.post("/api/saas/tenants", input); +} + +/** + * 编辑租户 + * @param {string} id 参数 + * @param {TenantUpdateDto} input 参数 + * @returns 编辑的租户 + */ +export function updateApi(id: string, input: TenantUpdateDto): Promise { + return requestClient.put(`/api/saas/tenants/${id}`, input); +} + +/** + * 查询租户 + * @param {string} id Id + * @returns 查询的租户 + */ +export function getApi(id: string): Promise { + return requestClient.get(`/api/saas/tenants/${id}`); +} + +/** + * 删除租户 + * @param {string} id Id + * @returns {void} + */ +export function deleteApi(id: string): Promise { + return requestClient.delete(`/api/saas/tenants/${id}`); +} + +/** + * 查询租户分页列表 + * @param {GetTenantPagedListInput} input 参数 + * @returns {void} + */ +export function getPagedListApi(input?: GetTenantPagedListInput): Promise> { + return requestClient.get>("/api/saas/tenants", { + params: input, + }); +} + +/** + * 设置连接字符串 + * @param {string} id 租户Id + * @param {TenantConnectionStringSetInput} input 参数 + * @returns 连接字符串 + */ +export function setConnectionStringApi( + id: string, + input: TenantConnectionStringSetInput, +): Promise { + return requestClient.put(`/api/saas/tenants/${id}/connection-string`, input); +} + +/** + * 查询连接字符串 + * @param {string} id 租户Id + * @param {string} name 连接字符串名称 + * @returns 连接字符串 + */ +export function getConnectionStringApi(id: string, name: string): Promise { + return requestClient.get(`/api/saas/tenants/${id}/connection-string/${name}`); +} + +/** + * 查询所有连接字符串 + * @param {string} id 租户Id + * @returns 连接字符串列表 + */ +export function getConnectionStringsApi(id: string): Promise> { + return requestClient.get>(`/api/saas/tenants/${id}/connection-string`); +} + +/** + * 删除租户 + * @param {string} id 租户Id + * @param {string} name 连接字符串名称 + * @returns {void} + */ +export function deleteConnectionStringApi(id: string, name: string): Promise { + return requestClient.delete(`/api/saas/tenants/${id}/connection-string/${name}`); +} + +/** + * 检查数据库连接字符串 + * @param input 参数 + */ +export function checkConnectionString(input: TenantConnectionStringCheckInput): Promise { + return requestClient.post("/api/saas/tenants/connection-string/check", input); +} diff --git a/apps/react-admin/src/api/tasks/job-infos.ts b/apps/react-admin/src/api/tasks/job-infos.ts new file mode 100644 index 000000000..a34c0c1a2 --- /dev/null +++ b/apps/react-admin/src/api/tasks/job-infos.ts @@ -0,0 +1,86 @@ +import type { ListResultDto, PagedResultDto } from "#/abp-core"; + +import type { + BackgroundJobDefinitionDto, + BackgroundJobInfoBatchInput, + BackgroundJobInfoCreateDto, + BackgroundJobInfoDto, + BackgroundJobInfoGetListInput, + BackgroundJobInfoUpdateDto, +} from "#/tasks/job-infos"; + +import requestClient from "../request"; + +export function createApi(input: BackgroundJobInfoCreateDto): Promise { + return requestClient.post("/api/task-management/background-jobs", input); +} + +export function deleteApi(id: string): Promise { + return requestClient.delete(`/api/task-management/background-jobs/${id}`); +} + +export function bulkDeleteApi(input: BackgroundJobInfoBatchInput): Promise { + return requestClient.delete("/api/task-management/background-jobs/bulk-delete", { + data: input, + }); +} + +export function getApi(id: string): Promise { + return requestClient.get(`/api/task-management/background-jobs/${id}`); +} + +export function getPagedListApi(input?: BackgroundJobInfoGetListInput): Promise> { + return requestClient.get>("/api/task-management/background-jobs", { + params: input, + }); +} + +export function pauseApi(id: string): Promise { + return requestClient.put(`/api/task-management/background-jobs/${id}/pause`); +} + +export function bulkPauseApi(input: BackgroundJobInfoBatchInput): Promise { + return requestClient.put("/api/task-management/background-jobs/bulk-pause", input); +} + +export function resumeApi(id: string): Promise { + return requestClient.put(`/api/task-management/background-jobs/${id}/resume`); +} + +export function bulkResumeApi(input: BackgroundJobInfoBatchInput): Promise { + return requestClient.put("/api/task-management/background-jobs/bulk-resume", input); +} + +export function triggerApi(id: string): Promise { + return requestClient.put(`/api/task-management/background-jobs/${id}/trigger`); +} + +export function bulkTriggerApi(input: BackgroundJobInfoBatchInput): Promise { + return requestClient.put("/api/task-management/background-jobs/bulk-trigger", input); +} + +export function stopApi(id: string): Promise { + return requestClient.put(`/api/task-management/background-jobs/${id}/stop`); +} + +export function bulkStopApi(input: BackgroundJobInfoBatchInput): Promise { + return requestClient.put("/api/task-management/background-jobs/bulk-stop", input); +} + +export function startApi(id: string): Promise { + return requestClient.put(`/api/task-management/background-jobs/${id}/start`); +} + +export function bulkStartApi(input: BackgroundJobInfoBatchInput): Promise { + return requestClient.put("/api/task-management/background-jobs/bulk-start", input); +} + +export function updateApi(id: string, input: BackgroundJobInfoUpdateDto): Promise { + return requestClient.put(`/api/task-management/background-jobs/${id}`, input); +} + +export function getDefinitionsApi(): Promise> { + return requestClient.get>( + "/api/task-management/background-jobs/definitions", + ); +} diff --git a/apps/react-admin/src/api/tasks/job-logs.ts b/apps/react-admin/src/api/tasks/job-logs.ts new file mode 100644 index 000000000..e92cc6189 --- /dev/null +++ b/apps/react-admin/src/api/tasks/job-logs.ts @@ -0,0 +1,19 @@ +import type { PagedResultDto } from "#/abp-core"; + +import type { BackgroundJobLogDto, BackgroundJobLogGetListInput } from "#/tasks/job-logs"; + +import requestClient from "../request"; + +export function getApi(id: string): Promise { + return requestClient.get(`/api/task-management/background-jobs/logs/${id}`); +} + +export function deleteApi(id: string): Promise { + return requestClient.delete(`/api/task-management/background-jobs/logs/${id}`); +} + +export function getPagedListApi(input?: BackgroundJobLogGetListInput): Promise> { + return requestClient.get>("/api/task-management/background-jobs/logs", { + params: input, + }); +} diff --git a/apps/react-admin/src/api/text-templating/template-contents.ts b/apps/react-admin/src/api/text-templating/template-contents.ts new file mode 100644 index 000000000..dfbbe3192 --- /dev/null +++ b/apps/react-admin/src/api/text-templating/template-contents.ts @@ -0,0 +1,39 @@ +import type { + TextTemplateContentDto, + TextTemplateContentGetInput, + TextTemplateContentUpdateDto, + TextTemplateRestoreInput, +} from "#/text-templating/contents"; + +import requestClient from "../request"; + +/** + * 获取模板内容 + * @param input 参数 + * @returns 模板内容数据传输对象 + */ +export function getApi(input: TextTemplateContentGetInput): Promise { + let url = "/api/text-templating/templates/content"; + url += input.culture ? `/${input.culture}/${input.name}` : `/${input.name}`; + return requestClient.get(url); +} + +/** + * 重置模板内容为默认值 + * @param name 模板名称 + * @param input 参数 + * @returns 模板定义数据传输对象列表 + */ +export function restoreToDefaultApi(name: string, input: TextTemplateRestoreInput): Promise { + return requestClient.put(`/api/text-templating/templates/content/${name}/restore-to-default`, input); +} + +/** + * 更新模板内容 + * @param name 模板名称 + * @param input 参数 + * @returns 模板内容数据传输对象 + */ +export function updateApi(name: string, input: TextTemplateContentUpdateDto): Promise { + return requestClient.put(`/api/text-templating/templates/content/${name}`, input); +} diff --git a/apps/react-admin/src/api/text-templating/template-definitions.ts b/apps/react-admin/src/api/text-templating/template-definitions.ts new file mode 100644 index 000000000..ffc218251 --- /dev/null +++ b/apps/react-admin/src/api/text-templating/template-definitions.ts @@ -0,0 +1,58 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { + TextTemplateDefinitionCreateDto, + TextTemplateDefinitionDto, + TextTemplateDefinitionGetListInput, + TextTemplateDefinitionUpdateDto, +} from "#/text-templating/definitions"; + +import requestClient from "../request"; + +/** + * 新增模板定义 + * @param input 参数 + * @returns 模板定义数据传输对象 + */ +export function createApi(input: TextTemplateDefinitionCreateDto): Promise { + return requestClient.post("/api/text-templating/template/definitions", input); +} + +/** + * 删除模板定义 + * @param name 模板名称 + */ +export function deleteApi(name: string): Promise { + return requestClient.delete(`/api/text-templating/template/definitions/${name}`); +} + +/** + * 获取模板定义 + * @param name 模板名称 + * @returns 模板定义数据传输对象 + */ +export function getApi(name: string): Promise { + return requestClient.get(`/api/text-templating/template/definitions/${name}`); +} + +/** + * 获取模板定义列表 + * @param input 过滤参数 + * @returns 模板定义数据传输对象列表 + */ +export function getListApi( + input?: TextTemplateDefinitionGetListInput, +): Promise> { + return requestClient.get>("/api/text-templating/template/definitions", { + params: input, + }); +} + +/** + * 更新模板定义 + * @param name 模板名称 + * @returns 模板定义数据传输对象 + */ +export function updateApi(name: string, input: TextTemplateDefinitionUpdateDto): Promise { + return requestClient.put(`/api/text-templating/template/definitions/${name}`, input); +} diff --git a/apps/react-admin/src/api/webhooks/send-attempts.ts b/apps/react-admin/src/api/webhooks/send-attempts.ts new file mode 100644 index 000000000..4159601c8 --- /dev/null +++ b/apps/react-admin/src/api/webhooks/send-attempts.ts @@ -0,0 +1,64 @@ +import type { PagedResultDto } from "#/abp-core"; + +import type { + WebhookSendRecordDeleteManyInput, + WebhookSendRecordDto, + WebhookSendRecordGetListInput, + WebhookSendRecordResendManyInput, +} from "#/webhooks/send-attempts"; + +import requestClient from "../request"; + +/** + * 查询发送记录 + * @param id 记录Id + * @returns 发送记录Dto + */ +export function getApi(id: string): Promise { + return requestClient.get(`/api/webhooks/send-attempts/${id}`); +} + +/** + * 删除发送记录 + * @param id 记录Id + */ +export function deleteApi(id: string): Promise { + return requestClient.delete(`/api/webhooks/send-attempts/${id}`); +} + +/** + * 批量删除发送记录 + * @param input 参数 + */ +export function bulkDeleteApi(input: WebhookSendRecordDeleteManyInput): Promise { + return requestClient.delete("/api/webhooks/send-attempts/delete-many", { + data: input, + }); +} + +/** + * 查询发送记录分页列表 + * @param input 过滤参数 + * @returns 发送记录Dto分页列表 + */ +export function getPagedListApi(input: WebhookSendRecordGetListInput): Promise> { + return requestClient.get>("/api/webhooks/send-attempts", { + params: input, + }); +} + +/** + * 重新发送 + * @param id 记录Id + */ +export function reSendApi(id: string): Promise { + return requestClient.post(`/api/webhooks/send-attempts/${id}/resend`); +} + +/** + * 批量重新发送 + * @param input 参数 + */ +export function bulkReSendApi(input: WebhookSendRecordResendManyInput): Promise { + return requestClient.post("/api/webhooks/send-attempts/resend-many", input); +} diff --git a/apps/react-admin/src/api/webhooks/subscriptions.ts b/apps/react-admin/src/api/webhooks/subscriptions.ts new file mode 100644 index 000000000..6b4663349 --- /dev/null +++ b/apps/react-admin/src/api/webhooks/subscriptions.ts @@ -0,0 +1,79 @@ +import type { ListResultDto, PagedResultDto } from "#/abp-core"; + +import type { + WebhookAvailableGroupDto, + WebhookSubscriptionCreateDto, + WebhookSubscriptionDeleteManyInput, + WebhookSubscriptionDto, + WebhookSubscriptionGetListInput, + WebhookSubscriptionUpdateDto, +} from "#/webhooks/subscriptions"; + +import requestClient from "../request"; + +/** + * 创建订阅 + * @param input 参数 + * @returns 订阅Dto + */ +export function createApi(input: WebhookSubscriptionCreateDto): Promise { + return requestClient.post("/api/webhooks/subscriptions", input); +} + +/** + * 删除订阅 + * @param id 订阅Id + */ +export function deleteApi(id: string): Promise { + return requestClient.delete(`/api/webhooks/subscriptions/${id}`); +} + +/** + * 批量删除订阅 + * @param input 参数 + */ +export function bulkDeleteApi(input: WebhookSubscriptionDeleteManyInput): Promise { + return requestClient.delete("/api/webhooks/subscriptions/delete-many", { + data: input, + }); +} + +/** + * 查询所有可用的Webhook分组列表 + * @returns Webhook分组列表 + */ +export function getAllAvailableWebhooksApi(): Promise> { + return requestClient.get>("/api/webhooks/subscriptions/availables"); +} + +/** + * 查询订阅 + * @param id 订阅Id + * @returns 订阅Dto + */ +export function getApi(id: string): Promise { + return requestClient.get(`/api/webhooks/subscriptions/${id}`); +} + +/** + * 查询订阅分页列表 + * @param input 过滤参数 + * @returns 订阅Dto列表 + */ +export function getPagedListApi( + input: WebhookSubscriptionGetListInput, +): Promise> { + return requestClient.get>("/api/webhooks/subscriptions", { + params: input, + }); +} + +/** + * 更新订阅 + * @param id 订阅Id + * @param input 更新参数 + * @returns 订阅Dto + */ +export function updateApi(id: string, input: WebhookSubscriptionUpdateDto): Promise { + return requestClient.put(`/api/webhooks/subscriptions/${id}`, input); +} diff --git a/apps/react-admin/src/api/webhooks/webhook-definitions.ts b/apps/react-admin/src/api/webhooks/webhook-definitions.ts new file mode 100644 index 000000000..357961787 --- /dev/null +++ b/apps/react-admin/src/api/webhooks/webhook-definitions.ts @@ -0,0 +1,57 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { + WebhookDefinitionCreateDto, + WebhookDefinitionDto, + WebhookDefinitionGetListInput, + WebhookDefinitionUpdateDto, +} from "#/webhooks/definitions"; + +import requestClient from "../request"; + +/** + * 删除Webhook定义 + * @param name Webhook名称 + */ +export function deleteApi(name: string): Promise { + return requestClient.delete(`/api/webhooks/definitions/${name}`); +} + +/** + * 查询Webhook定义 + * @param name Webhook名称 + * @returns Webhook定义数据传输对象 + */ +export function getApi(name: string): Promise { + return requestClient.get(`/api/webhooks/definitions/${name}`); +} + +/** + * 查询Webhook定义列表 + * @param input Webhook过滤条件 + * @returns Webhook定义数据传输对象列表 + */ +export function getListApi(input?: WebhookDefinitionGetListInput): Promise> { + return requestClient.get>("/api/webhooks/definitions", { + params: input, + }); +} + +/** + * 创建Webhook定义 + * @param input Webhook定义参数 + * @returns Webhook定义数据传输对象 + */ +export function createApi(input: WebhookDefinitionCreateDto): Promise { + return requestClient.post("/api/webhooks/definitions", input); +} + +/** + * 更新Webhook定义 + * @param name Webhook名称 + * @param input Webhook定义参数 + * @returns Webhook定义数据传输对象 + */ +export function updateApi(name: string, input: WebhookDefinitionUpdateDto): Promise { + return requestClient.put(`/api/webhooks/definitions/${name}`, input); +} diff --git a/apps/react-admin/src/api/webhooks/webhook-group-definitions.ts b/apps/react-admin/src/api/webhooks/webhook-group-definitions.ts new file mode 100644 index 000000000..bc759f108 --- /dev/null +++ b/apps/react-admin/src/api/webhooks/webhook-group-definitions.ts @@ -0,0 +1,59 @@ +import type { ListResultDto } from "#/abp-core"; + +import type { + WebhookGroupDefinitionCreateDto, + WebhookGroupDefinitionDto, + WebhookGroupDefinitionGetListInput, + WebhookGroupDefinitionUpdateDto, +} from "#/webhooks/groups"; + +import requestClient from "../request"; + +/** + * 删除Webhook分组定义 + * @param name Webhook分组名称 + */ +export function deleteApi(name: string): Promise { + return requestClient.delete(`/api/webhooks/definitions/groups/${name}`); +} + +/** + * 查询Webhook分组定义 + * @param name Webhook分组名称 + * @returns Webhook分组定义数据传输对象 + */ +export function getApi(name: string): Promise { + return requestClient.get(`/api/webhooks/definitions/groups/${name}`); +} + +/** + * 查询Webhook分组定义列表 + * @param input Webhook分组过滤条件 + * @returns Webhook分组定义数据传输对象列表 + */ +export function getListApi( + input?: WebhookGroupDefinitionGetListInput, +): Promise> { + return requestClient.get>("/api/webhooks/definitions/groups", { + params: input, + }); +} + +/** + * 创建Webhook分组定义 + * @param input Webhook分组定义参数 + * @returns Webhook分组定义数据传输对象 + */ +export function createApi(input: WebhookGroupDefinitionCreateDto): Promise { + return requestClient.post("/api/webhooks/definitions/groups", input); +} + +/** + * 更新Webhook分组定义 + * @param name Webhook分组名称 + * @param input Webhook分组定义参数 + * @returns Webhook分组定义数据传输对象 + */ +export function updateApi(name: string, input: WebhookGroupDefinitionUpdateDto): Promise { + return requestClient.put(`/api/webhooks/definitions/groups/${name}`, input); +} diff --git a/apps/react-admin/src/components/abp/account/change-password-modal.tsx b/apps/react-admin/src/components/abp/account/change-password-modal.tsx new file mode 100644 index 000000000..978f63533 --- /dev/null +++ b/apps/react-admin/src/components/abp/account/change-password-modal.tsx @@ -0,0 +1,110 @@ +import type React from "react"; +import { useState } from "react"; +import { Modal, Form, Input } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { useMutation } from "@tanstack/react-query"; +import { changePasswordApi } from "@/api/account/profile"; +import { usePasswordValidator } from "@/hooks/abp/identity/usePasswordValidator"; + +interface Props { + visible: boolean; + onClose: () => void; +} + +const ChangePasswordModal: React.FC = ({ visible, onClose }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + const [submitting, setSubmitting] = useState(false); + const { validate } = usePasswordValidator(); + + const { mutateAsync: changePassword } = useMutation({ + mutationFn: changePasswordApi, + onSuccess: () => { + toast.success($t("AbpIdentity.PasswordChangedMessage")); + onClose(); + form.resetFields(); + }, + }); + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + setSubmitting(true); + await changePassword({ + currentPassword: values.currentPassword, + newPassword: values.newPassword, + }); + } catch (error) { + console.error(error); + } finally { + setSubmitting(false); + } + }; + + return ( + +
+ + + + + ({ + validator: async (_, value) => { + if (!value) return Promise.resolve(); + if (value === getFieldValue("currentPassword")) { + return Promise.reject(new Error($t("AbpAccount.NewPasswordSameAsOld"))); + } + try { + await validate(value); + return Promise.resolve(); + } catch (error: any) { + return Promise.reject(error); // Validation error from hook + } + }, + }), + ]} + > + + + + ({ + validator(_, value) { + if (!value || getFieldValue("newPassword") === value) { + return Promise.resolve(); + } + return Promise.reject(new Error($t("AbpIdentity.Volo_Abp_Identity:PasswordConfirmationFailed"))); + }, + }), + ]} + > + + +
+
+ ); +}; + +export default ChangePasswordModal; diff --git a/apps/react-admin/src/components/abp/account/change-phone-number-modal.tsx b/apps/react-admin/src/components/abp/account/change-phone-number-modal.tsx new file mode 100644 index 000000000..c8bccd5fd --- /dev/null +++ b/apps/react-admin/src/components/abp/account/change-phone-number-modal.tsx @@ -0,0 +1,112 @@ +import type React from "react"; +import { useState, useEffect } from "react"; +import { Modal, Form, Input, Button, Col, Row } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { useMutation } from "@tanstack/react-query"; +import { changePhoneNumberApi, sendChangePhoneNumberCodeApi } from "@/api/account/profile"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (phoneNumber: string) => void; +} + +const ChangePhoneNumberModal: React.FC = ({ visible, onClose, onChange }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + const [submitting, setSubmitting] = useState(false); + const [countdown, setCountdown] = useState(0); + + // Custom countdown logic using useEffect + useEffect(() => { + let timer: NodeJS.Timeout; + if (countdown > 0) { + timer = setInterval(() => { + setCountdown((prev) => (prev > 0 ? prev - 1 : 0)); + }, 1000); + } + return () => clearInterval(timer); + }, [countdown]); + + const { mutateAsync: sendCode } = useMutation({ + mutationFn: sendChangePhoneNumberCodeApi, + onSuccess: () => { + setCountdown(60); // Set countdown to 60 seconds + toast.success($t("AbpUi.SavedSuccessfully")); + }, + }); + + const { mutateAsync: changePhone } = useMutation({ + mutationFn: changePhoneNumberApi, + onSuccess: (_, variables) => { + toast.success($t("AbpAccount.PhoneNumberChangedMessage")); + onChange(variables.newPhoneNumber); + onClose(); + form.resetFields(); + }, + }); + + const handleSendCode = async () => { + try { + await form.validateFields(["newPhoneNumber"]); + const newPhone = form.getFieldValue("newPhoneNumber"); + await sendCode({ newPhoneNumber: newPhone }); + } catch (error) { + console.error(error); + } + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + setSubmitting(true); + await changePhone({ + code: values.code, + newPhoneNumber: values.newPhoneNumber, + }); + } catch (error) { + console.error(error); + } finally { + setSubmitting(false); + } + }; + + return ( + +
+ + + + + + + + + + + + + + + + +
+
+ ); +}; + +export default ChangePhoneNumberModal; diff --git a/apps/react-admin/src/components/abp/account/notice-settings.tsx b/apps/react-admin/src/components/abp/account/notice-settings.tsx index 18e775431..e16770519 100644 --- a/apps/react-admin/src/components/abp/account/notice-settings.tsx +++ b/apps/react-admin/src/components/abp/account/notice-settings.tsx @@ -1,14 +1,134 @@ -import { Card, Empty } from "antd"; +import type React from "react"; +import { useEffect, useState } from "react"; +import { Card, Collapse, List, Switch, Skeleton } from "antd"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { getMySubscribesApi, subscribeApi, unSubscribeApi } from "@/api/management/notifications/my-subscribes"; +import { getAssignableNotifiersApi } from "@/api/management/notifications/notifications"; -const BindSettings: React.FC = () => { +interface NotificationItem { + description?: string; + displayName: string; + isSubscribe: boolean; + loading: boolean; + name: string; +} + +interface NotificationGroup { + displayName: string; + name: string; + notifications: NotificationItem[]; +} + +const NotificationSettings: React.FC = () => { const { t: $t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [notificationGroups, setNotificationGroups] = useState([]); + + useEffect(() => { + initData(); + }, []); + + const initData = async () => { + try { + setLoading(true); + const [subRes, notifierRes] = await Promise.all([getMySubscribesApi(), getAssignableNotifiersApi()]); + + const groups: NotificationGroup[] = notifierRes.items.map((group) => { + const notifications: NotificationItem[] = group.notifications.map((notification) => ({ + description: notification.description, + displayName: notification.displayName, + // Check if the user is already subscribed + isSubscribe: subRes.items.some((x) => x.name === notification.name), + loading: false, + name: notification.name, + })); + + return { + displayName: group.displayName, + name: group.name, + notifications, + }; + }); + + setNotificationGroups(groups); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + const handleSubscribeChange = async (checked: boolean, groupIndex: number, itemIndex: number) => { + const targetGroup = notificationGroups[groupIndex]; + const targetItem = targetGroup.notifications[itemIndex]; + + // Optimistic Update / Loading State + const updateState = (isLoading: boolean, isSubscribed: boolean) => { + setNotificationGroups((prev) => { + const next = [...prev]; + const group = { ...next[groupIndex] }; + const items = [...group.notifications]; + items[itemIndex] = { ...items[itemIndex], loading: isLoading, isSubscribe: isSubscribed }; + group.notifications = items; + next[groupIndex] = group; + return next; + }); + }; + + updateState(true, checked); + + try { + if (checked) { + await subscribeApi(targetItem.name); + } else { + await unSubscribeApi(targetItem.name); + } + toast.success($t("AbpUi.SavedSuccessfully")); + updateState(false, checked); + } catch (error) { + console.error(error); + // Revert on error + updateState(false, !checked); + } + }; + + if (loading && notificationGroups.length === 0) { + return ( + + + + ); + } return ( - - + + g.name)}> + {notificationGroups.map((group, groupIndex) => ( + + ( + handleSubscribeChange(checked, groupIndex, itemIndex)} + />, + ]} + > + + + )} + /> + + ))} + ); }; -export default BindSettings; +export default NotificationSettings; diff --git a/apps/react-admin/src/components/abp/account/security-settings.tsx b/apps/react-admin/src/components/abp/account/security-settings.tsx index 87076eb39..a730f07ed 100644 --- a/apps/react-admin/src/components/abp/account/security-settings.tsx +++ b/apps/react-admin/src/components/abp/account/security-settings.tsx @@ -65,7 +65,7 @@ const SecuritySettings: React.FC = ({ userInfo, onChangePassword, onChang if (sendMailInterval > 0) { return `${sendMailInterval} s`; } - return $t("AbpAccount.ClickToValidation"); + return $t("AbpAccountSecurity.ClickToValidation"); }; return ( diff --git a/apps/react-admin/src/components/abp/account/session-settings.tsx b/apps/react-admin/src/components/abp/account/session-settings.tsx index 54956ffaf..0f2cf0959 100644 --- a/apps/react-admin/src/components/abp/account/session-settings.tsx +++ b/apps/react-admin/src/components/abp/account/session-settings.tsx @@ -24,7 +24,7 @@ const SessionSettings: React.FC = () => { const { mutateAsync: revokeSession } = useMutation({ mutationFn: revokeSessionApi, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["mySessions"] }); + queryClient.refetchQueries({ queryKey: ["mySessions"] }); toast.success($t("AbpIdentity.SuccessfullyRevoked")); }, }); diff --git a/apps/react-admin/src/components/abp/adapter/api-select.tsx b/apps/react-admin/src/components/abp/adapter/api-select.tsx new file mode 100644 index 000000000..edd7ffb69 --- /dev/null +++ b/apps/react-admin/src/components/abp/adapter/api-select.tsx @@ -0,0 +1,87 @@ +import type React from "react"; +import { useEffect, useState, useMemo } from "react"; +import { Select, type SelectProps, Spin } from "antd"; + +function getNestedValue(obj: any, path: string) { + if (!path) return undefined; + return path.split(".").reduce((acc, part) => acc?.[part], obj); +} + +export interface ApiSelectProps extends Omit { + /** Function to fetch data, returning a Promise */ + api?: (params?: any) => Promise; + /** Parameters to pass to the api function */ + params?: any; + /** Key in the response object containing the array (e.g., 'items') */ + resultField?: string; + /** Property name to use for the label */ + labelField?: string; + /** Property name to use for the value */ + valueField?: string; + /** Trigger fetch immediately on mount */ + immediate?: boolean; +} + +const ApiSelect: React.FC = ({ + api, + params, + resultField = "items", + labelField = "label", + valueField = "value", + immediate = true, + ...props +}) => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (immediate && api) { + fetchData(); + } + }, [JSON.stringify(params)]); + + const fetchData = async () => { + if (!api) return; + setLoading(true); + try { + const res = await api(params); + // Use helper instead of lodash.get + const list = resultField ? getNestedValue(res, resultField) : res; + + if (Array.isArray(list)) { + setData(list); + } else if (Array.isArray(res)) { + // Fallback: if extracting resultField failed but response itself is an array + setData(res); + } + } catch (error) { + console.error("ApiSelect fetch error:", error); + } finally { + setLoading(false); + } + }; + + const options = useMemo(() => { + return data.map((item) => ({ + ...item, + label: item[labelField], + value: item[valueField], + })); + }, [data, labelField, valueField]); + + return ( + + ); +}; + +export default IconPicker; diff --git a/apps/react-admin/src/components/abp/claims/claim-table.tsx b/apps/react-admin/src/components/abp/claims/claim-table.tsx index 362374b01..0bff0d7a3 100644 --- a/apps/react-admin/src/components/abp/claims/claim-table.tsx +++ b/apps/react-admin/src/components/abp/claims/claim-table.tsx @@ -1,6 +1,6 @@ import type React from "react"; import { useState, useRef } from "react"; -import { Button, Popconfirm, Space } from "antd"; +import { Button, Card, Popconfirm, Space } from "antd"; import { EditOutlined, DeleteOutlined } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; import { ProTable, type ActionType, type ProColumns } from "@ant-design/pro-table"; @@ -99,22 +99,24 @@ const ClaimTable: React.FC = ({ return (
- - actionRef={actionRef} - columns={columns} - rowKey="id" - search={false} - dataSource={claimsData?.items} - pagination={false} - toolBarRender={() => [ - withAccessChecker( - , - [createPolicy], - ), - ]} - /> + + + actionRef={actionRef} + columns={columns} + rowKey="id" + search={false} + dataSource={claimsData?.items} + pagination={false} + toolBarRender={() => [ + withAccessChecker( + , + [createPolicy], + ), + ]} + /> + = ({ data, onChange, onDelete return ( <> - - columns={columns} - dataSource={dataSource} - search={false} - pagination={false} - toolBarRender={() => [ - , - ]} - /> - + + + columns={columns} + dataSource={dataSource} + search={false} + pagination={false} + toolBarRender={() => [ + , + ]} + /> + setModalVisible(false)} diff --git a/apps/react-admin/src/components/abp/features/feature-modal.tsx b/apps/react-admin/src/components/abp/features/feature-modal.tsx new file mode 100644 index 000000000..10fad4004 --- /dev/null +++ b/apps/react-admin/src/components/abp/features/feature-modal.tsx @@ -0,0 +1,333 @@ +import type { Validator } from "#/abp-core"; +import type React from "react"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { getApi, updateApi } from "@/api/management/features/features"; +import { useMutation } from "@tanstack/react-query"; +import { toast } from "sonner"; +import type { FeatureGroupDto, UpdateFeaturesDto } from "#/management/features/features"; + +import { Modal, Form, Tabs, Card, Input, InputNumber, Checkbox, Select, Spin } from "antd"; +import { useValidation } from "@/hooks/abp/use-validation"; +import { useLocalizer } from "@/hooks/abp/use-localization"; + +interface FeatureManagementModalProps { + visible: boolean; + onClose: () => void; + providerName: string; + providerKey?: string; + displayName?: string; // For the modal title +} + +// Helper Interface for Form Data Structure +export interface FeatureFormData { + groups: FeatureGroupDto[]; +} + +const FeatureManagementModal: React.FC = ({ + visible, + onClose, + providerName, + providerKey, + displayName, +}) => { + const { t: $t } = useTranslation(); + const { Lr } = useLocalizer(); + const [form] = Form.useForm(); + + // Validation Hook + const { fieldMustBeetWeen, fieldMustBeStringWithMinimumLengthAndMaximumLength, fieldRequired } = useValidation(); + + // State + const [activeTabKey, setActiveTabKey] = useState(""); + const [groups, setGroups] = useState([]); + + // --- Helpers --- + + /** + * Generates AntD Form Rules based on ABP Validator metadata (custom hook) + */ + const createRules = (fieldLabel: string, validator: Validator) => { + const rules: any[] = []; + if (validator?.properties) { + switch (validator.name) { + case "NUMERIC": { + rules.push( + ...fieldMustBeetWeen({ + name: fieldLabel, + start: Number(validator.properties.MinValue), + end: Number(validator.properties.MaxValue), + trigger: "change", + }), + ); + break; + } + case "STRING": { + if (validator.properties.AllowNull && String(validator.properties.AllowNull).toLowerCase() === "true") { + rules.push( + ...fieldRequired({ + name: fieldLabel, + trigger: "blur", + }), + ); + } + rules.push( + ...fieldMustBeStringWithMinimumLengthAndMaximumLength({ + name: fieldLabel, + minimum: Number(validator.properties.MinLength), + maximum: Number(validator.properties.MaxLength), + trigger: "blur", + }), + ); + break; + } + default: + break; + } + } + return rules; + }; + + /** + * Process raw API data for the UI + * 1. Localize Selection items + * 2. Convert string values to boolean/number for inputs + */ + const mapFeaturesForUi = (rawGroups: FeatureGroupDto[]) => { + // Deep clone to avoid mutating read-only props if using strict mode + const processedGroups = JSON.parse(JSON.stringify(rawGroups)) as FeatureGroupDto[]; + + processedGroups.forEach((group) => { + group.features.forEach((feature) => { + // Handle Selection Localization + if (feature.valueType?.name === "SelectionStringValueType") { + const valueType: any = feature.valueType; + if (valueType.itemSource?.items) { + valueType.itemSource.items.forEach((item: any) => { + if (item.displayText?.resourceName === "Fixed") { + item.displayName = item.displayText.name; + } else { + item.displayName = Lr(item.displayText.resourceName, item.displayText.name); + } + }); + } + } + + // Handle Value Conversion + else if (feature.valueType?.validator) { + switch (feature.valueType.validator.name) { + case "BOOLEAN": + feature.value = String(feature.value).toLowerCase() === "true"; + break; + case "NUMERIC": + feature.value = Number(feature.value); + break; + default: + // Keep as string + break; + } + } + }); + }); + + return processedGroups; + }; + + /** + * Convert Form Data back to DTO for API submission + */ + const getFeatureInput = (formValues: any): UpdateFeaturesDto => { + const input: UpdateFeaturesDto = { features: [] }; + + const formGroups = formValues.groups as FeatureGroupDto[]; + if (!formGroups) return input; + + formGroups.forEach((g) => { + if (!g?.features) return; + g.features.forEach((f) => { + // Only include non-null values + if (f.value !== null && f.value !== undefined) { + input.features.push({ + name: f.name, + value: String(f.value), // Convert back to string + }); + } + }); + }); + + return input; + }; + + // --- API Hooks --- + + const { mutateAsync: fetchData, isPending: isLoading } = useMutation({ + mutationFn: async () => { + if (!providerName) return; + const res = await getApi({ providerName, providerKey }); + return res.groups; + }, + onSuccess: (rawGroups) => { + if (rawGroups) { + const processed = mapFeaturesForUi(rawGroups); + setGroups(processed); + form.setFieldsValue({ groups: processed }); + + // Set active tab to first group + if (processed.length > 0 && !activeTabKey) { + setActiveTabKey(processed[0].name); + } + } + }, + }); + + const { mutateAsync: updateData, isPending: isSaving } = useMutation({ + mutationFn: async (input: UpdateFeaturesDto) => { + return updateApi({ providerName, providerKey }, input); + }, + onSuccess: () => { + toast.success($t("AbpUi.SavedSuccessfully")); + onClose(); + }, + }); + + // --- Effects --- + + useEffect(() => { + if (visible) { + form.resetFields(); + setGroups([]); + setActiveTabKey(""); + fetchData(); + } + }, [visible, providerName, providerKey]); + + // --- Handlers --- + + const handleOk = async () => { + try { + const values = await form.validateFields(); + const input = getFeatureInput(values); + await updateData(input); + } catch (e) { + console.error("Validation Failed", e); + } + }; + + // --- Renderers --- + + const renderFeatureInput = (feature: any, groupIndex: number, featureIndex: number) => { + if (!feature.valueType) return null; + + const fieldName = ["groups", groupIndex, "features", featureIndex, "value"]; + const valueTypeName = feature.valueType.name; + const validatorName = feature.valueType.validator?.name; + + // 1. Checkbox (Boolean) + if (valueTypeName === "ToggleStringValueType" && validatorName === "BOOLEAN") { + return ( + + {feature.displayName} + + ); + } + + // 2. Select (Selection) + if (valueTypeName === "SelectionStringValueType") { + return ( + + } + + ); + } + + // Fallback + return ( + + + + ); + }; + + const modalTitle = displayName + ? `${$t("AbpFeatureManagement.Features")} - ${displayName}` + : $t("AbpFeatureManagement.Features"); + + return ( + + +
+ {/* We render hidden inputs for names to ensure they exist in the form values structure for getFeatureInput logic + */} + {groups.map((g, gIdx) => ( +
+ {g.features.map((f, fIdx) => ( + + + + ))} +
+ ))} + + ({ + key: group.name, + label: group.displayName, + children: ( +
+ + {group.features.map((feature, featureIndex) => ( +
{renderFeatureInput(feature, groupIndex, featureIndex)}
+ ))} +
+
+ ), + }))} + /> + +
+
+ ); +}; + +export default FeatureManagementModal; diff --git a/apps/react-admin/src/components/abp/features/state-check/feature-state-check.tsx b/apps/react-admin/src/components/abp/features/state-check/feature-state-check.tsx new file mode 100644 index 000000000..e85365bb8 --- /dev/null +++ b/apps/react-admin/src/components/abp/features/state-check/feature-state-check.tsx @@ -0,0 +1,149 @@ +import type React from "react"; +import { useMemo } from "react"; +import { Checkbox, TreeSelect, Spin } from "antd"; +import { useTranslation } from "react-i18next"; +import { useQuery } from "@tanstack/react-query"; +import { getListApi as getFeaturesApi } from "@/api/management/features/feature-definitions"; +import { getListApi as getGroupsApi } from "@/api/management/features/feature-group-definitions"; + +import { localizationSerializer } from "@/utils/abp/localization-serializer"; +import { useLocalizer } from "@/hooks/abp/use-localization"; + +import { listToTree } from "@/utils/tree"; +import { valueTypeSerializer } from "../../string-value-type"; + +interface ValueType { + featureNames: string[]; + requiresAll: boolean; +} + +interface FeatureStateCheckProps { + value?: ValueType; + onChange?: (value: ValueType) => void; +} + +const FeatureStateCheck: React.FC = ({ + value = { featureNames: [], requiresAll: false }, + onChange, +}) => { + const { t: $t } = useTranslation(); + const { deserialize } = localizationSerializer(); + const { Lr } = useLocalizer(); + + // 1. Fetch Data + const { data: featureData, isLoading } = useQuery({ + queryKey: ["featureStateCheckData"], + queryFn: async () => { + const [groupsRes, featuresRes] = await Promise.all([getGroupsApi(), getFeaturesApi()]); + + // Filter: Only BOOLEAN features are relevant for state checking + const validFeatures = featuresRes.items.filter((item) => { + if (item.valueType) { + try { + const vt = valueTypeSerializer.deserialize(item.valueType); + return vt.validator.name === "BOOLEAN"; + } catch { + return false; + } + } + return true; + }); + + // Prepare localized features list for lookup and tree building + const features = validFeatures.map((f) => { + const d = deserialize(f.displayName); + return { + ...f, + title: Lr(d.resourceName, d.name), // Localized Title + key: f.name, + value: f.name, + }; + }); + + // Prepare localized groups + const groups = groupsRes.items.map((g) => { + const d = deserialize(g.displayName); + return { + ...g, + title: Lr(d.resourceName, d.name), + }; + }); + + return { groups, features }; + }, + staleTime: Number.POSITIVE_INFINITY, // Config data rarely changes + }); + + // 2. Construct Tree Data + const treeData = useMemo(() => { + if (!featureData) return []; + const { groups, features } = featureData; + + return groups.map((group) => { + // Find features belonging to this group + const groupFeatures = features.filter((f) => f.groupName === group.name); + + // Build hierarchy for features (parent/child) + const children = listToTree(groupFeatures, { id: "name", pid: "parentName" }); + + // Return Group Node + return { + title: group.title, + value: group.name, + key: group.name, + selectable: false, // Cannot select the group itself + checkable: false, // Cannot check the group itself + disableCheckbox: true, // Visual disabled checkbox + children: children, + }; + }); + }, [featureData]); + + // 3. Map selected string[] to { label, value }[] for TreeSelect (Required for treeCheckStrictly) + const treeValue = useMemo(() => { + if (!featureData || !value.featureNames) return []; + return value.featureNames.map((name) => { + const feature = featureData.features.find((f) => f.name === name); + return { + label: feature?.title || name, + value: name, + }; + }); + }, [featureData, value.featureNames]); + + // 4. Handle Change + const triggerChange = (changedValue: Partial) => { + onChange?.({ ...value, ...changedValue }); + }; + + const onTreeChange = (labeledValues: { label: React.ReactNode; value: string }[]) => { + // Extract just the feature names (strings) to send back + const names = labeledValues.map((item) => item.value); + triggerChange({ featureNames: names }); + }; + + if (isLoading) return ; + + return ( +
+ triggerChange({ requiresAll: e.target.checked })}> + {$t("component.simple_state_checking.requireFeatures.requiresAll")} + + + +
+ ); +}; + +export default FeatureStateCheck; diff --git a/apps/react-admin/src/components/abp/features/state-check/global-feature-state-check.tsx b/apps/react-admin/src/components/abp/features/state-check/global-feature-state-check.tsx new file mode 100644 index 000000000..79b3b4538 --- /dev/null +++ b/apps/react-admin/src/components/abp/features/state-check/global-feature-state-check.tsx @@ -0,0 +1,53 @@ +import type React from "react"; +import { useMemo } from "react"; +import { Checkbox, Select } from "antd"; +import { useTranslation } from "react-i18next"; +import useAbpStore from "@/store/abpCoreStore"; + +interface ValueType { + globalFeatureNames: string[]; + requiresAll: boolean; +} + +interface GlobalFeatureStateCheckProps { + value?: ValueType; + onChange?: (value: ValueType) => void; +} + +const GlobalFeatureStateCheck: React.FC = ({ + value = { globalFeatureNames: [], requiresAll: false }, + onChange, +}) => { + const { t: $t } = useTranslation(); + const application = useAbpStore((state) => state.application); + + const options = useMemo(() => { + if (!application?.globalFeatures?.enabledFeatures) return []; + return application.globalFeatures.enabledFeatures.map((f) => ({ + label: f, + value: f, + })); + }, [application]); + + const triggerChange = (changedValue: Partial) => { + onChange?.({ ...value, ...changedValue }); + }; + + return ( +
+ triggerChange({ requiresAll: e.target.checked })}> + {$t("component.simple_state_checking.requireFeatures.requiresAll")} + + + + + prev.name !== curr.name}> + {({ getFieldValue }) => { + const type = getFieldValue("name"); + + if (type === "A") { + // Authenticated check requires no extra config + return null; + } + + if (type === "F") { + return ( + + val?.featureNames?.length > 0 + ? Promise.resolve() + : Promise.reject($t("component.simple_state_checking.requireFeatures.featureNames")), + }, + ]} + > + + + ); + } + + if (type === "G") { + return ( + + val?.globalFeatureNames?.length > 0 + ? Promise.resolve() + : Promise.reject($t("component.simple_state_checking.requireFeatures.featureNames")), + }, + ]} + > + + + ); + } + + if (type === "P") { + return ( + + val?.permissions?.length > 0 + ? Promise.resolve() + : Promise.reject($t("component.simple_state_checking.requirePermissions.permissions")), + }, + ]} + > + + + ); + } + + return null; + }} + + + + ); +}; + +export default SimpleStateCheckingModal; diff --git a/apps/react-admin/src/components/abp/simple-state-checking/simple-state-checking.tsx b/apps/react-admin/src/components/abp/simple-state-checking/simple-state-checking.tsx new file mode 100644 index 000000000..65097b5fc --- /dev/null +++ b/apps/react-admin/src/components/abp/simple-state-checking/simple-state-checking.tsx @@ -0,0 +1,214 @@ +import type React from "react"; +import { useState, useMemo, useEffect } from "react"; +import { Button, Card, Table, Tag, Space, Row, Col, Popconfirm } from "antd"; +import { EditOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { useSimpleStateCheck } from "@/hooks/abp/fake-hooks/use-simple-state-check"; // TODO re hooks in this chain +import type { SimplaCheckStateBase } from "./interface"; +import SimpleStateCheckingModal from "./simple-state-checking-modal"; +import type { ColumnsType } from "antd/es/table"; + +interface SimpleStateCheckingProps { + state: SimplaCheckStateBase; + value?: string; // Serialized string + onChange?: (value: string | undefined) => void; + disabled?: boolean; + allowEdit?: boolean; + allowDelete?: boolean; +} + +const SimpleStateChecking: React.FC = ({ + state, + value, + onChange, + disabled = false, + allowEdit = false, + allowDelete = false, +}) => { + const { t: $t } = useTranslation(); + const { deserializeArray, serializeArray, deserialize } = useSimpleStateCheck(); + + // Local state to manage the list of checkers + const [checkers, setCheckers] = useState([]); + const [modalVisible, setModalVisible] = useState(false); + const [editingRecord, setEditingRecord] = useState(null); + + // Sync prop value with local state + useEffect(() => { + if (!value || value.length === 0) { + setCheckers([]); + } else { + const deserialized = deserializeArray(value, state); + setCheckers(deserialized); + } + }, [value, state]); + + // Map for friendly names + const simpleCheckerMap: Record = { + A: $t("component.simple_state_checking.requireAuthenticated.title"), + F: $t("component.simple_state_checking.requireFeatures.title"), + G: $t("component.simple_state_checking.requireGlobalFeatures.title"), + P: $t("component.simple_state_checking.requirePermissions.title"), + }; + + const handleAddNew = () => { + setEditingRecord(null); + setModalVisible(true); + }; + + const handleEdit = (record: any) => { + setEditingRecord(record); + setModalVisible(true); + }; + + const handleUpdateCheckers = (newCheckers: any[]) => { + setCheckers(newCheckers); + const serialized = serializeArray(newCheckers); + onChange?.(serialized); //TODO + }; + + const handleModalConfirm = (data: any) => { + // 'data' comes in format { T: 'type', A: bool, N: [] } from the modal + + // Deserialize the simple object from modal back into the complex class instance used by the list + const deserializedItem = deserialize(data, state); + if (!deserializedItem) return; + + const updatedList = [...checkers]; + const existingIndex = updatedList.findIndex((x) => x.name === data.T); + + if (existingIndex > -1) { + updatedList[existingIndex] = deserializedItem; + } else { + updatedList.push(deserializedItem); + } + + handleUpdateCheckers(updatedList); + }; + + const handleDelete = (record: any) => { + const updatedList = checkers.filter((x) => x.name !== record.name); + handleUpdateCheckers(updatedList); + }; + + const handleClean = () => { + handleUpdateCheckers([]); + }; + + const options = useMemo(() => { + return Object.keys(simpleCheckerMap).map((key) => ({ + label: simpleCheckerMap[key], + value: key, + // Disable if this type already exists in the list (assuming 1 per type based on Vue logic findIndex) + disabled: checkers.some((x) => x.name === key), + })); + }, [checkers, $t]); + + const columns: ColumnsType = [ + { + title: $t("component.simple_state_checking.table.name"), + dataIndex: "name", + key: "name", + width: 150, + render: (name) => simpleCheckerMap[name] || name, + }, + { + title: $t("component.simple_state_checking.table.properties"), + key: "properties", + render: (_, record) => { + if (record.name === "F") { + return ( + + {record.featureNames?.map((f: string) => ( + {f} + ))} + + ); + } + if (record.name === "G") { + return ( + + {record.globalFeatureNames?.map((f: string) => ( + {f} + ))} + + ); + } + if (record.name === "P") { + const permissions = record.model?.permissions || record.permissions || []; + return ( + + {permissions.map((p: string) => ( + {p} + ))} + + ); + } + if (record.name === "A") { + return $t("component.simple_state_checking.requireAuthenticated.title"); + } + return null; + }, + }, + ]; + + if (!disabled) { + columns.push({ + title: $t("component.simple_state_checking.table.actions"), + key: "action", + width: 180, + render: (_, record) => ( + + {allowEdit && ( + + )} + {allowDelete && ( + handleDelete(record)}> + + + )} + + ), + }); + } + + return ( +
+ + {$t("component.simple_state_checking.title")} + + {!disabled && ( + + + + + )} + + + } + > + + + + setModalVisible(false)} + onConfirm={handleModalConfirm} + record={editingRecord} + options={options} + /> + + ); +}; + +export default SimpleStateChecking; diff --git a/apps/react-admin/src/components/abp/string-value-type/index.ts b/apps/react-admin/src/components/abp/string-value-type/index.ts new file mode 100644 index 000000000..2e5659125 --- /dev/null +++ b/apps/react-admin/src/components/abp/string-value-type/index.ts @@ -0,0 +1,4 @@ +export * from "./interface"; +export { default as StringValueTypeInput } from "./string-value-type-input"; +export * from "./validator"; +export * from "./value-type.ts"; diff --git a/apps/react-admin/src/components/abp/string-value-type/interface.ts b/apps/react-admin/src/components/abp/string-value-type/interface.ts new file mode 100644 index 000000000..5288ff2e4 --- /dev/null +++ b/apps/react-admin/src/components/abp/string-value-type/interface.ts @@ -0,0 +1,3 @@ +export interface StringValueTypeInstance { + validate(value: any): Promise; +} diff --git a/apps/react-admin/src/components/abp/string-value-type/string-value-type-input.tsx b/apps/react-admin/src/components/abp/string-value-type/string-value-type-input.tsx new file mode 100644 index 000000000..1a039b353 --- /dev/null +++ b/apps/react-admin/src/components/abp/string-value-type/string-value-type-input.tsx @@ -0,0 +1,540 @@ +import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; +import { Button, Card, Checkbox, Col, Form, Input, InputNumber, Modal, Row, Select, Table, Space } from "antd"; +import { DeleteOutlined, EditOutlined, PlusOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { useLocalizer } from "@/hooks/abp/use-localization"; +import { + valueTypeSerializer, + FreeTextStringValueType, + SelectionStringValueType, + ToggleStringValueType, + type StringValueType, + type SelectionStringValueItem, +} from "./value-type"; +import { + AlwaysValidValueValidator, + BooleanValueValidator, + NumericValueValidator, + StringValueValidator, +} from "./validator"; +import LocalizableInput from "@/components/abp/localizable-input/localizable-input"; +import { localizationSerializer } from "@/utils/abp/localization-serializer"; +import type { LocalizableStringInfo } from "#/abp-core"; + +export interface ValueTypeInputProps { + allowDelete?: boolean; + allowEdit?: boolean; + disabled?: boolean; + value?: string; + onChange?: (value: string | undefined) => void; + onValueTypeChange?: (type: string) => void; + onValidatorChange?: (validator: string) => void; + onSelectionChange?: (items: SelectionStringValueItem[]) => void; +} + +export interface ValueTypeInputHandle { + validate: (value: any) => Promise; +} + +const ValueTypeInput = forwardRef( + ( + { + allowDelete = false, + allowEdit = false, + disabled = false, + value = "{}", + onChange, + onValueTypeChange, + onValidatorChange, + onSelectionChange, + }, + ref, + ) => { + const { t: $t } = useTranslation(); + const { Lr } = useLocalizer(); + + const { + deserialize: deserializeLocalizer, + serialize: serializeLocalizer, + validate: validateLocalizer, + } = localizationSerializer(); + + // Internal State + const [valueType, setValueType] = useState(new FreeTextStringValueType()); + + // Modal State for Selection Type + const [modalVisible, setModalVisible] = useState(false); + const [modalForm] = Form.useForm(); + const [editingItem, setEditingItem] = useState<{ + isEdit: boolean; + displayText?: string; + }>({ isEdit: false }); + + // Initialize/Sync State from Props + useEffect(() => { + if (!value || value.trim() === "" || value === "{}") { + setValueType(new FreeTextStringValueType()); + } else { + try { + const deserialized = valueTypeSerializer.deserialize(value); + setValueType(deserialized); + } catch (e) { + console.warn("Failed to deserialize valueType", e); + } + } + }, [value]); + + // Notify Parent of Changes + const triggerChange = (newValueType: StringValueType) => { + // We need to create a new object reference or clone to ensure React detects state changes + // Serialization/Deserialization is a safe way to deep clone and ensure logic consistency + const serialized = valueTypeSerializer.serialize(newValueType); + + // Update internal state only if not controlled (optimization), + // but here we rely on parent prop update usually. + // However, to make UI responsive immediately for nested properties: + setValueType(valueTypeSerializer.deserialize(serialized)); + + if (onChange) onChange(serialized); + if (onValueTypeChange) onValueTypeChange(newValueType.name); + if (onValidatorChange) onValidatorChange(newValueType.validator.name); + + if (newValueType instanceof SelectionStringValueType && onSelectionChange) { + onSelectionChange(newValueType.itemSource.items); + } + }; + + // Expose Validate Method + useImperativeHandle(ref, () => ({ + validate: async (val: any) => { + if (valueType instanceof SelectionStringValueType) { + const items = valueType.itemSource.items; + if (items.length === 0) { + return Promise.reject($t("component.value_type_nput.type.SELECTION.itemsNotBeEmpty")); + } + if (val && !items.some((item) => item.value === val)) { + return Promise.reject($t("component.value_type_nput.type.SELECTION.itemsNotFound")); + } + } + + if (!valueType.validator.isValid(val)) { + const validatorNameKey = `component.value_type_nput.validator.${valueType.validator.name}.name`; + return Promise.reject( + $t("component.value_type_nput.validator.isInvalidValue", { + 0: $t(validatorNameKey), + }), + ); + } + return Promise.resolve(val); + }, + })); + + // Handlers + const handleValueTypeChange = (type: string) => { + let newValueType: StringValueType; + switch (type) { + case "SELECTION": + case "SelectionStringValueType": + newValueType = new SelectionStringValueType(); + break; + case "TOGGLE": + case "ToggleStringValueType": + newValueType = new ToggleStringValueType(); + break; + default: + newValueType = new FreeTextStringValueType(); + break; + } + triggerChange(newValueType); + }; + + const handleValidatorChange = (validatorName: string) => { + const newValueType = valueTypeSerializer.deserialize(valueTypeSerializer.serialize(valueType)); + + switch (validatorName) { + case "BOOLEAN": + newValueType.validator = new BooleanValueValidator(); + break; + case "NULL": + newValueType.validator = new AlwaysValidValueValidator(); + break; + case "NUMERIC": + newValueType.validator = new NumericValueValidator(); + break; + default: + newValueType.validator = new StringValueValidator(); + break; + } + triggerChange(newValueType); + }; + + // Generic Property Update Handler (Deep Update) + const updateValidatorProperty = (updater: (validator: any) => void) => { + const serialized = valueTypeSerializer.serialize(valueType); + const cloned = valueTypeSerializer.deserialize(serialized); + updater(cloned.validator); + triggerChange(cloned); + }; + + // Selection Logic + const handleAddSelection = () => { + setEditingItem({ isEdit: false }); + modalForm.resetFields(); + setModalVisible(true); + }; + + const handleClearSelection = () => { + if (valueType instanceof SelectionStringValueType) { + const cloned = valueTypeSerializer.deserialize( + valueTypeSerializer.serialize(valueType), + ) as SelectionStringValueType; + cloned.itemSource.items = []; + triggerChange(cloned); + } + }; + + const handleEditSelection = (record: SelectionStringValueItem) => { + setEditingItem({ isEdit: true, displayText: serializeLocalizer(record.displayText) }); + modalForm.setFieldsValue({ + displayText: serializeLocalizer(record.displayText), + value: record.value, + }); + setModalVisible(true); + }; + + const handleDeleteSelection = (record: SelectionStringValueItem) => { + if (valueType instanceof SelectionStringValueType) { + const displayText = serializeLocalizer(record.displayText); + const cloned = valueType as SelectionStringValueType; + cloned.itemSource.items = cloned.itemSource.items.filter( + (x) => serializeLocalizer(x.displayText) !== displayText, + ); + + triggerChange(cloned); + } + }; + + const handleModalOk = async () => { + try { + const values = await modalForm.validateFields(); + if (valueType instanceof SelectionStringValueType) { + const cloned = valueTypeSerializer.deserialize( + valueTypeSerializer.serialize(valueType), + ) as SelectionStringValueType; + + if (editingItem.isEdit) { + const index = cloned.itemSource.items.findIndex( + (x) => serializeLocalizer(x.displayText) === editingItem.displayText, + ); + if (index > -1) { + cloned.itemSource.items[index] = { + displayText: values.displayText, + value: values.value, + }; + } + } else { + cloned.itemSource.items.push({ + displayText: deserializeLocalizer(values.displayText), + value: values.value, + }); + } + triggerChange(cloned); + setModalVisible(false); + modalForm.resetFields(); + } + } catch (e) { + // Validation failed + console.warn("Validation failed in handleModalOk", e); + } + }; + + // Columns for Selection Table + const selectionColumns = useMemo(() => { + const cols: any[] = [ + { + title: $t("component.value_type_nput.type.SELECTION.displayText"), + dataIndex: "displayText", + key: "displayText", + width: 180, + render: (text: LocalizableStringInfo) => Lr(text.resourceName, text.name), + }, + { + title: $t("component.value_type_nput.type.SELECTION.value"), + dataIndex: "value", + key: "value", + width: 200, + }, + ]; + + if (!disabled) { + cols.push({ + title: $t("component.value_type_nput.type.SELECTION.actions.title"), + key: "action", + width: 220, + render: (_: any, record: SelectionStringValueItem) => ( + + {allowEdit && ( + + )} + {allowDelete && ( + + )} + + ), + }); + } + return cols; + }, [disabled, allowEdit, allowDelete, $t, Lr]); + + return ( +
+ + +
{$t("component.value_type_nput.type.name")} + + {$t("component.value_type_nput.validator.name")} + + + + + + {$t("component.value_type_nput.validator.NULL.name")} + + {$t("component.value_type_nput.validator.BOOLEAN.name")} + + + {$t("component.value_type_nput.validator.NUMERIC.name")} + + + {$t("component.value_type_nput.validator.STRING.name")} + + + + + + } + > + {/* FreeText - NUMERIC */} + {valueType.name === "FreeTextStringValueType" && valueType.validator.name === "NUMERIC" && ( +
+ +
{$t("component.value_type_nput.validator.NUMERIC.minValue")} + + {$t("component.value_type_nput.validator.NUMERIC.maxValue")} + + + + + updateValidatorProperty((v) => (v.minValue = val ? Number(val) : undefined))} + /> + + + updateValidatorProperty((v) => (v.maxValue = val ? Number(val) : undefined))} + /> + + + + )} + + {/* FreeText - STRING */} + {valueType.name === "FreeTextStringValueType" && valueType.validator.name === "STRING" && ( +
+ updateValidatorProperty((v) => (v.allowNull = e.target.checked))} + > + {$t("component.value_type_nput.validator.STRING.allowNull")} + + +
+
{$t("component.value_type_nput.validator.STRING.regularExpression")}
+ updateValidatorProperty((v) => (v.regularExpression = e.target.value))} + /> +
+ +
+ +
{$t("component.value_type_nput.validator.STRING.minLength")} + + {$t("component.value_type_nput.validator.STRING.maxLength")} + + + + + updateValidatorProperty((v) => (v.minLength = val ? Number(val) : undefined))} + /> + + + updateValidatorProperty((v) => (v.maxLength = val ? Number(val) : undefined))} + /> + + + + + )} + + {/* SELECTION Type */} + {valueType instanceof SelectionStringValueType && ( + + + {valueType.itemSource.items.length <= 0 && ( + + {$t("component.value_type_nput.type.SELECTION.itemsNotBeEmpty")} + + )} + + + {!disabled && ( + + + + + )} + + + } + > +
+ + )} + + + {/* Modal for Selection Item */} + { + setModalVisible(false); + modalForm.resetFields(); + }} + maskClosable={false} + width={600} + > +
+ { + // Logic to validate duplicate Display Text + if (!validateLocalizer(value)) { + return Promise.reject($t("component.value_type_nput.type.SELECTION.displayTextNotBeEmpty")); + } + if (valueType instanceof SelectionStringValueType) { + // const serializedValue = serializeLocalizer(value); + const exists = valueType.itemSource.items.some((x) => { + return serializeLocalizer(x.displayText) === value; + }); + if (exists) { + return Promise.reject($t("component.value_type_nput.type.SELECTION.duplicateKeyOrValue")); + } + } + return Promise.resolve(); + }, + }, + ]} + > + + + { + if (valueType instanceof SelectionStringValueType) { + const exists = valueType.itemSource.items.some((x) => { + return x.value === val; + }); + if (exists) { + return Promise.reject($t("component.value_type_nput.type.SELECTION.duplicateKeyOrValue")); + } + } + return Promise.resolve(); + }, + }, + ]} + > + + + +
+ + ); + }, +); + +ValueTypeInput.displayName = "ValueTypeInput"; + +export default ValueTypeInput; diff --git a/apps/react-admin/src/components/abp/string-value-type/validator.ts b/apps/react-admin/src/components/abp/string-value-type/validator.ts new file mode 100644 index 000000000..fc42bf3b0 --- /dev/null +++ b/apps/react-admin/src/components/abp/string-value-type/validator.ts @@ -0,0 +1,130 @@ +import type { Dictionary } from "#/abp-core"; +import { isNullOrUnDef } from "@/utils/abp/is"; +import { isBoolean, isNumber } from "@/utils/inference"; +import { isNullOrWhiteSpace } from "@/utils/string"; + +export interface ValueValidator { + isValid(value?: any): boolean; + name: string; + + properties: Dictionary; +} + +export class AlwaysValidValueValidator implements ValueValidator { + name = "NULL"; + properties: Dictionary; + constructor() { + this.properties = {}; + } + isValid(_value?: any): boolean { + return true; + } +} + +export class BooleanValueValidator implements ValueValidator { + name = "BOOLEAN"; + properties: Dictionary; + constructor() { + this.properties = {}; + } + isValid(value?: any): boolean { + if (isNullOrUnDef(value)) return true; + if (isBoolean(value)) return true; + const bolString = String(value).toLowerCase(); + if (bolString === "true" || bolString === "false") return true; + return false; + } +} + +export class NumericValueValidator implements ValueValidator { + name = "NUMERIC"; + properties: Dictionary; + get maxValue(): number | undefined { + return Number(this.properties.MaxValue); + } + + set maxValue(value: number) { + this.properties.MaxValue = value; + } + + get minValue(): number | undefined { + return Number(this.properties.MinValue); + } + + set minValue(value: number) { + this.properties.MinValue = value; + } + + constructor() { + this.properties = {}; + } + + _isValidInternal(value: number): boolean { + if (this.minValue && value < this.minValue) return false; + if (this.maxValue && value > this.maxValue) return false; + return true; + } + + isValid(value?: any): boolean { + if (isNullOrUnDef(value)) return true; + if (isNumber(value)) return this._isValidInternal(value); + const numString = String(value); + if (!isNullOrUnDef(numString)) { + const num = Number(numString); + if (num) return this._isValidInternal(num); + } + return false; + } +} + +export class StringValueValidator implements ValueValidator { + name = "STRING"; + properties: Dictionary; + get allowNull(): boolean { + return String(this.properties.AllowNull ?? "true")?.toLowerCase() === "true"; + } + + set allowNull(value: boolean) { + this.properties.AllowNull = value; + } + + get maxLength(): number | undefined { + return Number(this.properties.MaxLength); + } + + set maxLength(value: number) { + this.properties.MaxLength = value; + } + + get minLength(): number | undefined { + return Number(this.properties.MinLength); + } + + set minLength(value: number) { + this.properties.MinLength = value; + } + + get regularExpression(): string { + return String(this.properties.RegularExpression ?? ""); + } + + set regularExpression(value: string) { + this.properties.RegularExpression = value; + } + + constructor() { + this.properties = {}; + } + + isValid(value?: any): boolean { + if (!this.allowNull && isNullOrUnDef(value)) return false; + const valueString = String(value); + if (!this.allowNull && isNullOrWhiteSpace(valueString.trim())) return false; + if (this.minLength && this.minLength > 0 && valueString.length < this.minLength) return false; + if (this.maxLength && this.maxLength > 0 && valueString.length > this.maxLength) return false; + if (!isNullOrWhiteSpace(this.regularExpression)) { + return new RegExp(this.regularExpression).test(valueString); + } + return true; + } +} diff --git a/apps/react-admin/src/components/abp/string-value-type/value-type.ts b/apps/react-admin/src/components/abp/string-value-type/value-type.ts new file mode 100644 index 000000000..bc6191d9c --- /dev/null +++ b/apps/react-admin/src/components/abp/string-value-type/value-type.ts @@ -0,0 +1,119 @@ +import type { Dictionary, LocalizableStringInfo } from "#/abp-core"; + +import type { ValueValidator } from "./validator"; + +import { + AlwaysValidValueValidator, + BooleanValueValidator, + NumericValueValidator, + StringValueValidator, +} from "./validator"; + +export interface StringValueType { + name: string; + properties: Dictionary; + validator: ValueValidator; +} + +export interface SelectionStringValueItem { + displayText: LocalizableStringInfo; + value: string; +} + +export interface SelectionStringValueItemSource { + items: SelectionStringValueItem[]; +} + +export class FreeTextStringValueType implements StringValueType { + name = "FreeTextStringValueType"; + properties: Dictionary; + validator: ValueValidator; + constructor(validator?: ValueValidator) { + this.properties = {}; + this.validator = validator ?? new AlwaysValidValueValidator(); + } +} + +export class ToggleStringValueType implements StringValueType { + name = "ToggleStringValueType"; + properties: Dictionary; + validator: ValueValidator; + constructor(validator?: ValueValidator) { + this.properties = {}; + this.validator = validator ?? new BooleanValueValidator(); + } +} + +export class SelectionStringValueType implements StringValueType { + itemSource: SelectionStringValueItemSource; + name = "SelectionStringValueType"; + properties: Dictionary; + validator: ValueValidator; + constructor(validator?: ValueValidator) { + this.properties = {}; + this.itemSource = { + items: [], + }; + this.validator = validator ?? new AlwaysValidValueValidator(); + } +} + +class StringValueTypeSerializer { + _deserializeValidator(validator: any): ValueValidator { + let convertValidator: ValueValidator = new AlwaysValidValueValidator(); + if (validator.name) { + switch (validator.name) { + case "BOOLEAN": { + convertValidator = new BooleanValueValidator(); + break; + } + case "NULL": { + convertValidator = new AlwaysValidValueValidator(); + break; + } + case "NUMERIC": { + convertValidator = new NumericValueValidator(); + break; + } + case "STRING": { + convertValidator = new StringValueValidator(); + break; + } + } + } + convertValidator.properties = validator.properties; + return convertValidator; + } + + deserialize(value: string): StringValueType { + let valueType: StringValueType; + const valueTypeObj = JSON.parse(value); + switch (valueTypeObj.name) { + case "SELECTION": + case "SelectionStringValueType": { + valueType = new SelectionStringValueType(); + (valueType as SelectionStringValueType).itemSource = valueTypeObj.itemSource; + break; + } + case "TOGGLE": + case "ToggleStringValueType": { + valueType = new ToggleStringValueType(); + break; + } + default: { + valueType = new FreeTextStringValueType(); + break; + } + } + valueType.properties = valueTypeObj.properties; + valueType.validator = this._deserializeValidator(valueTypeObj.validator); + return valueType; + } + + serialize(value: StringValueType): string { + const valueTypeString = JSON.stringify(value); + return valueTypeString; + } +} + +export const valueTypeSerializer = new StringValueTypeSerializer(); diff --git a/apps/react-admin/src/constants/management/auditing/permissions.ts b/apps/react-admin/src/constants/management/auditing/permissions.ts index 66f903ae1..82e8a5392 100644 --- a/apps/react-admin/src/constants/management/auditing/permissions.ts +++ b/apps/react-admin/src/constants/management/auditing/permissions.ts @@ -4,3 +4,7 @@ export const AuditLogPermissions = { /** 删除 */ Delete: "AbpAuditing.AuditLog.Delete", }; +/** 系统日志权限 */ +export const SystemLogPermissions = { + Default: "AbpAuditing.SystemLog", +}; diff --git a/apps/react-admin/src/constants/management/features/index.ts b/apps/react-admin/src/constants/management/features/index.ts new file mode 100644 index 000000000..972144448 --- /dev/null +++ b/apps/react-admin/src/constants/management/features/index.ts @@ -0,0 +1 @@ +export * from "./permissions"; diff --git a/apps/react-admin/src/constants/management/features/permissions.ts b/apps/react-admin/src/constants/management/features/permissions.ts new file mode 100644 index 000000000..a2b8ebcf2 --- /dev/null +++ b/apps/react-admin/src/constants/management/features/permissions.ts @@ -0,0 +1,20 @@ +/** 分组权限 */ +export const GroupDefinitionsPermissions = { + /** 新增 */ + Create: "FeatureManagement.GroupDefinitions.Create", + Default: "FeatureManagement.GroupDefinitions", + /** 删除 */ + Delete: "FeatureManagement.GroupDefinitions.Delete", + /** 更新 */ + Update: "FeatureManagement.GroupDefinitions.Update", +}; +/** 功能定义权限 */ +export const FeatureDefinitionsPermissions = { + /** 新增 */ + Create: "FeatureManagement.Definitions.Create", + Default: "FeatureManagement.Definitions", + /** 删除 */ + Delete: "FeatureManagement.Definitions.Delete", + /** 更新 */ + Update: "FeatureManagement.Definitions.Update", +}; diff --git a/apps/react-admin/src/constants/management/localization/index.ts b/apps/react-admin/src/constants/management/localization/index.ts new file mode 100644 index 000000000..972144448 --- /dev/null +++ b/apps/react-admin/src/constants/management/localization/index.ts @@ -0,0 +1 @@ +export * from "./permissions"; diff --git a/apps/react-admin/src/constants/management/localization/permissions.ts b/apps/react-admin/src/constants/management/localization/permissions.ts new file mode 100644 index 000000000..a1d973c6e --- /dev/null +++ b/apps/react-admin/src/constants/management/localization/permissions.ts @@ -0,0 +1,30 @@ +/** 资源管理权限 */ +export const ResourcesPermissions = { + /** 新增 */ + Create: "LocalizationManagement.Resource.Create", + Default: "LocalizationManagement.Resource", + /** 删除 */ + Delete: "LocalizationManagement.Resource.Delete", + /** 更新 */ + Update: "LocalizationManagement.Resource.Update", +}; +/** 语言管理权限 */ +export const LanguagesPermissions = { + /** 新增 */ + Create: "LocalizationManagement.Language.Create", + Default: "LocalizationManagement.Language", + /** 删除 */ + Delete: "LocalizationManagement.Language.Delete", + /** 更新 */ + Update: "LocalizationManagement.Language.Update", +}; +/** 文本管理权限 */ +export const TextsPermissions = { + /** 新增 */ + Create: "LocalizationManagement.Text.Create", + Default: "LocalizationManagement.Text", + /** 删除 */ + Delete: "LocalizationManagement.Text.Delete", + /** 更新 */ + Update: "LocalizationManagement.Text.Update", +}; diff --git a/apps/react-admin/src/constants/notifications/permissions.ts b/apps/react-admin/src/constants/notifications/permissions.ts new file mode 100644 index 000000000..522d56d61 --- /dev/null +++ b/apps/react-admin/src/constants/notifications/permissions.ts @@ -0,0 +1,28 @@ +/** 分组权限 */ +export const GroupDefinitionsPermissions = { + /** 新增 */ + Create: "Notifications.GroupDefinitions.Create", + Default: "Notifications.GroupDefinitions", + /** 删除 */ + Delete: "Notifications.GroupDefinitions.Delete", + /** 更新 */ + Update: "Notifications.GroupDefinitions.Update", +}; +/** 通知定义权限 */ +export const NotificationDefinitionsPermissions = { + /** 新增 */ + Create: "Notifications.Definitions.Create", + Default: "Notifications.Definitions", + /** 删除 */ + Delete: "Notifications.Definitions.Delete", + /** 更新 */ + Update: "Notifications.Definitions.Update", +}; +/** 通知权限 */ +export const NotificationPermissions = { + /** 发送通知 */ + Create: "Notifications.Notification.Send", + Default: "Notifications.Notification", + /** 删除 */ + Delete: "Notifications.Notification.Delete", +}; diff --git a/apps/react-admin/src/constants/oss/permissions.ts b/apps/react-admin/src/constants/oss/permissions.ts new file mode 100644 index 000000000..810441674 --- /dev/null +++ b/apps/react-admin/src/constants/oss/permissions.ts @@ -0,0 +1,18 @@ +/** 容器权限 */ +export const ContainerPermissions = { + /** 新增 */ + Create: "AbpOssManagement.Container.Create", + Default: "AbpOssManagement.Container", + /** 删除 */ + Delete: "AbpOssManagement.Container.Delete", +}; +/** 容器权限 */ +export const OssObjectPermissions = { + /** 新增 */ + Create: "AbpOssManagement.OssObject.Create", + Default: "AbpOssManagement.OssObject", + /** 删除 */ + Delete: "AbpOssManagement.OssObject.Delete", + /** 下载 */ + Download: "AbpOssManagement.OssObject.Download", +}; diff --git a/apps/react-admin/src/constants/platform/permissions.ts b/apps/react-admin/src/constants/platform/permissions.ts new file mode 100644 index 000000000..4b9805a0a --- /dev/null +++ b/apps/react-admin/src/constants/platform/permissions.ts @@ -0,0 +1,59 @@ +/** 邮件消息权限 */ +export const EmailMessagesPermissions = { + Default: "Platform.EmailMessage", + /** 删除 */ + Delete: "Platform.EmailMessage.Delete", + /** 发送消息 */ + SendMessage: "Platform.EmailMessage.SendMessage", +}; +/** 短信消息权限 */ +export const SmsMessagesPermissions = { + Default: "Platform.SmsMessage", + /** 删除 */ + Delete: "Platform.SmsMessage.Delete", + /** 发送消息 */ + SendMessage: "Platform.SmsMessage.SendMessage", +}; +/** 数据字典权限 */ +export const DataDictionaryPermissions = { + /** 新增 */ + Create: "Platform.DataDictionary.Create", + /** 默认 */ + Default: "Platform.DataDictionary", + /** 删除 */ + Delete: "Platform.DataDictionary.Delete", + /** 管理项目 */ + ManageItems: "Platform.DataDictionary.ManageItems", + /** 移动 */ + Move: "Platform.DataDictionary.Move", + /** 更新 */ + Update: "Platform.DataDictionary.Update", +}; +/** 布局权限 */ +export const LayoutPermissions = { + /** 新增 */ + Create: "Platform.Layout.Create", + /** 默认 */ + Default: "Platform.Layout", + /** 删除 */ + Delete: "Platform.Layout.Delete", + /** 更新 */ + Update: "Platform.Layout.Update", +}; +/** 菜单权限 */ +export const MenuPermissions = { + /** 新增 */ + Create: "Platform.Menu.Create", + /** 默认 */ + Default: "Platform.Menu", + /** 删除 */ + Delete: "Platform.Menu.Delete", + /** 管理角色菜单 */ + ManageRoles: "Platform.Menu.ManageRoles", + /** 管理用户收藏菜单 */ + ManageUserFavorites: "Platform.Menu.ManageUserFavorites", + /** 管理用户菜单 */ + ManageUsers: "Platform.Menu.ManageUsers", + /** 更新 */ + Update: "Platform.Menu.Update", +}; diff --git a/apps/react-admin/src/constants/request/http-status.ts b/apps/react-admin/src/constants/request/http-status.ts new file mode 100644 index 000000000..de5ff3e5e --- /dev/null +++ b/apps/react-admin/src/constants/request/http-status.ts @@ -0,0 +1,44 @@ +export enum HttpStatusCode { + Accepted = 202, + Ambiguous = 300, + BadGateway = 502, + BadRequest = 400, + Conflict = 409, + Continue = 100, + Created = 201, + ExpectationFailed = 417, + Forbidden = 403, + GatewayTimeout = 504, + Gone = 410, + HttpVersionNotSupported = 505, + InternalServerError = 500, + LengthRequired = 411, + MethodNotAllowed = 405, + Moved = 301, + NoContent = 204, + NonAuthoritativeInformation = 203, + NotAcceptable = 406, + NotFound = 404, + NotImplemented = 501, + NotModified = 304, + OK = 200, + PartialContent = 206, + PaymentRequired = 402, + PreconditionFailed = 412, + ProxyAuthenticationRequired = 407, + Redirect = 302, + RedirectKeepVerb = 307, + RedirectMethod = 303, + RequestedRangeNotSatisfiable = 416, + RequestEntityTooLarge = 413, + RequestTimeout = 408, + RequestUriTooLong = 414, + ResetContent = 205, + ServiceUnavailable = 503, + SwitchingProtocols = 101, + Unauthorized = 401, + UnsupportedMediaType = 415, + Unused = 306, + UpgradeRequired = 426, + UseProxy = 305, +} diff --git a/apps/react-admin/src/constants/saas/permissions.ts b/apps/react-admin/src/constants/saas/permissions.ts new file mode 100644 index 000000000..047702331 --- /dev/null +++ b/apps/react-admin/src/constants/saas/permissions.ts @@ -0,0 +1,28 @@ +/** 版本权限 */ +export const EditionsPermissions = { + /** 新增 */ + Create: "AbpSaas.Editions.Create", + /** 默认 */ + Default: "AbpSaas.Editions", + /** 删除 */ + Delete: "AbpSaas.Editions.Delete", + /** 管理功能 */ + ManageFeatures: "AbpSaas.Editions.ManageFeatures", + /** 更新 */ + Update: "AbpSaas.Editions.Update", +}; +/** 租户权限 */ +export const TenantsPermissions = { + /** 新增 */ + Create: "AbpSaas.Tenants.Create", + /** 默认 */ + Default: "AbpSaas.Tenants", + /** 删除 */ + Delete: "AbpSaas.Tenants.Delete", + /** 管理连接字符串 */ + ManageConnectionStrings: "AbpSaas.Tenants.ManageConnectionStrings", + /** 管理功能 */ + ManageFeatures: "AbpSaas.Tenants.ManageFeatures", + /** 更新 */ + Update: "AbpSaas.Tenants.Update", +}; diff --git a/apps/react-admin/src/constants/tasks/permissions.ts b/apps/react-admin/src/constants/tasks/permissions.ts new file mode 100644 index 000000000..f38ca9645 --- /dev/null +++ b/apps/react-admin/src/constants/tasks/permissions.ts @@ -0,0 +1,24 @@ +/** 作业管理权限 */ +export const BackgroundJobsPermissions = { + /** 新增 */ + Create: "TaskManagement.BackgroundJobs.Create", + Default: "TaskManagement.BackgroundJobs", + /** 删除 */ + Delete: "TaskManagement.BackgroundJobs.Delete", + /** 管理触发器 */ + ManageActions: "TaskManagement.BackgroundJobs.ManageActions", + /** 管理系统作业 */ + ManageSystemJobs: "TaskManagement.BackgroundJobs.ManageSystemJobs", + /** 暂停 */ + Pause: "TaskManagement.BackgroundJobs.Pause", + /** 恢复 */ + Resume: "TaskManagement.BackgroundJobs.Resume", + /** 启动 */ + Start: "TaskManagement.BackgroundJobs.Start", + /** 停止 */ + Stop: "TaskManagement.BackgroundJobs.Stop", + /** 触发 */ + Trigger: "TaskManagement.BackgroundJobs.Trigger", + /** 修改 */ + Update: "TaskManagement.BackgroundJobs.Update", +}; diff --git a/apps/react-admin/src/constants/text-templating/permissions.ts b/apps/react-admin/src/constants/text-templating/permissions.ts new file mode 100644 index 000000000..8123be7d7 --- /dev/null +++ b/apps/react-admin/src/constants/text-templating/permissions.ts @@ -0,0 +1,10 @@ +/** 模板定义权限 */ +export const TextTemplatePermissions = { + /** 新增 */ + Create: "AbpTextTemplating.TextTemplateDefinitions.Create", + Default: "AbpTextTemplating.TextTemplateDefinitions", + /** 删除 */ + Delete: "AbpTextTemplating.TextTemplateDefinitions.Delete", + /** 更新 */ + Update: "AbpTextTemplating.TextTemplateDefinitions.Update", +}; diff --git a/apps/react-admin/src/constants/webhooks/permissions.ts b/apps/react-admin/src/constants/webhooks/permissions.ts new file mode 100644 index 000000000..5a07466f0 --- /dev/null +++ b/apps/react-admin/src/constants/webhooks/permissions.ts @@ -0,0 +1,41 @@ +/** 分组权限 */ +export const GroupDefinitionsPermissions = { + /** 新增 */ + Create: "AbpWebhooks.GroupDefinitions.Create", + Default: "AbpWebhooks.GroupDefinitions", + /** 删除 */ + Delete: "AbpWebhooks.GroupDefinitions.Delete", + /** 更新 */ + Update: "AbpWebhooks.GroupDefinitions.Update", +}; + +/** Webhook定义权限 */ +export const WebhookDefinitionsPermissions = { + /** 新增 */ + Create: "AbpWebhooks.Definitions.Create", + Default: "AbpWebhooks.Definitions", + /** 删除 */ + Delete: "AbpWebhooks.Definitions.Delete", + /** 更新 */ + Update: "AbpWebhooks.Definitions.Update", +}; + +/** Webhook订阅权限 */ +export const WebhookSubscriptionPermissions = { + /** 新增 */ + Create: "AbpWebhooks.Subscriptions.Create", + Default: "AbpWebhooks.Subscriptions", + /** 删除 */ + Delete: "AbpWebhooks.Subscriptions.Delete", + /** 更新 */ + Update: "AbpWebhooks.Subscriptions.Update", +}; + +/** Webhook发送记录权限 */ +export const WebhooksSendAttemptsPermissions = { + Default: "AbpWebhooks.SendAttempts", + /** 删除 */ + Delete: "AbpWebhooks.SendAttempts.Delete", + /** 更新 */ + Resend: "AbpWebhooks.SendAttempts.Resend", +}; diff --git a/apps/react-admin/src/hooks/abp/fake-hooks/readme.md b/apps/react-admin/src/hooks/abp/fake-hooks/readme.md new file mode 100644 index 000000000..5256cfaf5 --- /dev/null +++ b/apps/react-admin/src/hooks/abp/fake-hooks/readme.md @@ -0,0 +1,2 @@ +1. change them to fake as little as possible +2. /abp-react-admin/slash-admin/src/utils/abp diff --git a/apps/react-admin/src/hooks/abp/fake-hooks/simple-state-checking/use-require-authenticated-simple-state-checker.ts b/apps/react-admin/src/hooks/abp/fake-hooks/simple-state-checking/use-require-authenticated-simple-state-checker.ts new file mode 100644 index 000000000..a111f2d13 --- /dev/null +++ b/apps/react-admin/src/hooks/abp/fake-hooks/simple-state-checking/use-require-authenticated-simple-state-checker.ts @@ -0,0 +1,37 @@ +import type { + CurrentUser, + IHasSimpleStateCheckers, + ISimpleStateChecker, + SimpleStateCheckerContext, +} from "#/abp-core/global"; +import useAbpStore from "@/store/abpCoreStore"; + +export interface RequireAuthenticatedStateChecker { + name: string; +} + +export class RequireAuthenticatedSimpleStateChecker> + implements ISimpleStateChecker, RequireAuthenticatedStateChecker +{ + _currentUser?: CurrentUser; + name = "A"; + constructor(currentUser?: CurrentUser) { + this._currentUser = currentUser; + } + isEnabled(_context: SimpleStateCheckerContext): boolean { + return this._currentUser?.isAuthenticated ?? false; + } + + serialize(): string { + return JSON.stringify({ + T: this.name, + }); + } +} + +export function requireAuthenticatedSimpleStateChecker< + TState extends IHasSimpleStateCheckers, +>(): ISimpleStateChecker { + const { application } = useAbpStore.getState(); + return new RequireAuthenticatedSimpleStateChecker(application?.currentUser); +} diff --git a/apps/react-admin/src/hooks/abp/fake-hooks/simple-state-checking/use-require-features-simple-state-checker.ts b/apps/react-admin/src/hooks/abp/fake-hooks/simple-state-checking/use-require-features-simple-state-checker.ts new file mode 100644 index 000000000..317c6adcb --- /dev/null +++ b/apps/react-admin/src/hooks/abp/fake-hooks/simple-state-checking/use-require-features-simple-state-checker.ts @@ -0,0 +1,42 @@ +import type { IHasSimpleStateCheckers, ISimpleStateChecker, SimpleStateCheckerContext } from "#/abp-core/global"; +import type { IFeatureChecker } from "#/features"; +import { useFeatures } from "../use-abp-feature"; + +export interface RequireFeaturesStateChecker { + featureNames: string[]; + name: string; + requiresAll: boolean; +} + +export class RequireFeaturesSimpleStateChecker> + implements ISimpleStateChecker, RequireFeaturesStateChecker +{ + _featureChecker: IFeatureChecker; + featureNames: string[]; + name = "F"; + requiresAll: boolean; + constructor(featureChecker: IFeatureChecker, featureNames: string[], requiresAll = false) { + this._featureChecker = featureChecker; + this.featureNames = featureNames; + this.requiresAll = requiresAll; + } + isEnabled(_context: SimpleStateCheckerContext): boolean { + return this._featureChecker.isEnabled(this.featureNames, this.requiresAll); + } + + serialize(): string { + return JSON.stringify({ + A: this.requiresAll, + N: this.featureNames, + T: this.name, + }); + } +} + +export function useRequireFeaturesSimpleStateChecker>( + featureNames: string[], + requiresAll = false, +): ISimpleStateChecker { + const featureChecker = useFeatures(); + return new RequireFeaturesSimpleStateChecker(featureChecker, featureNames, requiresAll); +} diff --git a/apps/react-admin/src/hooks/abp/fake-hooks/simple-state-checking/use-require-global-features-simple-state-checker.ts b/apps/react-admin/src/hooks/abp/fake-hooks/simple-state-checking/use-require-global-features-simple-state-checker.ts new file mode 100644 index 000000000..4b0474e80 --- /dev/null +++ b/apps/react-admin/src/hooks/abp/fake-hooks/simple-state-checking/use-require-global-features-simple-state-checker.ts @@ -0,0 +1,43 @@ +import type { IGlobalFeatureChecker } from "#/features"; +import type { IHasSimpleStateCheckers, ISimpleStateChecker, SimpleStateCheckerContext } from "#/abp-core/global"; + +import { useGlobalFeatures } from "../use-abp-global-feature"; + +export interface RequireGlobalFeaturesStateChecker { + globalFeatureNames: string[]; + name: string; + requiresAll: boolean; +} + +export class RequireGlobalFeaturesSimpleStateChecker> + implements ISimpleStateChecker, RequireGlobalFeaturesStateChecker +{ + _globalFeatureChecker: IGlobalFeatureChecker; + globalFeatureNames: string[]; + name = "G"; + requiresAll: boolean; + constructor(globalFeatureChecker: IGlobalFeatureChecker, globalFeatureNames: string[], requiresAll = false) { + this._globalFeatureChecker = globalFeatureChecker; + this.globalFeatureNames = globalFeatureNames; + this.requiresAll = requiresAll; + } + isEnabled(_context: SimpleStateCheckerContext): boolean { + return this._globalFeatureChecker.isEnabled(this.globalFeatureNames, this.requiresAll); + } + + serialize(): string { + return JSON.stringify({ + A: this.requiresAll, + N: this.globalFeatureNames, + T: this.name, + }); + } +} + +export function useRequireGlobalFeaturesSimpleStateChecker>( + globalFeatureNames: string[], + requiresAll = false, +): ISimpleStateChecker { + const globalFeatureChecker = useGlobalFeatures(); + return new RequireGlobalFeaturesSimpleStateChecker(globalFeatureChecker, globalFeatureNames, requiresAll); +} diff --git a/apps/react-admin/src/hooks/abp/fake-hooks/simple-state-checking/use-require-permissions-simple-state-checker.ts b/apps/react-admin/src/hooks/abp/fake-hooks/simple-state-checking/use-require-permissions-simple-state-checker.ts new file mode 100644 index 000000000..8ef637965 --- /dev/null +++ b/apps/react-admin/src/hooks/abp/fake-hooks/simple-state-checking/use-require-permissions-simple-state-checker.ts @@ -0,0 +1,93 @@ +import type { + IHasSimpleStateCheckers, + ISimpleBatchStateChecker, + ISimpleStateChecker, + SimpleBatchStateCheckerContext, + SimpleStateCheckerContext, +} from "#/abp-core/global"; +import type { IPermissionChecker } from "#/abp-core/permissions"; + +import { useAuthorization } from "../use-abp-authorization"; + +export class RequirePermissionsSimpleBatchStateCheckerModel> { + permissions: string[]; + requiresAll: boolean; + state: TState; + constructor(state: TState, permissions: string[], requiresAll = true) { + this.state = state; + this.permissions = permissions; + this.requiresAll = requiresAll; + } +} + +export interface RequirePermissionsStateChecker> { + model: RequirePermissionsSimpleBatchStateCheckerModel; + name: string; +} + +export class RequirePermissionsSimpleStateChecker> + implements ISimpleStateChecker, RequirePermissionsStateChecker +{ + _permissionChecker: IPermissionChecker; + model: RequirePermissionsSimpleBatchStateCheckerModel; + name = "P"; + constructor(permissionChecker: IPermissionChecker, model: RequirePermissionsSimpleBatchStateCheckerModel) { + this.model = model; + this._permissionChecker = permissionChecker; + } + isEnabled(_context: SimpleStateCheckerContext): boolean { + return this._permissionChecker.isGranted(this.model.permissions, this.model.requiresAll); + } + + serialize(): string { + return JSON.stringify({ + A: this.model.requiresAll, + N: this.model.permissions, + T: this.name, + }); + } +} + +export class RequirePermissionsSimpleBatchStateChecker> + implements ISimpleBatchStateChecker +{ + _permissionChecker: IPermissionChecker; + models: RequirePermissionsSimpleBatchStateCheckerModel[]; + name = "P"; + constructor(permissionChecker: IPermissionChecker, models: RequirePermissionsSimpleBatchStateCheckerModel[]) { + this.models = models; + this._permissionChecker = permissionChecker; + } + isEnabled(context: SimpleBatchStateCheckerContext) { + // 1. Initialize a Map instead of a plain object + const result = new Map(); + + context.states.forEach((state) => { + const model = this.models.find((x) => x.state === state); + if (model) { + // 2. Use .set() to map the object key to the boolean value + result.set(model.state, this._permissionChecker.isGranted(model.permissions, model.requiresAll)); + } + }); + + return result; + } + + serialize(): string | undefined { + return undefined; + } +} + +export function useRequirePermissionsSimpleStateChecker>( + model: RequirePermissionsSimpleBatchStateCheckerModel, +): ISimpleStateChecker { + const permissionChecker = useAuthorization(); + return new RequirePermissionsSimpleStateChecker(permissionChecker, model); +} + +export function useRequirePermissionsSimpleBatchStateChecker>( + models: RequirePermissionsSimpleBatchStateCheckerModel[], +): ISimpleBatchStateChecker { + const permissionChecker = useAuthorization(); + return new RequirePermissionsSimpleBatchStateChecker(permissionChecker, models); +} diff --git a/apps/react-admin/src/hooks/abp/fake-hooks/use-abp-authorization.ts b/apps/react-admin/src/hooks/abp/fake-hooks/use-abp-authorization.ts new file mode 100644 index 000000000..9fac741c0 --- /dev/null +++ b/apps/react-admin/src/hooks/abp/fake-hooks/use-abp-authorization.ts @@ -0,0 +1,33 @@ +import type { IPermissionChecker } from "#/abp-core"; +import useAbpStore from "@/store/abpCoreStore"; +// import { useMemo } from "react"; + +export function useAuthorization(): IPermissionChecker { + // const application = useAbpStore((state) => state.application); + const { application } = useAbpStore.getState(); + // const grantedPolicies = useMemo(() => { + // return application?.auth.grantedPolicies ?? {}; + // }, [application]); + const grantedPolicies = application?.auth.grantedPolicies ?? {}; + + function isGranted(name: string | string[], requiresAll?: boolean): boolean { + if (Array.isArray(name)) { + if (requiresAll === undefined || requiresAll === true) { + return name.every((n) => grantedPolicies[n]); + } + return name.some((n) => grantedPolicies[n]); + } + return grantedPolicies[name] ?? false; + } + + function authorize(name: string | string[]): void { + if (!isGranted(name)) { + throw new Error(`Authorization failed! Given policy has not granted: ${name}`); + } + } + + return { + authorize, + isGranted, + }; +} diff --git a/apps/react-admin/src/hooks/abp/fake-hooks/use-abp-feature.ts b/apps/react-admin/src/hooks/abp/fake-hooks/use-abp-feature.ts new file mode 100644 index 000000000..43b4f5c9c --- /dev/null +++ b/apps/react-admin/src/hooks/abp/fake-hooks/use-abp-feature.ts @@ -0,0 +1,55 @@ +import type { FeatureValue, IFeatureChecker } from "#/features"; +import useAbpStore from "@/store/abpCoreStore"; +// import { useMemo } from "react"; + +export function useFeatures(): IFeatureChecker { + // const application = useAbpStore((state) => state.application); + const { application } = useAbpStore.getState(); + // const features = useMemo(() => { + // if (!application?.features?.values) { + // return []; + // } + // return Object.keys(application.features.values).map((name) => ({ + // name, + // value: application.features.values[name] ?? "", + // })); + // }, [application]); + + let features: FeatureValue[]; + if (!application?.features?.values) { + features = []; + } else { + features = Object.keys(application.features.values).map((name) => ({ + name, + value: application.features.values[name] ?? "", + })); + } + + function get(name: string): FeatureValue | undefined { + return features.find((feature) => feature.name === name); + } + + function _isEnabled(name: string): boolean { + const setting = get(name); + return setting?.value.toLowerCase() === "true"; + } + + const featureChecker: IFeatureChecker = { + getOrEmpty(name: string) { + return get(name)?.value ?? ""; + }, + + isEnabled(featureNames: string | string[], requiresAll?: boolean) { + if (Array.isArray(featureNames)) { + if (featureNames.length === 0) return true; + if (requiresAll === undefined || requiresAll === true) { + return featureNames.every(_isEnabled); + } + return featureNames.some(_isEnabled); + } + return _isEnabled(featureNames); + }, + }; + + return featureChecker; +} diff --git a/apps/react-admin/src/hooks/abp/fake-hooks/use-abp-global-feature.ts b/apps/react-admin/src/hooks/abp/fake-hooks/use-abp-global-feature.ts new file mode 100644 index 000000000..8bca6dc7f --- /dev/null +++ b/apps/react-admin/src/hooks/abp/fake-hooks/use-abp-global-feature.ts @@ -0,0 +1,43 @@ +import useAbpStore from "@/store/abpCoreStore"; +import { isNullOrWhiteSpace } from "@/utils/string"; +// import { useMemo } from "react"; + +export function useGlobalFeatures() { + // const application = useAbpStore((state) => state.application); + const { application } = useAbpStore.getState(); + + // const enabledFeatures = useMemo(() => { + // if (!application?.globalFeatures?.enabledFeatures) { + // return []; + // } + // return application.globalFeatures.enabledFeatures; + // }, [application]); + + let enabledFeatures: string[]; + if (!application?.globalFeatures?.enabledFeatures) { + enabledFeatures = []; + } else { + enabledFeatures = application.globalFeatures.enabledFeatures; + } + + function _isEnabled(name: string): boolean { + // Find if the feature exists in the enabled list + const feature = enabledFeatures.find((f) => f === name); + return !isNullOrWhiteSpace(feature); + } + + function isEnabled(featureNames: string | string[], requiresAll?: boolean): boolean { + if (Array.isArray(featureNames)) { + if (featureNames.length === 0) return true; + if (requiresAll === undefined || requiresAll === true) { + return featureNames.every(_isEnabled); + } + return featureNames.some(_isEnabled); + } + return _isEnabled(featureNames); + } + + return { + isEnabled, + }; +} diff --git a/apps/react-admin/src/hooks/abp/fake-hooks/use-http-status-code-map.ts b/apps/react-admin/src/hooks/abp/fake-hooks/use-http-status-code-map.ts new file mode 100644 index 000000000..c47e4f2df --- /dev/null +++ b/apps/react-admin/src/hooks/abp/fake-hooks/use-http-status-code-map.ts @@ -0,0 +1,71 @@ +import { HttpStatusCode } from "@/constants/request/http-status"; + +export function useHttpStatusCodeMap() { + const httpStatusCodeMap: { [key: number]: string } = { + [HttpStatusCode.Accepted]: "202 - Accepted", + [HttpStatusCode.Ambiguous]: "300 - Ambiguous/Multiple Choices", + [HttpStatusCode.BadGateway]: "502 - Bad Gateway", + [HttpStatusCode.BadRequest]: "400 - Bad Request", + [HttpStatusCode.Conflict]: "409 - Conflict", + [HttpStatusCode.Continue]: "100 - Continue", + [HttpStatusCode.Created]: "201 - Created", + [HttpStatusCode.ExpectationFailed]: "417 - Expectation Failed", + [HttpStatusCode.Forbidden]: "403 - Forbidden", + [HttpStatusCode.GatewayTimeout]: "504 - Gateway Timeout", + [HttpStatusCode.Gone]: "410 - Gone", + [HttpStatusCode.HttpVersionNotSupported]: "505 - Http Version Not Supported", + [HttpStatusCode.InternalServerError]: "500 - Internal Server Error", + [HttpStatusCode.LengthRequired]: "411 - Length Required", + [HttpStatusCode.MethodNotAllowed]: "405 - Method Not Allowed", + [HttpStatusCode.Moved]: "301 - Moved/Moved Permanently", + [HttpStatusCode.NoContent]: "204 - No Content", + [HttpStatusCode.NonAuthoritativeInformation]: "203 - Non Authoritative Information", + [HttpStatusCode.NotAcceptable]: "406 - Not Acceptable", + [HttpStatusCode.NotFound]: "404 - Not Found", + [HttpStatusCode.NotImplemented]: "501 - Not Implemented", + [HttpStatusCode.NotModified]: "304 - Not Modified", + [HttpStatusCode.OK]: "200 - OK", + [HttpStatusCode.PartialContent]: "206 - Partial Content", + [HttpStatusCode.PaymentRequired]: "402 - Payment Required", + [HttpStatusCode.PreconditionFailed]: "412 - Precondition Failed", + [HttpStatusCode.ProxyAuthenticationRequired]: "407 - Proxy Authentication Required", + [HttpStatusCode.Redirect]: "302 - Found/Redirect", + [HttpStatusCode.RedirectKeepVerb]: "307 - Redirect Keep Verb/Temporary Redirect", + [HttpStatusCode.RedirectMethod]: "303 - Redirect Method/See Other", + [HttpStatusCode.RequestedRangeNotSatisfiable]: "416 - Requested Range Not Satisfiable", + [HttpStatusCode.RequestEntityTooLarge]: "413 - Request Entity Too Large", + [HttpStatusCode.RequestTimeout]: "408 - Request Timeout", + [HttpStatusCode.RequestUriTooLong]: "414 - Request Uri Too Long", + [HttpStatusCode.ResetContent]: "205 - Reset Content", + [HttpStatusCode.ServiceUnavailable]: "503 - Service Unavailable", + [HttpStatusCode.SwitchingProtocols]: "101 - Switching Protocols", + [HttpStatusCode.Unauthorized]: "401 - Unauthorized", + [HttpStatusCode.UnsupportedMediaType]: "415 - Unsupported Media Type", + [HttpStatusCode.Unused]: "306 - Unused", + [HttpStatusCode.UpgradeRequired]: "426 - Upgrade Required", + [HttpStatusCode.UseProxy]: "305 - Use Proxy", + }; + + function getHttpStatusColor(statusCode: HttpStatusCode) { + if (statusCode < 200) { + return "default"; + } + if (statusCode >= 200 && statusCode < 300) { + return "success"; + } + if (statusCode >= 300 && statusCode < 400) { + return "processing"; + } + if (statusCode >= 400 && statusCode < 500) { + return "warning"; + } + if (statusCode >= 500) { + return "error"; + } + } + + return { + getHttpStatusColor, + httpStatusCodeMap, + }; +} diff --git a/apps/react-admin/src/hooks/abp/fake-hooks/use-simple-state-check.ts b/apps/react-admin/src/hooks/abp/fake-hooks/use-simple-state-check.ts new file mode 100644 index 000000000..6df2e7dfb --- /dev/null +++ b/apps/react-admin/src/hooks/abp/fake-hooks/use-simple-state-check.ts @@ -0,0 +1,107 @@ +import { isNullOrUnDef } from "@/utils/abp/is"; +import type { IHasSimpleStateCheckers, ISimpleStateChecker, ISimpleStateCheckerSerializer } from "#/abp-core/global"; + +import { isNullOrWhiteSpace } from "@/utils/string"; +import { requireAuthenticatedSimpleStateChecker } from "./simple-state-checking/use-require-authenticated-simple-state-checker"; +import { useRequireFeaturesSimpleStateChecker } from "./simple-state-checking/use-require-features-simple-state-checker"; +import { useRequirePermissionsSimpleStateChecker } from "./simple-state-checking/use-require-permissions-simple-state-checker"; +import { useRequireGlobalFeaturesSimpleStateChecker } from "./simple-state-checking/use-require-global-features-simple-state-checker"; + +export function useSimpleStateCheck>(): ISimpleStateCheckerSerializer { + function deserialize>( + jsonObject: any, + state: TState, + ): ISimpleStateChecker | undefined { + if (isNullOrUnDef(jsonObject) || !Reflect.has(jsonObject, "T")) { + return undefined; + } + switch (String(jsonObject.T)) { + case "A": { + return requireAuthenticatedSimpleStateChecker(); + } + case "F": { + const features = jsonObject.N as string[]; + if (features === undefined) { + throw new Error(`'N' is not an array in the serialized state checker! JsonObject: ${jsonObject}`); + } + return useRequireFeaturesSimpleStateChecker(features, jsonObject.A === true); + } + case "G": { + const globalFeatures = jsonObject.N as string[]; + if (globalFeatures === undefined) { + throw new Error(`'N' is not an array in the serialized state checker! JsonObject: ${jsonObject}`); + } + return useRequireGlobalFeaturesSimpleStateChecker(globalFeatures, jsonObject.A === true); + } + case "P": { + const permissions = jsonObject.N as string[]; + if (permissions === undefined) { + throw new Error(`'N' is not an array in the serialized state checker! JsonObject: ${jsonObject}`); + } + return useRequirePermissionsSimpleStateChecker({ + permissions, + requiresAll: jsonObject.A === true, + state, + }); + } + default: { + return undefined; + } + } + } + + function deserializeArray>( + value: string, + state: TState, + ): ISimpleStateChecker[] { + if (isNullOrWhiteSpace(value)) return []; + const jsonObject = JSON.parse(value); + if (isNullOrUnDef(jsonObject)) return []; + if (Array.isArray(jsonObject)) { + if (jsonObject.length === 0) return []; + return jsonObject + .map((json) => deserialize(json, state)) + .filter((checker) => !isNullOrUnDef(checker)) + .map((checker) => checker); + } + const stateChecker = deserialize(jsonObject, state); + if (!stateChecker) return []; + return [stateChecker]; + } + + function serialize>( + checker: ISimpleStateChecker, + ): string | undefined { + return checker.serialize(); + } + + function serializeArray>( + stateCheckers: ISimpleStateChecker[], + ): string | undefined { + if (stateCheckers.length === 0) return undefined; + if (stateCheckers.length === 1) { + const stateChecker = stateCheckers[0]; + const single = stateChecker?.serialize(); + if (isNullOrUnDef(single)) return undefined; + return `[${single}]`; + } + let serializedCheckers = ""; + stateCheckers.forEach((checker) => { + const serializedChecker = checker.serialize(); + if (!isNullOrUnDef(serializedChecker)) { + serializedCheckers += `${serializedChecker},`; + } + }); + if (serializedCheckers.endsWith(",")) { + serializedCheckers = serializedCheckers.slice(0, Math.max(0, serializedCheckers.length - 1)); + } + return serializedCheckers.length > 0 ? `[${serializedCheckers}]` : undefined; + } + + return { + deserialize, + deserializeArray, + serialize, + serializeArray, + }; +} diff --git a/apps/react-admin/src/hooks/abp/identity/usePasswordValidator.ts b/apps/react-admin/src/hooks/abp/identity/usePasswordValidator.ts new file mode 100644 index 000000000..b1c0daa12 --- /dev/null +++ b/apps/react-admin/src/hooks/abp/identity/usePasswordValidator.ts @@ -0,0 +1,69 @@ +import { useAbpSettings } from "@/store/abpSettingStore"; +import { ValidationEnum } from "@/constants/abp-core"; +import { getUnique, isNullOrWhiteSpace } from "@/utils/string"; +import { isDigit, isLetterOrDigit, isLower, isUpper } from "@/utils/abp/regex"; +import { useLocalizer } from "../use-localization"; +import { useMemo } from "react"; + +export function usePasswordValidator() { + const { getNumber, isTrue } = useAbpSettings(); + const { L } = useLocalizer(["AbpIdentity", "AbpValidation", "AbpUi"]); + + const passwordSetting = useMemo(() => { + return { + requiredDigit: isTrue("Abp.Identity.Password.RequireDigit"), + requiredLength: getNumber("Abp.Identity.Password.RequiredLength"), + requiredLowercase: isTrue("Abp.Identity.Password.RequireLowercase"), + requiredUniqueChars: getNumber("Abp.Identity.Password.RequiredUniqueChars"), + requireNonAlphanumeric: isTrue("Abp.Identity.Password.RequireNonAlphanumeric"), + requireUppercase: isTrue("Abp.Identity.Password.RequireUppercase"), + }; + }, [getNumber, isTrue]); + + function validate(password: string): Promise { + return new Promise((resolve, reject) => { + // 1. Check Empty + if (isNullOrWhiteSpace(password)) { + return reject(L(ValidationEnum.FieldRequired, [L("DisplayName:Password")])); + } + + const setting = passwordSetting; + + // 2. Check Length + if (setting.requiredLength > 0 && password.length < setting.requiredLength) { + return reject(L("Volo.Abp.Identity:PasswordTooShort", [String(setting.requiredLength)])); + } + + // 3. Check Non-Alphanumeric + if (setting.requireNonAlphanumeric && isLetterOrDigit(password)) { + return reject(L("Volo.Abp.Identity:PasswordRequiresNonAlphanumeric")); + } + + // 4. Check Digit + if (setting.requiredDigit && !isDigit(password)) { + return reject(L("Volo.Abp.Identity:PasswordRequiresDigit")); + } + + // 5. Check Lowercase + if (setting.requiredLowercase && !isLower(password)) { + return reject(L("Volo.Abp.Identity:PasswordRequiresLower")); + } + + // 6. Check Uppercase + if (setting.requireUppercase && !isUpper(password)) { + return reject(L("Volo.Abp.Identity:PasswordRequiresUpper")); + } + + // 7. Check Unique Chars + if (setting.requiredUniqueChars >= 1 && getUnique(password).length < setting.requiredUniqueChars) { + return reject(L("Volo.Abp.Identity:PasswordRequiredUniqueChars", [String(setting.requiredUniqueChars)])); + } + + return resolve(); + }); + } + + return { + validate, + }; +} diff --git a/apps/react-admin/src/hooks/abp/identity/useRandomPassword.ts b/apps/react-admin/src/hooks/abp/identity/useRandomPassword.ts new file mode 100644 index 000000000..b729ebce6 --- /dev/null +++ b/apps/react-admin/src/hooks/abp/identity/useRandomPassword.ts @@ -0,0 +1,65 @@ +import { useAbpSettings } from "@/store/abpSettingStore"; + +/** + * 摘自 https://www.html5tricks.com/demo/js-passwd-generator/index.html + * 侵权请联系删除 + */ +export function useRandomPassword() { + const randomFunc: { [key: string]: () => string } = { + defaultNumber: getRandomNumber, + lower: getRandomLower, + number: getRandomNumber, + symbol: getRandomSymbol, + upper: getRandomUpper, + }; + + function getRandomLower() { + return String.fromCodePoint(Math.floor(Math.random() * 26) + 97); + } + + function getRandomUpper() { + return String.fromCodePoint(Math.floor(Math.random() * 26) + 65); + } + + function getRandomNumber() { + return String.fromCodePoint(Math.floor(Math.random() * 10) + 48); + } + + function getRandomSymbol() { + const symbols = '~!@#$%^&*()_+{}":?><;.,'; + return symbols[Math.floor(Math.random() * symbols.length)] ?? ""; + } + + function generatePassword() { + const { getNumber, isTrue } = useAbpSettings(); + // 根据配置项生成随机密码 + // 密码长度 + const length = getNumber("Abp.Identity.Password.RequiredLength"); + // 需要小写字母 + const lower = isTrue("Abp.Identity.Password.RequireLowercase"); + // 需要大写字母 + const upper = isTrue("Abp.Identity.Password.RequireUppercase"); + // 需要数字 + const number = isTrue("Abp.Identity.Password.RequireDigit"); + // 需要符号 + const symbol = isTrue("Abp.Identity.Password.RequireNonAlphanumeric"); + // 默认生成数字 + const defaultNumber = !lower && !upper && !number && !symbol; + + let generatedPassword = ""; + const typesArr = [{ lower }, { upper }, { number }, { symbol }, { defaultNumber }].filter( + (item) => Object.values(item)[0], + ); + for (let i = 0; i < length; i++) { + typesArr.forEach((type) => { + const funcName = Object.keys(type)[0]; + if (funcName && randomFunc[funcName]) { + generatedPassword += randomFunc[funcName](); + } + }); + } + return generatedPassword.slice(0, length); + } + + return { generatePassword }; +} diff --git a/apps/react-admin/src/hooks/abp/use-Job-enums-map.ts b/apps/react-admin/src/hooks/abp/use-Job-enums-map.ts new file mode 100644 index 000000000..96484d4d4 --- /dev/null +++ b/apps/react-admin/src/hooks/abp/use-Job-enums-map.ts @@ -0,0 +1,63 @@ +import { useTranslation } from "react-i18next"; +import { JobPriority, JobSource, JobStatus, JobType } from "#/tasks/job-infos"; + +export function useJobEnumsMap() { + const { t: $t } = useTranslation(); + + const jobStatusMap: Record = { + [JobStatus.Completed]: $t("TaskManagement.DisplayName:Completed"), + [JobStatus.FailedRetry]: $t("TaskManagement.DisplayName:FailedRetry"), + [JobStatus.None]: $t("TaskManagement.DisplayName:None"), + [JobStatus.Paused]: $t("TaskManagement.DisplayName:Paused"), + [JobStatus.Queuing]: $t("TaskManagement.DisplayName:Queuing"), + [JobStatus.Running]: $t("TaskManagement.DisplayName:Running"), + [JobStatus.Stopped]: $t("TaskManagement.DisplayName:Stopped"), + }; + + const jobStatusColor: Record = { + [JobStatus.Completed]: "#339933", + [JobStatus.FailedRetry]: "#FF6600", + [JobStatus.None]: "", + [JobStatus.Paused]: "#CC6633", + [JobStatus.Queuing]: "#008B8B", + [JobStatus.Running]: "#3399CC", + [JobStatus.Stopped]: "#F00000", + }; + + const jobTypeMap: Record = { + [JobType.Once]: $t("TaskManagement.DisplayName:Once"), + [JobType.Period]: $t("TaskManagement.DisplayName:Period"), + [JobType.Persistent]: $t("TaskManagement.DisplayName:Persistent"), + }; + + const jobPriorityMap: Record = { + [JobPriority.AboveNormal]: $t("TaskManagement.DisplayName:AboveNormal"), + [JobPriority.BelowNormal]: $t("TaskManagement.DisplayName:BelowNormal"), + [JobPriority.High]: $t("TaskManagement.DisplayName:High"), + [JobPriority.Low]: $t("TaskManagement.DisplayName:Low"), + [JobPriority.Normal]: $t("TaskManagement.DisplayName:Normal"), + }; + + const jobPriorityColor: Record = { + [JobPriority.AboveNormal]: "orange", + [JobPriority.BelowNormal]: "cyan", + [JobPriority.High]: "red", + [JobPriority.Low]: "purple", + [JobPriority.Normal]: "blue", + }; + + const jobSourceMap: Record = { + [JobSource.None]: $t("TaskManagement.DisplayName:None"), + [JobSource.System]: $t("TaskManagement.DisplayName:System"), + [JobSource.User]: $t("TaskManagement.DisplayName:User"), + }; + + return { + jobPriorityColor, + jobPriorityMap, + jobSourceMap, + jobStatusColor, + jobStatusMap, + jobTypeMap, + }; +} diff --git a/apps/react-admin/src/hooks/abp/use-localization.ts b/apps/react-admin/src/hooks/abp/use-localization.ts index fa78f95a0..74fe658de 100644 --- a/apps/react-admin/src/hooks/abp/use-localization.ts +++ b/apps/react-admin/src/hooks/abp/use-localization.ts @@ -1,5 +1,4 @@ import { useEffect, useMemo } from "react"; -import type { Dictionary } from "#/abp-core/global"; import { format } from "@/utils/string"; import useLocaleStore, { useLocale } from "@/store/localeI18nStore"; import { getResources } from "@/utils/abp/localzations/get-resources"; diff --git a/apps/react-admin/src/hooks/abp/use-validation.ts b/apps/react-admin/src/hooks/abp/use-validation.ts new file mode 100644 index 000000000..ed5f4bbe3 --- /dev/null +++ b/apps/react-admin/src/hooks/abp/use-validation.ts @@ -0,0 +1,346 @@ +import type { RuleCreator } from "#/abp-core"; +import type { + Field, + FieldBeetWeen, + FieldContains, + FieldDefineValidator, + FieldLength, + FieldMatch, + FieldRange, + FieldRegular, + FieldValidator, + Rule, + RuleType, +} from "#/abp-core"; +import { ValidationEnum } from "@/constants/abp-core"; + +import { useLocalizer } from "./use-localization"; +import { isEmail, isPhone } from "@/utils/abp/regex"; + +export function useValidation(): RuleCreator { + const { L } = useLocalizer(["AbpValidation"]); + + function _getFieldName(field: Field) { + return __getFieldName(field.name ?? "", field.resourceName, field.prefix, field.connector); + } + + function __getFieldName(fieldName: string, resourceName?: string, prefix?: string, connector?: string) { + if (fieldName && resourceName) { + const finalFieldName = prefix ? `${prefix}${connector ?? ":"}${fieldName}` : fieldName; + const { L: l } = useLocalizer(resourceName); + return l(finalFieldName); + } + return fieldName; + } + + function _createRule(options: { + len?: number; + max?: number; + message?: string; + min?: number; + required?: boolean; + trigger?: "blur" | "change" | ["change", "blur"]; + type?: "array" | RuleType; + validator?: (rule: any, value: any, callback: any, source?: any, options?: any) => Promise | void; + }): Rule[] { + return [ + { + len: options.len, + max: options.max, + message: options.message, + min: options.min, + required: options.required, + trigger: options.trigger, + type: options.type, + validator: options.validator, + }, + ]; + } + + function _createValidator(field: Field, useNameEnum: string, notNameEnum: string, required?: boolean): Rule { + const message = field.name ? L(useNameEnum, [_getFieldName(field)]) : L(notNameEnum); + return { + message, + required, + trigger: field.trigger, + type: field.type, + }; + } + + function _createLengthValidator( + field: FieldLength, + checkMaximum: boolean, + useNameEnum: string, + notNameEnum: string, + required?: boolean, + ): Rule { + const message = field.name ? L(useNameEnum, [_getFieldName(field), field.length]) : L(notNameEnum, [field.length]); + + function checkLength(value: any[] | string) { + return checkMaximum ? field.length > value.length : value.length > field.length; + } + + return { + message, + required, + trigger: field.trigger, + type: field.type, + validator: (_: any, value: string) => { + if (!checkLength(value)) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }; + } + + function _createLengthRangValidator( + field: FieldRange, + useNameEnum: string, + notNameEnum: string, + required?: boolean, + ): Rule { + const message = field.name + ? L(useNameEnum, [_getFieldName(field), field.maximum, field.minimum]) + : L(notNameEnum, [field.minimum, field.maximum]); + return { + message, + required, + trigger: field.trigger, + type: field.type, + validator: (_: any, value: string) => { + if (value.length < field.minimum || value.length > field.maximum) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }; + } + + function _createBeetWeenValidator(field: FieldBeetWeen): Rule { + const message = field.name + ? L(ValidationEnum.FieldMustBeetWeen, [_getFieldName(field), field.start, field.end]) + : L(ValidationEnum.ThisFieldMustBeBetween, [field.start, field.end]); + return { + message, + trigger: field.trigger, + validator: (_: any, value: number) => { + // beetween不在进行必输检查, 改为数字有效性检查 + if (Number.isNaN(value)) { + return Promise.reject(message); + } + return value < field.start || value > field.end ? Promise.reject(message) : Promise.resolve(); + }, + }; + } + + function _createRegularExpressionValidator(field: FieldRegular, required?: boolean): Rule { + const message = field.name + ? L(ValidationEnum.FieldMustMatchRegularExpression, [_getFieldName(field), field.expression]) + : L(ValidationEnum.ThisFieldMustMatchTheRegularExpression, [field.expression]); + return { + message, + pattern: new RegExp(field.expression), + required, + trigger: field.trigger, + type: field.type, + }; + } + + function _createEmailValidator(field: Field, required?: boolean): Rule { + const message = field.name + ? L(ValidationEnum.FieldDoNotValidEmailAddress, [_getFieldName(field)]) + : L(ValidationEnum.ThisFieldIsNotAValidEmailAddress); + return { + message, + required, + trigger: field.trigger, + type: field.type, + validator: (_: any, value: string) => { + if (!isEmail(value)) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }; + } + + function _createPhoneValidator(field: Field, required?: boolean): Rule { + const message = field.name + ? L(ValidationEnum.FieldDoNotValidPhoneNumber, [_getFieldName(field)]) + : L(ValidationEnum.ThisFieldIsNotAValidPhoneNumber); + return { + message, + required, + trigger: field.trigger, + type: field.type, + validator: (_: any, value: string) => { + if (!isPhone(value)) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }; + } + + const ruleCreator: RuleCreator = { + defineValidator(field: FieldDefineValidator) { + return _createRule(field); + }, + doNotMatch(field: FieldMatch) { + const message = L(ValidationEnum.DoNotMatch, [ + __getFieldName(field.name, field.resourceName, field.prefix), + __getFieldName(field.matchField, field.resourceName, field.prefix), + ]); + return _createRule({ + message, + required: field.required, + trigger: field.trigger, + type: field.type, + validator: (_, value: string) => { + if (value !== field.matchValue) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }); + }, + fieldDoNotValidCreditCardNumber(field: Field) { + if (field.name) { + return _createRule({ + message: L(ValidationEnum.FieldDoNotValidCreditCardNumber, [_getFieldName(field)]), + trigger: field.trigger, + type: field.type, + }); + } + return _createRule({ + message: L(ValidationEnum.ThisFieldIsNotAValidCreditCardNumber), + trigger: field.trigger, + type: field.type, + }); + }, + fieldDoNotValidEmailAddress(field: Field) { + return [_createEmailValidator(field)]; + }, + fieldDoNotValidFullyQualifiedUrl(field: Field) { + if (field.name) { + return _createRule({ + message: L(ValidationEnum.FieldDoNotValidFullyQualifiedUrl, [_getFieldName(field)]), + trigger: field.trigger, + type: field.type, + }); + } + return _createRule({ + message: L(ValidationEnum.ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl), + trigger: field.trigger, + type: field.type, + }); + }, + fieldDoNotValidPhoneNumber(field: Field) { + return [_createPhoneValidator(field)]; + }, + fieldInvalid(field: FieldValidator) { + const message = field.name + ? L(ValidationEnum.FieldInvalid, [_getFieldName(field)]) + : L(ValidationEnum.ThisFieldIsInvalid); + return _createRule({ + message, + required: field.required, + trigger: field.trigger, + type: field.type, + validator: (_, value: any) => { + if (!field.validator(value)) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }); + }, + fieldIsNotValid(field: FieldValidator) { + const message = field.name + ? L(ValidationEnum.FieldIsNotValid, [_getFieldName(field)]) + : L(ValidationEnum.ThisFieldIsNotValid); + return _createRule({ + message, + required: field.required, + trigger: field.trigger, + type: field.type, + validator: (_, value: any) => { + if (field.validator(value)) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }); + }, + fieldMustBeetWeen(field: FieldBeetWeen) { + return [_createBeetWeenValidator(field)]; + }, + fieldMustBeStringOrArrayWithMaximumLength(field: FieldLength) { + return [ + _createLengthValidator( + field, + true, + ValidationEnum.FieldMustBeStringOrArrayWithMaximumLength, + ValidationEnum.ThisFieldMustBeAStringOrArrayTypeWithAMaximumLength, + ), + ]; + }, + fieldMustBeStringOrArrayWithMinimumLength(field: FieldLength) { + return [ + _createLengthValidator( + field, + false, + ValidationEnum.FieldMustBeStringOrArrayWithMinimumLength, + ValidationEnum.ThisFieldMustBeAStringOrArrayTypeWithAMinimumLength, + ), + ]; + }, + fieldMustBeStringWithMaximumLength(field: FieldLength) { + return [ + _createLengthValidator( + field, + true, + ValidationEnum.FieldMustBeStringWithMaximumLength, + ValidationEnum.ThisFieldMustBeAStringWithAMaximumLength, + ), + ]; + }, + fieldMustBeStringWithMinimumLengthAndMaximumLength(field: FieldRange) { + return [ + _createLengthRangValidator( + field, + ValidationEnum.FieldMustBeStringWithMinimumLengthAndMaximumLength, + ValidationEnum.ThisFieldMustBeAStringWithAMinimumLengthAndAMaximumLength, + ), + ]; + }, + fieldMustMatchRegularExpression(field: FieldRegular) { + return [_createRegularExpressionValidator(field)]; + }, + fieldOnlyAcceptsFilesExtensions(field: FieldContains) { + const message = field.name + ? L(ValidationEnum.FieldOnlyAcceptsFilesExtensions, [_getFieldName(field), field.value]) + : L(ValidationEnum.ThisFieldMustMatchTheRegularExpression, [field.value]); + return _createRule({ + message, + trigger: field.trigger, + type: field.type, + validator: (_, value: string) => { + if (!field.value.includes(value)) { + return Promise.reject(message); + } + return Promise.resolve(); + }, + }); + }, + fieldRequired(field: Field) { + return [_createValidator(field, ValidationEnum.FieldRequired, ValidationEnum.ThisFieldIsRequired, true)]; + }, + mapEnumValidMessage(enumName: string, args?: any[] | Record | undefined) { + return L(enumName, args); + }, + }; + + return ruleCreator; +} diff --git a/apps/react-admin/src/locales/lang/en_US/abp.json b/apps/react-admin/src/locales/lang/en_US/abp.json index 9a77b967a..6132394d8 100644 --- a/apps/react-admin/src/locales/lang/en_US/abp.json +++ b/apps/react-admin/src/locales/lang/en_US/abp.json @@ -9,6 +9,18 @@ "phoneNumber": "Phone Number", "getCode": "Get Code", "code": "Code" + }, + "qrcodeLogin": { + "scaned": "Please confirm login on your phone." + }, + "errors": { + "accountLockedByInvalidLoginAttempts": "The user account has been locked out due to invalid login attempts. Please wait a while and try again.", + "accountInactive": "You are not allowed to login! Your account is inactive.", + "invalidUserNameOrPassword": "Invalid username or password!", + "tokenHasExpired": "The token is no longer valid!", + "requiresTwoFactor": "Identity verification is required. Please select a verification method!", + "shouldChangePassword": "Your password has expired. Please change it and login!", + "accessDenied": "You have refused the necessary authorization for the application. Please log in again!" } }, "manage": { @@ -20,7 +32,6 @@ "claimTypes": "Claim Types", "securityLogs": "Security Logs", "organizationUnits": "Organization Units", - "auditLogs": "Audit Logs", "sessions": "Sessions" }, "permissions": { @@ -28,6 +39,11 @@ "groups": "Groups", "definitions": "Definitions" }, + "features": { + "title": "Features", + "groups": "Groups", + "definitions": "Definitions" + }, "settings": { "title": "Settings", "definitions": "Definitions", @@ -35,8 +51,24 @@ }, "notifications": { "title": "Notifications", - "myNotifilers": "My Notifilers" - } + "myNotifilers": "My Notifilers", + "groups": "Groups", + "definitions": "Definitions" + }, + "localization": { + "title": "Localization", + "resources": "Resources", + "languages": "Languages", + "texts": "Texts" + }, + "dataProtection": { + "title": "Data Protection", + "entityTypeInfos": "Entity Type Infos" + }, + "auditLogs": "Audit Logs", + "loggings": "System Logs", + "openApi": "Api Document", + "cache": "Cache Management" }, "openiddict": { "title": "OpenIddict", @@ -66,7 +98,8 @@ "noticeSettings": "Notice Settings", "authenticatorSettings": "Authenticator Settings", "changeAvatar": "Change Avatar", - "sessionSettings": "Session Settings" + "sessionSettings": "Session Settings", + "personalDataSettings": "Personal Data Settings" }, "profile": "My Profile" }, @@ -76,7 +109,45 @@ "title": "Message Manage", "email": "Email Messages", "sms": "Sms Messages" + }, + "dataDictionaries": "Data Dictionaries", + "layouts": "Layouts", + "menus": "Menus" + }, + "saas": { + "title": "Saas", + "editions": "Editions", + "tenants": "Tenants" + }, + "demo": { + "title": "Demo", + "books": "Books" + }, + "tasks": { + "title": "Task Management", + "jobInfo": { + "title": "Job Manage" } + }, + "webhooks": { + "title": "Webhooks", + "groups": "Groups", + "definitions": "Definitions", + "subscriptions": "Subscriptions", + "sendAttempts": "Send Attempts" + }, + "textTemplating": { + "title": "Text Templating", + "definitions": "Definitions" + }, + "oss": { + "title": "Object storage", + "containers": "Containers", + "objects": "Files" + }, + "wechat": { + "title": "WeChat", + "settings": "Settings" } } } diff --git a/apps/react-admin/src/locales/lang/en_US/authentication.json b/apps/react-admin/src/locales/lang/en_US/authentication.json new file mode 100644 index 000000000..f00646b0d --- /dev/null +++ b/apps/react-admin/src/locales/lang/en_US/authentication.json @@ -0,0 +1,58 @@ +{ + "authentication": { + "welcomeBack": "Welcome Back", + "pageTitle": "Plug-and-play Admin system", + "pageDesc": "Efficient, versatile frontend template", + "loginSuccess": "Login Successful", + "loginSuccessDesc": "Welcome Back", + "loginSubtitle": "Enter your account details to manage your projects", + "selectAccount": "Quick Select Account", + "username": "Username", + "password": "Password", + "usernameTip": "Please enter username", + "passwordErrorTip": "Password is incorrect", + "passwordTip": "Please enter password", + "verifyRequiredTip": "Please complete the verification first", + "rememberMe": "Remember Me", + "createAnAccount": "Create an Account", + "createAccount": "Create Account", + "alreadyHaveAccount": "Already have an account?", + "accountTip": "Don't have an account?", + "signUp": "Sign Up", + "signUpSubtitle": "Make managing your applications simple and fun", + "confirmPassword": "Confirm Password", + "confirmPasswordTip": "The passwords do not match", + "agree": "I agree to", + "privacyPolicy": "Privacy-policy", + "terms": "Terms", + "agreeTip": "Please agree to the Privacy Policy and Terms", + "goToLogin": "Login instead", + "passwordStrength": "Use 8 or more characters with a mix of letters, numbers & symbols", + "forgetPassword": "Forget Password?", + "forgetPasswordSubtitle": "Enter your email and we'll send you instructions to reset your password", + "emailTip": "Please enter email", + "emailValidErrorTip": "The email format you entered is incorrect", + "sendResetLink": "Send Reset Link", + "email": "Email", + "qrcodeSubtitle": "Scan the QR code with your phone to login", + "qrcodePrompt": "Click 'Confirm' after scanning to complete login", + "qrcodeLogin": "QR Code Login", + "codeSubtitle": "Enter your phone number to start managing your project", + "code": "Security code", + "codeTip": "Security code required {0} characters", + "mobile": "Mobile", + "mobileLogin": "Mobile Login", + "mobileTip": "Please enter mobile number", + "mobileErrortip": "The phone number format is incorrect", + "sendCode": "Get Security code", + "sendText": "Resend in {0}s", + "thirdPartyLogin": "Or continue with", + "loginAgainTitle": "Please Log In Again", + "loginAgainSubTitle": "Your login session has expired. Please log in again to continue.", + "layout": { + "center": "Align Center", + "alignLeft": "Align Left", + "alignRight": "Align Right" + } + } +} diff --git a/apps/react-admin/src/locales/lang/en_US/component.json b/apps/react-admin/src/locales/lang/en_US/component.json index a6b60002d..c0b523ccc 100644 --- a/apps/react-admin/src/locales/lang/en_US/component.json +++ b/apps/react-admin/src/locales/lang/en_US/component.json @@ -117,6 +117,9 @@ "requiresAllDesc": "If checked, you need to have all the selected permissions.", "permissions": "Required Permissions" } + }, + "table": { + "selectedItemWellBeDeleted": "Multiple items selected will be deleted!" } } } diff --git a/apps/react-admin/src/locales/lang/en_US/index.ts b/apps/react-admin/src/locales/lang/en_US/index.ts index d4e54820d..97ef70015 100644 --- a/apps/react-admin/src/locales/lang/en_US/index.ts +++ b/apps/react-admin/src/locales/lang/en_US/index.ts @@ -3,6 +3,8 @@ import sys from "./sys.json"; import ui from "./ui.json"; import abp from "./abp.json"; import component from "./component.json"; +import workbench from "./workbench.json"; +import authentication from "./authentication.json"; export default { ...common, @@ -10,4 +12,6 @@ export default { ...ui, ...abp, ...component, + ...workbench, + ...authentication, }; diff --git a/apps/react-admin/src/locales/lang/en_US/workbench.json b/apps/react-admin/src/locales/lang/en_US/workbench.json new file mode 100644 index 000000000..a58bd5069 --- /dev/null +++ b/apps/react-admin/src/locales/lang/en_US/workbench.json @@ -0,0 +1,39 @@ +{ + "workbench": { + "header": { + "welcome": { + "atoon": "Good afternoon, {0}, pay attention to rest oh~", + "afternoon": "Good afternoon, {0}, relax in time, can improve work efficiency~", + "evening": "Good evening, {0}. Still at work? The off work~", + "morning": "Good morning, {0}. Begin your day~" + }, + "notifier": { + "title": "Notifier", + "count": "({0})" + } + }, + "content": { + "favoriteMenu": { + "title": "Favorite Menus", + "home": "Home", + "dashboard": "Dashboard", + "profile": "Personal Profile", + "settings": "Personal Settings", + "notifiers": "Notifiers", + "manage": "Manage menu", + "create": "New menu", + "delete": "Delete Menu", + "select": "Select Menu", + "color": "Select Color", + "alias": "Alias Name", + "icon": "Icon" + }, + "trends": { + "title": "Latest News" + }, + "todo": { + "title": "Todo List" + } + } + } +} diff --git a/apps/react-admin/src/locales/lang/zh_CN/abp.json b/apps/react-admin/src/locales/lang/zh_CN/abp.json index b19371222..269a2102e 100644 --- a/apps/react-admin/src/locales/lang/zh_CN/abp.json +++ b/apps/react-admin/src/locales/lang/zh_CN/abp.json @@ -9,6 +9,18 @@ "phoneNumber": "手机号码", "getCode": "获取验证码", "code": "验证码" + }, + "qrcodeLogin": { + "scaned": "请在手机上确认登录." + }, + "errors": { + "accountLockedByInvalidLoginAttempts": "由于尝试登录无效,用户帐户被锁定.请稍候再试!", + "accountInactive": "您不能登录,您的帐户是无效的!", + "invalidUserNameOrPassword": "用户名或密码错误!", + "tokenHasExpired": "您的请求会话已过期,请重新登录!", + "requiresTwoFactor": "需要验证身份,请选择一种验证方式!", + "shouldChangePassword": "您的密码已过期,请修改密码后登录!", + "accessDenied": "您拒绝了应用程序必须的授权, 请重新登录!" } }, "manage": { @@ -20,7 +32,6 @@ "claimTypes": "身份标识", "securityLogs": "安全日志", "organizationUnits": "组织机构", - "auditLogs": "审计日志", "sessions": "会话管理" }, "permissions": { @@ -28,6 +39,11 @@ "groups": "权限分组", "definitions": "权限定义" }, + "features": { + "title": "功能管理", + "groups": "功能分组", + "definitions": "功能定义" + }, "settings": { "title": "设置管理", "definitions": "设置定义", @@ -35,8 +51,24 @@ }, "notifications": { "title": "通知管理", - "myNotifilers": "我的通知" - } + "myNotifilers": "我的通知", + "groups": "通知分组", + "definitions": "通知定义" + }, + "localization": { + "title": "本地化管理", + "resources": "资源管理", + "languages": "语言管理", + "texts": "文档管理" + }, + "dataProtection": { + "title": "数据权限", + "entityTypeInfos": "实体列表" + }, + "auditLogs": "审计日志", + "loggings": "系统日志", + "openApi": "接口文档", + "cache": "缓存管理" }, "openiddict": { "title": "OpenIddict", @@ -66,7 +98,8 @@ "noticeSettings": "新消息通知", "authenticatorSettings": "身份验证程序", "changeAvatar": "更改头像", - "sessionSettings": "会话管理" + "sessionSettings": "会话管理", + "personalDataSettings": "个人信息管理" }, "profile": "个人中心" }, @@ -76,7 +109,45 @@ "title": "消息管理", "email": "邮件消息", "sms": "短信消息" + }, + "dataDictionaries": "数据字典", + "layouts": "布局管理", + "menus": "菜单管理" + }, + "saas": { + "title": "Saas", + "editions": "版本管理", + "tenants": "租户管理" + }, + "demo": { + "title": "演示", + "books": "书籍列表" + }, + "tasks": { + "title": "后台作业", + "jobInfo": { + "title": "作业管理" } + }, + "webhooks": { + "title": "Webhook管理", + "groups": "Webhook分组", + "definitions": "Webhook定义", + "subscriptions": "管理订阅", + "sendAttempts": "发送记录" + }, + "textTemplating": { + "title": "文本模板", + "definitions": "模板定义" + }, + "oss": { + "title": "对象存储", + "containers": "容器管理", + "objects": "文件管理" + }, + "wechat": { + "title": "微信集成", + "settings": "微信设置" } } } diff --git a/apps/react-admin/src/locales/lang/zh_CN/authentication.json b/apps/react-admin/src/locales/lang/zh_CN/authentication.json new file mode 100644 index 000000000..8149626aa --- /dev/null +++ b/apps/react-admin/src/locales/lang/zh_CN/authentication.json @@ -0,0 +1,58 @@ +{ + "authentication": { + "welcomeBack": "欢迎回来", + "pageTitle": "开箱即用的大型中后台管理系统", + "pageDesc": "工程化、高性能、跨组件库的前端模版", + "loginSuccess": "登录成功", + "loginSuccessDesc": "欢迎回来", + "loginSubtitle": "请输入您的帐户信息以开始管理您的项目", + "selectAccount": "快速选择账号", + "username": "账号", + "password": "密码", + "usernameTip": "请输入用户名", + "passwordTip": "请输入密码", + "verifyRequiredTip": "请先完成验证", + "passwordErrorTip": "密码错误", + "rememberMe": "记住账号", + "createAnAccount": "创建一个账号", + "createAccount": "创建账号", + "alreadyHaveAccount": "已经有账号了?", + "accountTip": "还没有账号?", + "signUp": "注册", + "signUpSubtitle": "让您的应用程序管理变得简单而有趣", + "confirmPassword": "确认密码", + "confirmPasswordTip": "两次输入的密码不一致", + "agree": "我同意", + "privacyPolicy": "隐私政策", + "terms": "条款", + "agreeTip": "请同意隐私政策和条款", + "goToLogin": "去登录", + "passwordStrength": "使用 8 个或更多字符,混合字母、数字和符号", + "forgetPassword": "忘记密码?", + "forgetPasswordSubtitle": "输入您的电子邮件,我们将向您发送重置密码的连接", + "emailTip": "请输入邮箱", + "emailValidErrorTip": "你输入的邮箱格式不正确", + "sendResetLink": "发送重置链接", + "email": "邮箱", + "qrcodeSubtitle": "请用手机扫描二维码登录", + "qrcodePrompt": "扫码后点击 '确认',即可完成登录", + "qrcodeLogin": "扫码登录", + "codeSubtitle": "请输入您的手机号码以开始管理您的项目", + "code": "验证码", + "codeTip": "请输入{0}位验证码", + "mobile": "手机号码", + "mobileTip": "请输入手机号", + "mobileErrortip": "手机号码格式错误", + "mobileLogin": "手机号登录", + "sendCode": "获取验证码", + "sendText": "{0}秒后重新获取", + "thirdPartyLogin": "其他登录方式", + "loginAgainTitle": "重新登录", + "loginAgainSubTitle": "您的登录状态已过期,请重新登录以继续。", + "layout": { + "center": "居中", + "alignLeft": "居左", + "alignRight": "居右" + } + } +} diff --git a/apps/react-admin/src/locales/lang/zh_CN/component.json b/apps/react-admin/src/locales/lang/zh_CN/component.json index 856042a26..c9b513a60 100644 --- a/apps/react-admin/src/locales/lang/zh_CN/component.json +++ b/apps/react-admin/src/locales/lang/zh_CN/component.json @@ -117,6 +117,9 @@ "requiresAllDesc": "如果勾选,则需要拥有所有选择的权限.", "permissions": "需要的权限" } + }, + "table": { + "selectedItemWellBeDeleted": "选择的多个项目将被删除!" } } } diff --git a/apps/react-admin/src/locales/lang/zh_CN/index.ts b/apps/react-admin/src/locales/lang/zh_CN/index.ts index d4e54820d..97ef70015 100644 --- a/apps/react-admin/src/locales/lang/zh_CN/index.ts +++ b/apps/react-admin/src/locales/lang/zh_CN/index.ts @@ -3,6 +3,8 @@ import sys from "./sys.json"; import ui from "./ui.json"; import abp from "./abp.json"; import component from "./component.json"; +import workbench from "./workbench.json"; +import authentication from "./authentication.json"; export default { ...common, @@ -10,4 +12,6 @@ export default { ...ui, ...abp, ...component, + ...workbench, + ...authentication, }; diff --git a/apps/react-admin/src/locales/lang/zh_CN/workbench.json b/apps/react-admin/src/locales/lang/zh_CN/workbench.json new file mode 100644 index 000000000..71e5703e6 --- /dev/null +++ b/apps/react-admin/src/locales/lang/zh_CN/workbench.json @@ -0,0 +1,39 @@ +{ + "workbench": { + "header": { + "welcome": { + "atoon": "中午好, {0}, 注意休息哦~", + "afternoon": "下午好, {0}, 适时放松,可以提高工作效率~", + "evening": "晚上好, {0}, 还在工作么?该下班了~", + "morning": "早安, {0}, 开始您一天的工作吧~" + }, + "notifier": { + "title": "通知", + "count": "({0})" + } + }, + "content": { + "favoriteMenu": { + "title": "常用", + "home": "首页", + "dashboard": "仪表盘", + "profile": "个人中心", + "settings": "个人设置", + "notifiers": "通知消息", + "manage": "管理菜单", + "create": "添加菜单", + "delete": "删除菜单", + "select": "选择菜单", + "color": "选择颜色", + "alias": "自定义别名", + "icon": "自定义图标" + }, + "trends": { + "title": "最新消息" + }, + "todo": { + "title": "待办事项" + } + } + } +} diff --git a/apps/react-admin/src/main.tsx b/apps/react-admin/src/main.tsx index cf9b2d7e0..4ace594e2 100644 --- a/apps/react-admin/src/main.tsx +++ b/apps/react-admin/src/main.tsx @@ -11,7 +11,6 @@ import { HelmetProvider } from "react-helmet-async"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import "virtual:svg-icons-register"; // mock api -import worker from "./_mock"; // i18n import "./locales/i18n"; // css diff --git a/apps/react-admin/src/pages/account/my-setting.tsx b/apps/react-admin/src/pages/account/my-setting.tsx index 6242e099a..21f25b0ad 100644 --- a/apps/react-admin/src/pages/account/my-setting.tsx +++ b/apps/react-admin/src/pages/account/my-setting.tsx @@ -1,11 +1,13 @@ -import { useState, useEffect, lazy } from "react"; -import { Card, Menu, Modal } from "antd"; +import type React from "react"; +import { useState, useEffect, lazy, Suspense } from "react"; +import { Card, Menu, Modal, Spin } from "antd"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { toast } from "sonner"; +import { getApi, updateApi } from "@/api/account/profile"; import type { UpdateProfileDto } from "#/account/profile"; import { useUserActions, useUserInfo } from "@/store/userStore"; -import { getApi, updateApi } from "@/api/account/profile"; -import { toast } from "sonner"; import { useSearchParams } from "react-router"; // Lazy load components @@ -15,9 +17,12 @@ const BindSettings = lazy(() => import("@/components/abp/account/bind-settings") const SecuritySettings = lazy(() => import("@/components/abp/account/security-settings")); const SessionSettings = lazy(() => import("@/components/abp/account/session-settings")); const NoticeSettings = lazy(() => import("@/components/abp/account/notice-settings")); +// const PersonalDataSettings = lazy(() => import("@/components/abp/account/personal-data-settings")); const EmailConfirmModal = lazy(() => import("@/components/abp/account/email-confirm-modal")); +const ChangePasswordModal = lazy(() => import("@/components/abp/account/change-password-modal")); +const ChangePhoneNumberModal = lazy(() => import("@/components/abp/account/change-phone-number-modal")); -const MySetting = () => { +const MySetting: React.FC = () => { const { t: $t } = useTranslation(); const [searchParams] = useSearchParams(); const [modal, contextHolder] = Modal.useModal(); @@ -26,7 +31,11 @@ const MySetting = () => { const queryClient = useQueryClient(); const [selectedKey, setSelectedKey] = useState("basic"); - const [emailConfirmModalVisible, setEmailConfirmModalVisible] = useState(false); + + // Modal Visibility States + const [emailConfirmVisible, setEmailConfirmVisible] = useState(false); + const [passwordModalVisible, setPasswordModalVisible] = useState(false); + const [phoneModalVisible, setPhoneModalVisible] = useState(false); const menuItems = [ { key: "basic", label: $t("abp.account.settings.basic.title") }, @@ -35,6 +44,7 @@ const MySetting = () => { { key: "session", label: $t("abp.account.settings.sessionSettings") }, { key: "notice", label: $t("abp.account.settings.noticeSettings") }, { key: "authenticator", label: $t("abp.account.settings.authenticatorSettings") }, + // { key: "personal-data", label: $t("abp.account.settings.personalDataSettings") }, ]; // Fetch profile data @@ -48,7 +58,7 @@ const MySetting = () => { mutationFn: updateApi, onSuccess: async () => { toast.success($t("AbpAccount.PersonalSettingsChangedConfirmationModalTitle")); - queryClient.invalidateQueries({ queryKey: ["profile"] }); //refresh user info + queryClient.invalidateQueries({ queryKey: ["profile"] }); await fetchAndSetUser(); }, }); @@ -56,7 +66,7 @@ const MySetting = () => { useEffect(() => { const confirmToken = searchParams.get("confirmToken"); if (confirmToken && profile) { - setEmailConfirmModalVisible(true); + setEmailConfirmVisible(true); } }, [searchParams, profile]); @@ -70,13 +80,16 @@ const MySetting = () => { }; const handleChangePassword = () => { - // TODO: Implement password change - console.warn("onChangePassword not implemented yet!"); + setPasswordModalVisible(true); }; const handleChangePhoneNumber = () => { - // TODO: Implement phone number change - console.warn("onChangePhoneNumber not implemented yet!"); + setPhoneModalVisible(true); + }; + + const onPhoneNumberChanged = async (phoneNumber: string) => { + // Optimistically update store or refetch user info + await fetchAndSetUser(); }; const renderContent = () => { @@ -112,6 +125,7 @@ const MySetting = () => { return ; case "session": return ; + // case "personal-data": return ; default: return null; } @@ -121,32 +135,53 @@ const MySetting = () => {
{contextHolder} -
-
+
+
setSelectedKey(key)} />
-
{renderContent()}
+
+ + +
+ } + > + {renderContent()} + +
- setEmailConfirmModalVisible(false)} - onSuccess={async () => { - await fetchAndSetUser(); - }} - /> + + setEmailConfirmVisible(false)} + onSuccess={async () => { + await fetchAndSetUser(); + }} + /> + + setPasswordModalVisible(false)} /> + + setPhoneModalVisible(false)} + onChange={onPhoneNumberChanged} + /> +
); }; diff --git a/apps/react-admin/src/pages/management/features/definitions-features/feature-definition-modal.tsx b/apps/react-admin/src/pages/management/features/definitions-features/feature-definition-modal.tsx new file mode 100644 index 000000000..170303e38 --- /dev/null +++ b/apps/react-admin/src/pages/management/features/definitions-features/feature-definition-modal.tsx @@ -0,0 +1,454 @@ +import { useEffect, useRef, useState } from "react"; +import { Form, Input, Modal, Tabs, Select, TreeSelect, InputNumber, Checkbox } from "antd"; +import { useTranslation } from "react-i18next"; +import type { FeatureDefinitionDto } from "#/management/features/definitions"; +import type { FeatureGroupDefinitionDto } from "#/management/features/groups"; +import type { PropertyInfo } from "@/components/abp/properties/types"; +import { + createApi, + getApi, + getListApi as getDefinitionsApi, + updateApi, +} from "@/api/management/features/feature-definitions"; +import { getListApi as getGroupsApi } from "@/api/management/features/feature-group-definitions"; +import LocalizableInput from "@/components/abp/localizable-input/localizable-input"; +import PropertyTable from "@/components/abp/properties/property-table"; +import ValueTypeInput, { type ValueTypeInputHandle } from "@/components/abp/string-value-type/string-value-type-input"; +import { listToTree } from "@/utils/tree"; +import { toast } from "sonner"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { localizationSerializer } from "@/utils/abp/localization-serializer"; +import { useLocalizer } from "@/hooks/abp/use-localization"; +import { + valueTypeSerializer, + FreeTextStringValueType, + SelectionStringValueType, + ToggleStringValueType, +} from "@/components/abp/string-value-type"; +import { useValidation } from "@/hooks/abp/use-validation"; +import { ValidationEnum } from "@/constants/abp-core"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (data: FeatureDefinitionDto) => void; + featureName?: string; + groupName?: string; +} + +type TabKeys = "basic" | "valueType" | "props"; + +const defaultModel: FeatureDefinitionDto = { + allowedProviders: [], + displayName: "", + extraProperties: {}, + groupName: "", + isAvailableToHost: true, + isEnabled: true, + isStatic: false, + isVisibleToClients: false, + name: "", + valueType: new FreeTextStringValueType() as any, // Initial serialized or object +} as FeatureDefinitionDto; + +const FeatureDefinitionModal: React.FC = ({ visible, onClose, onChange, featureName, groupName }) => { + const { t: $t } = useTranslation(); + const { Lr } = useLocalizer(); + const { deserialize, validate: validateLocalizer } = localizationSerializer(); + const queryClient = useQueryClient(); + const [form] = Form.useForm(); + + // Refs & State + const valueTypeInputRef = useRef(null); + const [formModel, setFormModel] = useState({ ...defaultModel }); + const [isEditModel, setIsEditModel] = useState(false); + const [activeTab, setActiveTab] = useState("basic"); + + // Options + const [availableGroups, setAvailableGroups] = useState([]); + const [availableDefinitions, setAvailableDefinitions] = useState([]); + + // Dynamic Default Value Control logic + const [currentValueTypeName, setCurrentValueTypeName] = useState("FreeTextStringValueType"); + const [currentValidatorName, setCurrentValidatorName] = useState("NULL"); + const [selectionOptions, setSelectionOptions] = useState<{ label: string; value: string }[]>([]); + + // + const { mapEnumValidMessage } = useValidation(); + // for checkbox refreshing issue + const defaultValue = Form.useWatch("defaultValue", form); + + // --- API Mutations --- + + const { mutateAsync: fetchGroups } = useMutation({ + mutationFn: getGroupsApi, + onSuccess: (res) => { + const groups = res.items.map((group) => { + const localizableGroup = deserialize(group.displayName); + return { + ...group, + displayName: Lr(localizableGroup.resourceName, localizableGroup.name), + }; + }); + setAvailableGroups(groups); + }, + }); + + const { mutateAsync: fetchDefinitions } = useMutation({ + mutationFn: getDefinitionsApi, + onSuccess: (res) => { + const features = res.items.map((item) => { + const displayName = deserialize(item.displayName); + return { + ...item, + disabled: item.name === formModel.name, // Prevent selecting self as parent + title: Lr(displayName.resourceName, displayName.name), + value: item.name, + key: item.name, + }; + }); + setAvailableDefinitions(listToTree(features, { id: "name", pid: "parentName" })); + }, + }); + + const { mutateAsync: fetchFeature, isPending: isFetching } = useMutation({ + mutationFn: getApi, + onSuccess: (dto) => { + setFormModel(dto); + form.setFieldsValue(dto); + // Initialize dynamic states based on fetched data + handleValueTypeStateUpdate(dto.valueType); + + // Load definitions for the group + if (dto.groupName) { + fetchDefinitions({ groupName: dto.groupName }); + } + }, + }); + + const { mutateAsync: createFeature, isPending: isCreating } = useMutation({ + mutationFn: createApi, + onSuccess: (res) => { + toast.success($t("AbpUi.SavedSuccessfully")); + queryClient.invalidateQueries({ queryKey: ["featureDefinitions"] }); + onChange(res); + onClose(); + }, + }); + + const { mutateAsync: updateFeature, isPending: isUpdating } = useMutation({ + mutationFn: (data: FeatureDefinitionDto) => updateApi(data.name, data), + onSuccess: (res) => { + toast.success($t("AbpUi.SavedSuccessfully")); + queryClient.invalidateQueries({ queryKey: ["featureDefinitions"] }); + onChange(res); + onClose(); + }, + }); + + // --- Effects --- + + useEffect(() => { + if (visible) { + setActiveTab("basic"); + setFormModel({ ...defaultModel }); + form.resetFields(); + + // Reset dynamic states + setCurrentValueTypeName("FreeTextStringValueType"); + setCurrentValidatorName("NULL"); + setSelectionOptions([]); + setAvailableDefinitions([]); + + const init = async () => { + await fetchGroups({}); + if (featureName) { + setIsEditModel(true); + await fetchFeature(featureName); + } else { + setIsEditModel(false); + // Pre-select group if provided + if (groupName) { + form.setFieldValue("groupName", groupName); + setFormModel((prev) => ({ ...prev, groupName: groupName })); + await fetchDefinitions({ groupName }); + } + } + }; + init(); + } + }, [visible, featureName, groupName]); + + // --- Handlers --- + + const handleGroupChange = async (val: string) => { + setFormModel((prev) => ({ ...prev, groupName: val })); + if (val) { + await fetchDefinitions({ groupName: val }); + } else { + setAvailableDefinitions([]); + } + }; + + /** + * Helper to parse valueType string/obj and update local state + * to determine which input widget to show for DefaultValue + */ + const handleValueTypeStateUpdate = (val: string | any) => { + let vTypeObj: any; + if (typeof val === "string") { + try { + vTypeObj = valueTypeSerializer.deserialize(val); + } catch { + return; + } + } else { + vTypeObj = val; + } + + if (!vTypeObj) return; + + setCurrentValueTypeName(vTypeObj.name); + setCurrentValidatorName(vTypeObj.validator?.name || "NULL"); + + if (vTypeObj instanceof SelectionStringValueType) { + const options = vTypeObj.itemSource.items.map((item) => ({ + label: Lr(item.displayText.resourceName, item.displayText.name), + value: item.value, + })); + setSelectionOptions(options); + + // Clear default value if current isn't in options + const currentDef = form.getFieldValue("defaultValue"); + if (currentDef && !options.find((o) => o.value === currentDef)) { + form.setFieldValue("defaultValue", undefined); + } + } else if (vTypeObj instanceof ToggleStringValueType) { + // Ensure default value is boolean-string + const currentDef = form.getFieldValue("defaultValue"); + if (currentDef !== "true" && currentDef !== "false") { + form.setFieldValue("defaultValue", "false"); + } + } + }; + + const handleOk = async () => { + try { + const values = await form.validateFields(); + const submitData = { + ...formModel, + ...values, + }; + + // Ensure defaultValue is string + if (submitData.defaultValue !== undefined && typeof submitData.defaultValue !== "string") { + submitData.defaultValue = String(submitData.defaultValue); + } + + if (isEditModel) { + await updateFeature(submitData); + } else { + await createFeature(submitData); + } + } catch (error) { + console.error(error); + } + }; + + const handlePropChange = (prop: PropertyInfo) => { + setFormModel((prev) => ({ + ...prev, + extraProperties: { + ...prev.extraProperties, + [prop.key]: prop.value, + }, + })); + }; + + const handlePropDelete = (prop: PropertyInfo) => { + setFormModel((prev) => { + const newProps = { ...prev.extraProperties }; + delete newProps[prop.key]; + return { + ...prev, + extraProperties: newProps, + }; + }); + }; + + // Render the specific input for Default Value based on ValueType settings + const renderDefaultValueInput = () => { + if (currentValueTypeName === "SelectionStringValueType") { + return + + + {availableDefinitions.length > 0 && ( + + + + )} + + + + + + { + if (!validateLocalizer(value)) { + return Promise.reject( + mapEnumValidMessage(ValidationEnum.FieldRequired, [ + $t("AbpFeatureManagement.DisplayName:DisplayName"), + ]), + ); + } + return Promise.resolve(); + }, + }, + ]} + > + + + + + + + + {/* Dynamic Default Value Input */} + {/* Note: Logic for Toggle checkbox handles label inside, others outside */} + { + if (valueTypeInputRef.current) { + return valueTypeInputRef.current.validate(value); + } + return Promise.resolve(); + }, + }, + ]} + > + {renderDefaultValueInput()} + + + + {$t("AbpFeatureManagement.DisplayName:IsVisibleToClients")} + +
+ {$t("AbpFeatureManagement.Description:IsVisibleToClients")} +
+ + + {$t("AbpFeatureManagement.DisplayName:IsAvailableToHost")} + +
{$t("AbpFeatureManagement.Description:IsAvailableToHost")}
+ + + {/* Value Validator Type Configuration */} + + + { + // When internal configuration changes, update the local state for the UI + handleValueTypeStateUpdate(val); + }} + // Sync up local state changes immediately + onValueTypeChange={(type) => setCurrentValueTypeName(type)} + onValidatorChange={(val) => setCurrentValidatorName(val)} + onSelectionChange={(items) => { + setSelectionOptions( + items.map((i) => ({ + label: Lr(i.displayText.resourceName, i.displayText.name), + value: i.value, + })), + ); + }} + /> + + + + {/* Properties */} + + + + + + + ); +}; + +export default FeatureDefinitionModal; diff --git a/apps/react-admin/src/pages/management/features/definitions-features/feature-definition-table.tsx b/apps/react-admin/src/pages/management/features/definitions-features/feature-definition-table.tsx new file mode 100644 index 000000000..cba4c3863 --- /dev/null +++ b/apps/react-admin/src/pages/management/features/definitions-features/feature-definition-table.tsx @@ -0,0 +1,258 @@ +import { useCallback, useRef, useState } from "react"; +import { Button, Card, Table, Modal } from "antd"; +import { EditOutlined, DeleteOutlined, PlusOutlined, CheckOutlined, CloseOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import type { FeatureDefinitionDto } from "#/management/features/definitions"; +import type { FeatureGroupDefinitionDto } from "#/management/features/groups"; +import { type ActionType, ProTable, type ProColumns } from "@ant-design/pro-table"; +import { hasAccessByCodes } from "@/utils/abp/access-checker"; +import { localizationSerializer } from "@/utils/abp/localization-serializer"; +import { deleteApi, getListApi as getFeaturesApi } from "@/api/management/features/feature-definitions"; +import { getListApi as getGroupsApi } from "@/api/management/features/feature-group-definitions"; +import { GroupDefinitionsPermissions } from "@/constants/management/permissions"; +import FeatureDefinitionModal from "./feature-definition-modal"; +import { useLocalizer } from "@/hooks/abp/use-localization"; +import { toast } from "sonner"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { listToTree } from "@/utils/tree"; + +interface FeatureGroupVo extends FeatureGroupDefinitionDto { + features: FeatureDefinitionDto[]; +} + +const FeatureDefinitionTable: React.FC = () => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const queryClient = useQueryClient(); + const [modal, contextHolder] = Modal.useModal(); + const { deserialize } = localizationSerializer(); + + // Modal States + const [modalVisible, setModalVisible] = useState(false); + const [selectedFeature, setSelectedFeature] = useState(); + + const { Lr } = useLocalizer( + undefined, + useCallback(() => { + actionRef.current?.reload(); + setModalVisible(false); + }, []), + ); + + const [searchParams, setSearchParams] = useState<{ filter?: string }>({}); + + // Fetch Logic: Combined Groups + Features + const { data, isLoading } = useQuery({ + queryKey: ["featureDefinitions", searchParams], + queryFn: async () => { + // Fetch both + const [groupRes, featureRes] = await Promise.all([getGroupsApi(searchParams), getFeaturesApi(searchParams)]); + + // Map and Nest + const mappedGroups: FeatureGroupVo[] = groupRes.items.map((group) => { + const localizableGroup = deserialize(group.displayName); + + // Find features belonging to this group + const groupFeatures = featureRes.items + .filter((f) => f.groupName === group.name) + .map((f) => { + const fDisplay = deserialize(f.displayName); + const fDesc = deserialize(f.description); + return { + ...f, + displayName: Lr(fDisplay.resourceName, fDisplay.name), + description: Lr(fDesc.resourceName, fDesc.name), + }; + }); + + return { + ...group, + displayName: Lr(localizableGroup.resourceName, localizableGroup.name), + features: listToTree(groupFeatures, { id: "name", pid: "parentName" }), + }; + }); + + return mappedGroups; + }, + }); + + // Delete Feature + const { mutateAsync: deleteFeature } = useMutation({ + mutationFn: deleteApi, + onSuccess: () => { + toast.success($t("AbpUi.DeletedSuccessfully")); + queryClient.invalidateQueries({ queryKey: ["featureDefinitions"] }); + }, + }); + + // Handlers + const handleCreate = () => { + setSelectedFeature(undefined); + setModalVisible(true); + }; + + const handleUpdate = (feature: FeatureDefinitionDto) => { + setSelectedFeature(feature); + setModalVisible(true); + }; + + const handleDelete = (feature: FeatureDefinitionDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessageWithFormat", { 0: feature.name }), + onOk: () => deleteFeature(feature.name), + }); + }; + + // Outer Table Columns (Groups) + const groupColumns: ProColumns[] = [ + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("AbpFeatureManagement.DisplayName:Name"), + dataIndex: "name", + hideInSearch: true, + }, + { + title: $t("AbpFeatureManagement.DisplayName:DisplayName"), + dataIndex: "displayName", + hideInSearch: true, + }, + ]; + + // Inner Table Columns (Features) + const expandedRowRender = (groupRecord: FeatureGroupVo) => { + const featureColumns: any[] = [ + { + title: $t("AbpFeatureManagement.DisplayName:Name"), + dataIndex: "name", + width: 200, + }, + { + title: $t("AbpFeatureManagement.DisplayName:DisplayName"), + dataIndex: "displayName", + width: 200, + }, + { + title: $t("AbpFeatureManagement.DisplayName:Description"), + dataIndex: "description", + }, + { + title: $t("AbpFeatureManagement.DisplayName:IsVisibleToClients"), + dataIndex: "isVisibleToClients", + width: 150, + align: "center", + render: (val: boolean) => + val ? : , + }, + { + title: $t("AbpFeatureManagement.DisplayName:IsAvailableToHost"), + dataIndex: "isAvailableToHost", + width: 150, + align: "center", + render: (val: boolean) => + val ? : , + }, + { + title: $t("AbpUi.Actions"), + width: 180, + fixed: "right", + render: (_: any, record: FeatureDefinitionDto) => ( +
+ + {!record.isStatic && ( + + )} +
+ ), + }, + ]; + + return ( +
+ ); + }; + + return ( + <> + {contextHolder} + + + headerTitle={$t("AbpFeatureManagement.FeatureDefinitions")} + actionRef={actionRef} + columns={groupColumns} + dataSource={data} + loading={isLoading} + rowKey="name" + pagination={{ + showSizeChanger: true, + total: data?.length, + }} + scroll={{ x: "max-content" }} + search={{ + labelWidth: "auto", + span: 12, + defaultCollapsed: true, + }} + toolBarRender={() => [ + hasAccessByCodes([GroupDefinitionsPermissions.Create]) && ( + + ), + ]} + request={async (params) => { + const { filter } = params; + setSearchParams({ filter }); + // Invalidate to trigger useQuery refetch + await queryClient.invalidateQueries({ queryKey: ["featureDefinitions"] }); + return { data, success: true, total: data?.length }; + }} + expandable={{ + expandedRowRender, + defaultExpandAllRows: false, + }} + /> + + + setModalVisible(false)} + onChange={() => { + setModalVisible(false); + actionRef.current?.reload(); + }} + /> + + ); +}; + +export default FeatureDefinitionTable; diff --git a/apps/react-admin/src/pages/management/features/definitions-groups/feature-group-definition-modal.tsx b/apps/react-admin/src/pages/management/features/definitions-groups/feature-group-definition-modal.tsx new file mode 100644 index 000000000..40bfbf66e --- /dev/null +++ b/apps/react-admin/src/pages/management/features/definitions-groups/feature-group-definition-modal.tsx @@ -0,0 +1,161 @@ +import { useEffect, useState } from "react"; +import { Form, Input, Modal, Tabs } from "antd"; +import { useTranslation } from "react-i18next"; +import type { FeatureGroupDefinitionDto } from "#/management/features/groups"; +import type { PropertyInfo } from "@/components/abp/properties/types"; +import { createApi, getApi, updateApi } from "@/api/management/features/feature-group-definitions"; +import LocalizableInput from "@/components/abp/localizable-input/localizable-input"; +import PropertyTable from "@/components/abp/properties/property-table"; +import { toast } from "sonner"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (data: FeatureGroupDefinitionDto) => void; + groupName?: string; +} + +type TabKeys = "basic" | "props"; + +const defaultModel: FeatureGroupDefinitionDto = {} as FeatureGroupDefinitionDto; + +const FeatureGroupDefinitionModal: React.FC = ({ visible, onClose, onChange, groupName }) => { + const { t: $t } = useTranslation(); + const queryClient = useQueryClient(); + const [form] = Form.useForm(); + const [formModel, setFormModel] = useState({ + ...defaultModel, + }); + const [isEditModel, setIsEditModel] = useState(false); + const [activeTab, setActiveTab] = useState("basic"); + + // Fetch Group Details( mutate for GET request to allow manual triggering ) + const { mutateAsync: fetchGroup, isPending: isFetching } = useMutation({ + mutationFn: getApi, + onMutate: () => { + setIsEditModel(true); + }, + onSuccess: (dto) => { + setFormModel(dto); + form.setFieldsValue(dto); + }, + }); + + // Create Group + const { mutateAsync: createGroup, isPending: isCreating } = useMutation({ + mutationFn: createApi, + onSuccess: (res) => { + toast.success($t("AbpUi.SavedSuccessfully")); + queryClient.invalidateQueries({ queryKey: ["featureGroups"] }); + onChange(res); + onClose(); + }, + }); + + // Update Group + const { mutateAsync: updateGroup, isPending: isUpdating } = useMutation({ + mutationFn: (data: FeatureGroupDefinitionDto) => updateApi(data.name, data), + onSuccess: (res) => { + toast.success($t("AbpUi.SavedSuccessfully")); + queryClient.invalidateQueries({ queryKey: ["featureGroups"] }); + onChange(res); + onClose(); + }, + }); + + useEffect(() => { + if (visible) { + setIsEditModel(false); + setActiveTab("basic"); + setFormModel({ ...defaultModel }); + form.resetFields(); + + if (groupName) { + fetchGroup(groupName); + } + } + }, [visible, groupName]); + + const handleOk = async () => { + try { + const values = await form.validateFields(); + const submitData = { + ...values, + extraProperties: formModel.extraProperties, + }; + + if (isEditModel) { + await updateGroup(submitData); + } else { + await createGroup(submitData); + } + } catch (error) { + console.error(error); + } + }; + + const handlePropChange = (prop: PropertyInfo) => { + setFormModel((prev) => ({ + ...prev, + extraProperties: { + ...prev.extraProperties, + [prop.key]: prop.value, + }, + })); + }; + + const handlePropDelete = (prop: PropertyInfo) => { + setFormModel((prev) => { + const newProps = { ...prev.extraProperties }; + delete newProps[prop.key]; + return { + ...prev, + extraProperties: newProps, + }; + }); + }; + + return ( + +
+ setActiveTab(key as TabKeys)}> + + + + + + + + + + + + + +
+ ); +}; + +export default FeatureGroupDefinitionModal; diff --git a/apps/react-admin/src/pages/management/features/definitions-groups/feature-group-definition-table.tsx b/apps/react-admin/src/pages/management/features/definitions-groups/feature-group-definition-table.tsx new file mode 100644 index 000000000..1897fd2f9 --- /dev/null +++ b/apps/react-admin/src/pages/management/features/definitions-groups/feature-group-definition-table.tsx @@ -0,0 +1,233 @@ +import { useCallback, useRef, useState } from "react"; +import { Button, Card, Dropdown, Modal } from "antd"; +import { EditOutlined, DeleteOutlined, PlusOutlined, EllipsisOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import type { FeatureGroupDefinitionDto } from "#/management/features/groups"; +import { type ActionType, ProTable, type ProColumns } from "@ant-design/pro-table"; +import { hasAccessByCodes } from "@/utils/abp/access-checker"; +import { localizationSerializer } from "@/utils/abp/localization-serializer"; +import { deleteApi, getListApi } from "@/api/management/features/feature-group-definitions"; +import { + FeatureDefinitionsPermissions, + GroupDefinitionsPermissions, +} from "@/constants/management/features/permissions"; +import FeatureGroupDefinitionModal from "./feature-group-definition-modal"; +import { useLocalizer } from "@/hooks/abp/use-localization"; +import { toast } from "sonner"; +import { Iconify } from "@/components/icon"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import FeatureDefinitionModal from "../definitions-features/feature-definition-modal"; + +const FeatureGroupDefinitionTable: React.FC = () => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const queryClient = useQueryClient(); + const [modal, contextHolder] = Modal.useModal(); + const { deserialize } = localizationSerializer(); + + // Modal states + const [groupModalVisible, setGroupModalVisible] = useState(false); + const [featureModalVisible, setFeatureModalVisible] = useState(false); + const [selectedGroup, setSelectedGroup] = useState(); + const [selectedGroupForFeature, setSelectedGroupForFeature] = useState(); + + const { Lr } = useLocalizer( + undefined, + useCallback(() => { + actionRef.current?.reload(); + setFeatureModalVisible(false); + setGroupModalVisible(false); + }, []), + ); + + const [searchParams, setSearchParams] = useState<{ filter?: string }>({}); + + // Fetch List + const { data, isLoading } = useQuery({ + queryKey: ["featureGroups", searchParams], + queryFn: async () => { + const { items } = await getListApi(searchParams); + return items.map((item) => { + const localizableString = deserialize(item.displayName); + return { + ...item, + displayName: Lr(localizableString.resourceName, localizableString.name), + }; + }); + }, + }); + + // Delete Group + const { mutateAsync: deleteGroup } = useMutation({ + mutationFn: deleteApi, + onSuccess: () => { + toast.success($t("AbpUi.DeletedSuccessfully")); + queryClient.invalidateQueries({ queryKey: ["featureGroups"] }); + }, + }); + + const handleCreate = () => { + setSelectedGroup(undefined); + setGroupModalVisible(true); + }; + + const handleUpdate = (group: FeatureGroupDefinitionDto) => { + setSelectedGroup(group); + setGroupModalVisible(true); + }; + + const handleDelete = (group: FeatureGroupDefinitionDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessageWithFormat", { + 0: group.name, + }), + onOk: () => deleteGroup(group.name), + }); + }; + + const handleMenuClick = (key: string, group: FeatureGroupDefinitionDto) => { + if (key === "features") { + setSelectedGroupForFeature(group.name); + setFeatureModalVisible(true); + } + }; + + const columns: ProColumns[] = [ + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("AbpFeatureManagement.DisplayName:Name"), + dataIndex: "name", + width: "auto", + hideInSearch: true, + sorter: true, + }, + { + title: $t("AbpFeatureManagement.DisplayName:DisplayName"), + dataIndex: "displayName", + width: "auto", + hideInSearch: true, + sorter: true, + }, + { + title: $t("AbpUi.Actions"), + width: 220, + fixed: "right", + hideInSearch: true, + render: (_, record) => ( +
+
+ +
+ {!record.isStatic && ( + <> +
+ +
+
+ , + label: $t("AbpFeatureManagement.FeatureDefinitions:AddNew"), + } + : null, + ].filter((item) => item !== null), + onClick: ({ key }) => handleMenuClick(key as string, record), + }} + > +
+ + )} +
+ ), + }, + ]; + + return ( + <> + {contextHolder} + + + headerTitle={$t("AbpFeatureManagement.GroupDefinitions")} + actionRef={actionRef} + columns={columns} + dataSource={data} + loading={isLoading} + rowKey="name" + pagination={{ + showSizeChanger: true, + total: data?.length, + }} + search={{ + labelWidth: "auto", + span: 12, + defaultCollapsed: true, + }} + toolBarRender={() => [ + hasAccessByCodes([GroupDefinitionsPermissions.Create]) && ( + + ), + ]} + request={async (params) => { + const { filter } = params; + setSearchParams({ filter }); + await queryClient.invalidateQueries({ + queryKey: ["featureGroups"], + }); + return { data, success: true, total: data?.length }; + }} + /> + + setGroupModalVisible(false)} + onChange={() => { + setGroupModalVisible(false); + actionRef.current?.reload(); + }} + /> + setFeatureModalVisible(false)} + onChange={() => { + setFeatureModalVisible(false); + actionRef.current?.reload(); + }} + groupName={selectedGroupForFeature} + /> + + ); +}; +//TODO 添加功能-给自添加分组 +export default FeatureGroupDefinitionTable; diff --git a/apps/react-admin/src/pages/management/identity/organization-units/organization-unit-role-table.tsx b/apps/react-admin/src/pages/management/identity/organization-units/organization-unit-role-table.tsx index a90096d56..8f75f1f61 100644 --- a/apps/react-admin/src/pages/management/identity/organization-units/organization-unit-role-table.tsx +++ b/apps/react-admin/src/pages/management/identity/organization-units/organization-unit-role-table.tsx @@ -7,7 +7,7 @@ import { ProTable, type ActionType, type ProColumns } from "@ant-design/pro-tabl import { hasAccessByCodes } from "@/utils/abp/access-checker"; import { OrganizationUnitPermissions } from "@/constants/management/identity/permissions"; import SelectRoleModal from "./select-role-modal"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { removeOrganizationUnitApi } from "@/api/management/identity/role"; import { addRoles, getRoleListApi } from "@/api/management/identity/organization-units"; import { toast } from "sonner"; @@ -19,7 +19,6 @@ interface Props { const OrganizationUnitRoleTable: React.FC = ({ selectedKey }) => { const { t: $t } = useTranslation(); const actionRef = useRef(); - const queryClient = useQueryClient(); const [modal, contextHolder] = Modal.useModal(); const [roleModalVisible, setRoleModalVisible] = useState(false); diff --git a/apps/react-admin/src/pages/management/identity/organization-units/select-role-modal.tsx b/apps/react-admin/src/pages/management/identity/organization-units/select-role-modal.tsx index 8e971e6da..dc4900294 100644 --- a/apps/react-admin/src/pages/management/identity/organization-units/select-role-modal.tsx +++ b/apps/react-admin/src/pages/management/identity/organization-units/select-role-modal.tsx @@ -15,15 +15,13 @@ interface Props { const SelectRoleModal: React.FC = ({ visible, onClose, onConfirm, organizationUnitId }) => { const { t: $t } = useTranslation(); const [selectedRoles, setSelectedRoles] = useState([]); - const [searchParams, setSearchParams] = useState<{ filter?: string }>({}); // 获取可添加的角色列表 const { data, isLoading } = useQuery({ - queryKey: ["organizationUnitRoles", "unAdded", organizationUnitId, searchParams], + queryKey: ["organizationUnitRoles", "unAdded", organizationUnitId], queryFn: () => getUnaddedRoleListApi({ id: organizationUnitId, - ...searchParams, }), enabled: visible, }); diff --git a/apps/react-admin/src/pages/management/identity/roles/role-table.tsx b/apps/react-admin/src/pages/management/identity/roles/role-table.tsx index 0d0dce368..7adf39a50 100644 --- a/apps/react-admin/src/pages/management/identity/roles/role-table.tsx +++ b/apps/react-admin/src/pages/management/identity/roles/role-table.tsx @@ -7,9 +7,6 @@ import type { IdentityRoleDto } from "#/management/identity/role"; import { type ActionType, ProTable, type ProColumns } from "@ant-design/pro-table"; import { hasAccessByCodes } from "@/utils/abp/access-checker"; import { deleteApi, getPagedListApi } from "@/api/management/identity/role"; -import RoleModal from "./role-modal"; -import RoleClaimModal from "./role-claim-modal"; -import PermissionModal from "@/components/abp/permissions/permission-modal"; import { toast } from "sonner"; import { Iconify } from "@/components/icon"; import useAbpStore from "@/store/abpCoreStore"; @@ -17,17 +14,26 @@ import { IdentityRolePermissions } from "@/constants/management/identity/permiss import { AuditLogPermissions } from "@/constants/management/auditing/permissions"; import { EntityChangeDrawer } from "@/components/abp/auditing/entity-change-drawer"; +// Import your components +import RoleModal from "./role-modal"; +import RoleClaimModal from "./role-claim-modal"; +import PermissionModal from "@/components/abp/permissions/permission-modal"; +import MenuAllotModal from "@/pages/platform/menus/menu-allot-modal"; + const RoleTable: React.FC = () => { const { t: $t } = useTranslation(); const actionRef = useRef(); const queryClient = useQueryClient(); const abpStore = useAbpStore(); const [modal, contextHolder] = Modal.useModal(); + // Modal states const [roleModalVisible, setRoleModalVisible] = useState(false); const [claimModalVisible, setClaimModalVisible] = useState(false); const [permissionModalVisible, setPermissionModalVisible] = useState(false); const [entityChangeDrawerVisible, setEntityChangeDrawerVisible] = useState(false); + const [menuModalVisible, setMenuModalVisible] = useState(false); // Added state for Menu Modal + const [selectedRole, setSelectedRole] = useState(); const [searchParams, setSearchParams] = useState<{ filter?: string }>({}); @@ -66,6 +72,9 @@ const RoleTable: React.FC = () => { case "entity-changes": setEntityChangeDrawerVisible(true); break; + case "menus": // Handle menu click + setMenuModalVisible(true); + break; } }; @@ -136,6 +145,14 @@ const RoleTable: React.FC = () => { label: $t("AbpIdentity.ManageClaim"), } : null, + // Added Menu Management Item + hasAccessByCodes(["Platform.Menu.ManageRoles"]) + ? { + key: "menus", + icon: , + label: $t("AppPlatform.Menu:Manage"), + } + : null, hasAccessByCodes([AuditLogPermissions.Default]) ? { key: "entity-changes", @@ -143,7 +160,7 @@ const RoleTable: React.FC = () => { label: $t("AbpAuditLogging.EntitiesChanged"), } : null, - ].filter(Boolean), + ].filter(Boolean) as any, // filter(Boolean) needs 'as any' or strict type guard in some TS configs onClick: ({ key }) => handleMenuClick(key as string, record), }} > @@ -178,7 +195,6 @@ const RoleTable: React.FC = () => { request={async (params) => { const { filter } = params; setSearchParams({ filter }); - // 强制重新请求数据 await queryClient.invalidateQueries({ queryKey: ["roles"] }); return { data: data?.items, success: true, total: data?.totalCount }; }} @@ -197,6 +213,8 @@ const RoleTable: React.FC = () => { ]} /> + + {/* Modals */} { setSelectedRole(undefined); }} onChange={() => { - // actionRef.current?.reload(); queryClient.invalidateQueries({ queryKey: ["roles"] }); }} /> + {selectedRole && ( { setClaimModalVisible(false); setSelectedRole(undefined); }} - // onChange={() => { - // actionRef.current?.reload(); - // }} /> )} + { setPermissionModalVisible(false); setSelectedRole(undefined); }} onChange={() => { - // actionRef.current?.reload(); queryClient.invalidateQueries({ queryKey: ["roles"] }); }} /> + { setSelectedRole(undefined); }} /> + + {/* Added MenuAllotModal */} + { + setMenuModalVisible(false); + setSelectedRole(undefined); + }} + /> ); }; diff --git a/apps/react-admin/src/pages/management/identity/users/user-lock-modal.tsx b/apps/react-admin/src/pages/management/identity/users/user-lock-modal.tsx index b4639825d..cfcb55a5d 100644 --- a/apps/react-admin/src/pages/management/identity/users/user-lock-modal.tsx +++ b/apps/react-admin/src/pages/management/identity/users/user-lock-modal.tsx @@ -52,12 +52,12 @@ const UserLockModal: React.FC = ({ visible, onClose, onChange, userId }) + + + + + + + + + + ); +}; + +export default LocalizationLanguageModal; diff --git a/apps/react-admin/src/pages/management/localization/languages/localization-language-table.tsx b/apps/react-admin/src/pages/management/localization/languages/localization-language-table.tsx new file mode 100644 index 000000000..197d6ae05 --- /dev/null +++ b/apps/react-admin/src/pages/management/localization/languages/localization-language-table.tsx @@ -0,0 +1,175 @@ +import { useRef, useState } from "react"; +import { Button, Card, Modal, Space } from "antd"; +import { EditOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { getListApi, deleteApi } from "@/api/management/localization/languages"; +import type { LanguageDto } from "#/management/localization/languages"; +import LocalizationLanguageModal from "./localization-language-modal"; +import { hasAccessByCodes } from "@/utils/abp/access-checker"; +import { LanguagesPermissions } from "@/constants/management/localization/permissions"; +import useAbpStore from "@/store/abpCoreStore"; +import orderBy from "lodash.orderby"; +import { useSetLocale } from "@/store/localeI18nStore"; +import { LocalEnum, StorageEnum } from "#/enum"; +import { getStringItem } from "@/utils/storage"; + +const LocalizationLanguageTable: React.FC = () => { + const { t: $t, i18n } = useTranslation(); + const setLocale = useSetLocale(); + const currentLng = getStringItem(StorageEnum.I18N) || LocalEnum.en_US; + + const actionRef = useRef(); + const [modalVisible, setModalVisible] = useState(false); + const [currentCulture, setCurrentCulture] = useState(); + const [modal, contextHolder] = Modal.useModal(); + + // Use store to access and update application configuration + const application = useAbpStore((state) => state.application); + + const handleOpenModal = (cultureName?: string) => { + setCurrentCulture(cultureName); + setModalVisible(true); + }; + + const handleDelete = (row: LanguageDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessageWithFormat", { 0: row.cultureName }), + onOk: async () => { + await deleteApi(row.cultureName); + toast.success($t("AbpUi.DeletedSuccessfully")); + handleDataChange(row); + }, + }); + }; + + const handleDataChange = async (data: LanguageDto) => { + actionRef.current?.reload(); + + // If the changed language is the currently active language, refresh the localization config + const currentCultureName = application?.localization.currentCulture.cultureName; + if (data.cultureName === currentCultureName) { + try { + await setLocale(currentLng as LocalEnum, i18n); + } catch (error) { + console.error("Failed to refresh localization", error); + } + } + }; + + const columns: ProColumns[] = [ + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("AbpLocalization.DisplayName:CultureName"), + dataIndex: "cultureName", + sorter: true, + hideInSearch: true, + minWidth: 150, + }, + { + title: $t("AbpLocalization.DisplayName:DisplayName"), + dataIndex: "displayName", + sorter: true, + hideInSearch: true, + minWidth: 150, + }, + { + title: $t("AbpLocalization.DisplayName:UiCultureName"), + dataIndex: "uiCultureName", + sorter: true, + hideInSearch: true, + minWidth: 150, + }, + { + title: $t("AbpUi.Actions"), + valueType: "option", + fixed: "right", + width: 220, + render: (_, record) => ( + + {hasAccessByCodes([LanguagesPermissions.Update]) && ( + + )} + {/* !record.isStatic && no isStatic */} + {hasAccessByCodes([LanguagesPermissions.Delete]) && ( + + )} + + ), + }, + ]; + + return ( + <> + {contextHolder} + + + headerTitle={$t("AbpLocalization.Languages")} + actionRef={actionRef} + rowKey="cultureName" + columns={columns} + search={{ + labelWidth: "auto", + }} + toolBarRender={() => [ + hasAccessByCodes([LanguagesPermissions.Create]) && ( + + ), + ]} + request={async (params, sorter) => { + // Client-side pagination and sorting logic mirroring the Vue component + // 1. Fetch all data + const { items } = await getListApi({ filter: params.filter }); + + let dataSource = [...items]; + + // 2. Sort + if (sorter && Object.keys(sorter).length > 0) { + const sortField = Object.keys(sorter)[0]; + const sortOrder = sorter[sortField] === "ascend" ? "asc" : "desc"; + dataSource = orderBy(dataSource, [sortField], [sortOrder]); + } + + // 3. Paginate + const current = params.current || 1; + const pageSize = params.pageSize || 10; + const startIndex = (current - 1) * pageSize; + const endIndex = startIndex + pageSize; + const pageData = dataSource.slice(startIndex, endIndex); + + return { + data: pageData, + success: true, + total: dataSource.length, + }; + }} + pagination={{ + defaultPageSize: 10, + showSizeChanger: true, + }} + /> + + setModalVisible(false)} + onChange={handleDataChange} + /> + + ); +}; + +export default LocalizationLanguageTable; diff --git a/apps/react-admin/src/pages/management/localization/resources/localization-resource-modal.tsx b/apps/react-admin/src/pages/management/localization/resources/localization-resource-modal.tsx new file mode 100644 index 000000000..dc535a9bc --- /dev/null +++ b/apps/react-admin/src/pages/management/localization/resources/localization-resource-modal.tsx @@ -0,0 +1,103 @@ +import { useEffect, useState } from "react"; +import { Modal, Form, Input, Checkbox } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { createApi, updateApi, getApi } from "@/api/management/localization/resources"; +import type { ResourceDto } from "#/management/localization/resources"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (data: ResourceDto) => void; + resourceName?: string; +} + +const defaultModel: ResourceDto = { + displayName: "", + enable: true, + name: "", +} as ResourceDto; + +const LocalizationResourceModal: React.FC = ({ visible, onClose, onChange, resourceName }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + if (visible) { + form.resetFields(); + if (resourceName) { + fetchResource(resourceName); + } else { + form.setFieldsValue(defaultModel); + } + } + }, [visible, resourceName, form]); + + const fetchResource = async (name: string) => { + try { + setLoading(true); + const dto = await getApi(name); + form.setFieldsValue(dto); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + setSubmitting(true); + + const api = resourceName ? updateApi(resourceName, values) : createApi(values); + + const res = await api; + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(res); + onClose(); + } catch (error) { + console.error(error); + } finally { + setSubmitting(false); + } + }; + + const modalTitle = resourceName + ? `${$t("AbpLocalization.Resources")} - ${resourceName}` + : $t("LocalizationManagement.Resource:AddNew"); + + return ( + +
+ + {$t("LocalizationManagement.DisplayName:Enable")} + + + + + + + + + + + +
+ ); +}; + +export default LocalizationResourceModal; diff --git a/apps/react-admin/src/pages/management/localization/resources/localization-resource-table.tsx b/apps/react-admin/src/pages/management/localization/resources/localization-resource-table.tsx new file mode 100644 index 000000000..4967c7377 --- /dev/null +++ b/apps/react-admin/src/pages/management/localization/resources/localization-resource-table.tsx @@ -0,0 +1,162 @@ +import { useRef, useState } from "react"; +import { Button, Card, Modal, Space } from "antd"; +import { EditOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { getListApi, deleteApi } from "@/api/management/localization/resources"; +import type { ResourceDto } from "#/management/localization/resources"; +import LocalizationResourceModal from "./localization-resource-modal"; +import { hasAccessByCodes } from "@/utils/abp/access-checker"; +import { ResourcesPermissions } from "@/constants/management/localization/permissions"; +import useAbpStore from "@/store/abpCoreStore"; +import orderBy from "lodash.orderby"; +import { useSetLocale } from "@/store/localeI18nStore"; +import { getStringItem } from "@/utils/storage"; +import { LocalEnum, StorageEnum } from "#/enum"; + +const LocalizationResourceTable: React.FC = () => { + const { t: $t, i18n } = useTranslation(); + const setLocale = useSetLocale(); + const currentLng = getStringItem(StorageEnum.I18N) || LocalEnum.en_US; + const actionRef = useRef(); + const [modalVisible, setModalVisible] = useState(false); + const [currentResource, setCurrentResource] = useState(); + const [modal, contextHolder] = Modal.useModal(); + + const application = useAbpStore((state) => state.application); + + const handleOpenModal = (name?: string) => { + setCurrentResource(name); + setModalVisible(true); + }; + + const handleDelete = (row: ResourceDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessageWithFormat", { 0: row.name }), + onOk: async () => { + await deleteApi(row.name); + toast.success($t("AbpUi.DeletedSuccessfully")); + handleDataChange(); + }, + }); + }; + + const handleDataChange = async () => { + actionRef.current?.reload(); + + // Refresh localization cache + const currentCultureName = application?.localization.currentCulture.cultureName; + if (currentCultureName) { + try { + await setLocale(currentLng as LocalEnum, i18n); + } catch (error) { + console.error("Failed to refresh localization", error); + } + } + }; + + const columns: ProColumns[] = [ + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("AbpFeatureManagement.DisplayName:Name"), + dataIndex: "name", + sorter: true, + hideInSearch: true, + minWidth: 150, + }, + { + title: $t("AbpFeatureManagement.DisplayName:DisplayName"), + dataIndex: "displayName", + sorter: true, + hideInSearch: true, + minWidth: 150, + }, + { + title: $t("AbpUi.Actions"), + valueType: "option", + fixed: "right", + width: 220, + render: (_, record) => ( + + {hasAccessByCodes([ResourcesPermissions.Update]) && ( + + )} + {/* !record.isStatic && no isStatic */} + {hasAccessByCodes([ResourcesPermissions.Delete]) && ( + + )} + + ), + }, + ]; + + return ( + <> + {contextHolder} + + + headerTitle={$t("AbpLocalization.Resources")} + actionRef={actionRef} + rowKey="name" + columns={columns} + search={{ + labelWidth: "auto", + }} + toolBarRender={() => [ + hasAccessByCodes([ResourcesPermissions.Create]) && ( + + ), + ]} + request={async (params, sorter) => { + const { items } = await getListApi({ filter: params.filter }); + + let dataSource = [...items]; + + if (sorter && Object.keys(sorter).length > 0) { + const sortField = Object.keys(sorter)[0]; + const sortOrder = sorter[sortField] === "ascend" ? "asc" : "desc"; + dataSource = orderBy(dataSource, [sortField], [sortOrder]); + } + + const current = params.current || 1; + const pageSize = params.pageSize || 10; + const startIndex = (current - 1) * pageSize; + const endIndex = startIndex + pageSize; + const pageData = dataSource.slice(startIndex, endIndex); + + return { + data: pageData, + success: true, + total: dataSource.length, + }; + }} + pagination={{ + defaultPageSize: 10, + showSizeChanger: true, + }} + /> + + setModalVisible(false)} + onChange={handleDataChange} + /> + + ); +}; + +export default LocalizationResourceTable; diff --git a/apps/react-admin/src/pages/management/localization/texts/localization-text-modal.tsx b/apps/react-admin/src/pages/management/localization/texts/localization-text-modal.tsx new file mode 100644 index 000000000..50e4c08fd --- /dev/null +++ b/apps/react-admin/src/pages/management/localization/texts/localization-text-modal.tsx @@ -0,0 +1,162 @@ +import { useEffect, useState } from "react"; +import { Modal, Form, Input, Select } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { getApi, setApi } from "@/api/management/localization/texts"; +import { getListApi as getLanguagesApi } from "@/api/management/localization/languages"; +import { getListApi as getResourcesApi } from "@/api/management/localization/resources"; +import type { TextDto, TextDifferenceDto } from "#/management/localization/texts"; +import type { LanguageDto } from "#/management/localization/languages"; +import type { ResourceDto } from "#/management/localization/resources"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (data: TextDto) => void; + data?: TextDifferenceDto; // Data passed from the table row + defaultTargetCulture?: string; // For creation + defaultResourceName?: string; // For creation +} + +const LocalizationTextModal: React.FC = ({ + visible, + onClose, + onChange, + data, + defaultTargetCulture, + defaultResourceName, +}) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [isEdit, setIsEdit] = useState(false); + + // Dropdown Data + const [languages, setLanguages] = useState([]); + const [resources, setResources] = useState([]); + + // Fetch Options on Mount + useEffect(() => { + const initOptions = async () => { + const [langRes, resRes] = await Promise.all([getLanguagesApi(), getResourcesApi()]); + setLanguages(langRes.items); + setResources(resRes.items); + }; + initOptions(); + }, []); + + // Handle Modal Open State + useEffect(() => { + if (visible) { + form.resetFields(); + if (data?.targetCultureName) { + // Edit Mode + setIsEdit(true); + fetchTextDetails({ + cultureName: data.targetCultureName, + key: data.key, + resourceName: data.resourceName, + }); + } else { + // Create Mode + setIsEdit(false); + form.setFieldsValue({ + cultureName: defaultTargetCulture, + resourceName: defaultResourceName, + }); + } + } + }, [visible, data, defaultTargetCulture, defaultResourceName, form]); + + const fetchTextDetails = async (input: { cultureName: string; key: string; resourceName: string }) => { + try { + setLoading(true); + const textDto = await getApi(input); + form.setFieldsValue(textDto); + } finally { + setLoading(false); + } + }; + + // Handle changing culture dropdown inside the modal to switch context + const handleLanguageChange = async (newCulture: string) => { + // Only fetch if we have enough info to look up the key + const currentValues = form.getFieldsValue(); + if (currentValues.key && currentValues.resourceName) { + await fetchTextDetails({ + cultureName: newCulture, + key: currentValues.key, + resourceName: currentValues.resourceName, + }); + } + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + setSubmitting(true); + await setApi(values); + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(values); + onClose(); + } catch (error) { + console.error(error); + } finally { + setSubmitting(false); + } + }; + + return ( + +
+ + + + + + + + + + + + +
+ ); +}; + +export default LocalizationTextModal; diff --git a/apps/react-admin/src/pages/management/localization/texts/localization-text-table.tsx b/apps/react-admin/src/pages/management/localization/texts/localization-text-table.tsx new file mode 100644 index 000000000..cff7c0f02 --- /dev/null +++ b/apps/react-admin/src/pages/management/localization/texts/localization-text-table.tsx @@ -0,0 +1,254 @@ +import { useRef, useState, useEffect } from "react"; +import { Button, Card, Space, type FormInstance } from "antd"; +import { EditOutlined, PlusOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import ProTable, { type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { getListApi } from "@/api/management/localization/texts"; +import { getListApi as getLanguagesApi } from "@/api/management/localization/languages"; +import { getListApi as getResourcesApi } from "@/api/management/localization/resources"; +import type { TextDifferenceDto, TextDto } from "#/management/localization/texts"; +import type { LanguageDto } from "#/management/localization/languages"; +import type { ResourceDto } from "#/management/localization/resources"; +import LocalizationTextModal from "./localization-text-modal"; +import { hasAccessByCodes } from "@/utils/abp/access-checker"; +import { TextsPermissions } from "@/constants/management/localization/permissions"; +import useAbpStore from "@/store/abpCoreStore"; +import orderBy from "lodash.orderby"; +import { useSetLocale } from "@/store/localeI18nStore"; +import { getStringItem } from "@/utils/storage"; +import { LocalEnum, StorageEnum } from "#/enum"; + +const LocalizationTextTable: React.FC = () => { + const { t: $t, i18n } = useTranslation(); + const setLocale = useSetLocale(); + const currentLng = getStringItem(StorageEnum.I18N) || LocalEnum.en_US; + + const actionRef = useRef(); + // 2. Fix the type here: Change to + const formRef = useRef(); + + // Modal State + const [modalVisible, setModalVisible] = useState(false); + const [currentRow, setCurrentRow] = useState(); + + // Filters Data + const [languages, setLanguages] = useState([]); + const [resources, setResources] = useState([]); + + const application = useAbpStore((state) => state.application); + + useEffect(() => { + const initData = async () => { + const [langRes, resRes] = await Promise.all([getLanguagesApi(), getResourcesApi()]); + setLanguages(langRes.items); + setResources(resRes.items); + }; + initData(); + }, []); + + const handleOpenCreate = () => { + setCurrentRow(undefined); + setModalVisible(true); + }; + + const handleOpenEdit = (row: TextDifferenceDto) => { + setCurrentRow(row); + setModalVisible(true); + }; + + const handleDataChange = async (data: TextDto) => { + actionRef.current?.reload(); + + // If the modified text belongs to the current app culture, refresh the app configuration + const currentAppCulture = application?.localization.currentCulture.cultureName; + if (data.cultureName === currentAppCulture) { + try { + await setLocale(currentLng as LocalEnum, i18n); + } catch (error) { + console.error("Failed to refresh localization cache", error); + } + } + }; + + const columns: ProColumns[] = [ + { + title: $t("AbpLocalization.DisplayName:CultureName"), + dataIndex: "cultureName", + valueType: "select", + fieldProps: { + options: languages, + fieldNames: { label: "displayName", value: "cultureName" }, + showSearch: true, + optionFilterProp: "displayName", + }, + hideInTable: true, + initialValue: application?.localization.currentCulture.cultureName, + formItemProps: { + rules: [{ required: true }], + }, + }, + { + title: $t("AbpLocalization.DisplayName:TargetCultureName"), + dataIndex: "targetCultureName", + valueType: "select", + fieldProps: { + options: languages, + fieldNames: { label: "displayName", value: "cultureName" }, + showSearch: true, + optionFilterProp: "displayName", + }, + hideInTable: true, + initialValue: application?.localization.currentCulture.cultureName, + formItemProps: { + rules: [{ required: true }], + }, + }, + { + title: $t("AbpLocalization.DisplayName:ResourceName"), + dataIndex: "resourceName", + valueType: "select", + fieldProps: { + options: resources, + fieldNames: { label: "displayName", value: "name" }, + showSearch: true, + optionFilterProp: "displayName", + }, + width: 150, + sorter: true, + }, + { + title: $t("AbpLocalization.DisplayName:TargetValue"), + dataIndex: "onlyNull", + valueType: "select", + hideInTable: true, + fieldProps: { + options: [ + { label: $t("AbpLocalization.DisplayName:Any"), value: "false" }, + { label: $t("AbpLocalization.DisplayName:OnlyNull"), value: "true" }, + ], + }, + initialValue: "false", + }, + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("AbpLocalization.DisplayName:Key"), + dataIndex: "key", + width: 200, + sorter: true, + hideInSearch: true, + }, + { + title: $t("AbpLocalization.DisplayName:Value"), + dataIndex: "value", + width: 300, + sorter: true, + hideInSearch: true, + render: (dom) =>
{dom}
, + }, + { + title: $t("AbpLocalization.DisplayName:TargetValue"), + dataIndex: "targetValue", + width: 300, + sorter: true, + hideInSearch: true, + render: (dom) =>
{dom}
, + }, + { + title: $t("AbpUi.Actions"), + valueType: "option", + fixed: "right", + width: 100, + render: (_, record) => ( + + {hasAccessByCodes([TextsPermissions.Update]) && ( + + )} + + ), + }, + ]; + + return ( + <> + + + + headerTitle={$t("AbpLocalization.Texts")} + actionRef={actionRef} + formRef={formRef} + rowKey={(record) => record.resourceName + record.key} + columns={columns} + search={{ + labelWidth: "auto", + defaultCollapsed: false, + }} + toolBarRender={() => [ + hasAccessByCodes([TextsPermissions.Create]) && ( + + ), + ]} + request={async (params, sorter) => { + // Provide defaults for required fields if they are missing (initial load) + const requestParams: any = { + ...params, + cultureName: params.cultureName || application?.localization.currentCulture.cultureName, + targetCultureName: params.targetCultureName || application?.localization.currentCulture.cultureName, + onlyNull: params.onlyNull || "false", + }; + + // Fetch Data + const { items } = await getListApi(requestParams); + + let dataSource = [...items]; + + // Client-side Sort + if (sorter && Object.keys(sorter).length > 0) { + const sortField = Object.keys(sorter)[0]; + const sortOrder = sorter[sortField] === "ascend" ? "asc" : "desc"; + dataSource = orderBy(dataSource, [sortField], [sortOrder]); + } + + // Client-side Pagination + const current = params.current || 1; + const pageSize = params.pageSize || 10; + const startIndex = (current - 1) * pageSize; + const endIndex = startIndex + pageSize; + const pageData = dataSource.slice(startIndex, endIndex); + + return { + data: pageData, + success: true, + total: dataSource.length, + }; + }} + pagination={{ + defaultPageSize: 10, + showSizeChanger: true, + }} + manualRequest={false} + /> + + + setModalVisible(false)} + onChange={handleDataChange} + /> + + ); +}; + +export default LocalizationTextTable; diff --git a/apps/react-admin/src/pages/management/loggings/logging-drawer.tsx b/apps/react-admin/src/pages/management/loggings/logging-drawer.tsx new file mode 100644 index 000000000..562bfa621 --- /dev/null +++ b/apps/react-admin/src/pages/management/loggings/logging-drawer.tsx @@ -0,0 +1,137 @@ +import type React from "react"; +import { useEffect, useState } from "react"; +import { Descriptions, Drawer, Tabs, Tag, Table, Input } from "antd"; +import { useTranslation } from "react-i18next"; +import type { LogDto, LogExceptionDto, LogLevel } from "#/management/auditing/loggings"; +import { getApi } from "@/api/management/auditing/loggings"; +import { formatToDateTime } from "@/utils/abp"; + +interface LoggingDrawerProps { + visible: boolean; + onClose: () => void; + logId?: string; + logLevelOptions: { color: string; label: string; value: LogLevel }[]; +} + +const LoggingDrawer: React.FC = ({ visible, onClose, logId, logLevelOptions }) => { + const { t: $t } = useTranslation(); + const [activeTab, setActiveTab] = useState("basic"); + const [logModel, setLogModel] = useState({} as LogDto); + const [loading, setLoading] = useState(false); + + // Helper to find level details + const getLevelOption = (level: LogLevel) => { + return logLevelOptions.find((opt) => opt.value === level); + }; + + useEffect(() => { + if (visible && logId) { + fetchLog(logId); + } else { + setLogModel({} as LogDto); + } + }, [visible, logId]); + + const fetchLog = async (id: string) => { + try { + setLoading(true); + const dto = await getApi(id); + setLogModel(dto); + } finally { + setLoading(false); + } + }; + + const exceptionColumns = [ + { + title: $t("AbpAuditLogging.Class"), + dataIndex: "class", + key: "class", + sorter: true, + }, + ]; + + const expandedExceptionRender = (record: LogExceptionDto) => ( + + {record.class} + {record.message} + {record.source} + + + + {record.hResult} + {record.helpUrl} + + ); + + return ( + + + {/* Basic Operation Tab */} + + + + {formatToDateTime(logModel.timeStamp)} + + + {logModel.level !== undefined && ( + {getLevelOption(logModel.level)?.label} + )} + + {logModel.message} + + + + {/* Fields Tab */} + + + + {logModel.fields?.application} + + + {logModel.fields?.machineName} + + + {logModel.fields?.environment} + + {logModel.fields?.processId} + {logModel.fields?.threadId} + {logModel.fields?.context} + {logModel.fields?.actionId} + + {logModel.fields?.actionName} + + {logModel.fields?.requestId} + + {logModel.fields?.requestPath} + + + {logModel.fields?.connectionId} + + + {logModel.fields?.correlationId} + + {logModel.fields?.clientId} + {logModel.fields?.userId} + + + + {/* Exceptions Tab */} + {logModel.exceptions && logModel.exceptions.length > 0 && ( + +
record?.class + (record?.message || "")} // Generate a key if no ID + expandable={{ expandedRowRender: expandedExceptionRender }} + pagination={false} + bordered + /> + + )} + + + ); +}; + +export default LoggingDrawer; diff --git a/apps/react-admin/src/pages/management/loggings/logging-table.tsx b/apps/react-admin/src/pages/management/loggings/logging-table.tsx new file mode 100644 index 000000000..c94f99862 --- /dev/null +++ b/apps/react-admin/src/pages/management/loggings/logging-table.tsx @@ -0,0 +1,238 @@ +import type React from "react"; +import { useRef, useState } from "react"; +import { Button, Tag, Card, Space, type FormInstance, Checkbox } from "antd"; +import { EditOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import ProTable, { type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { useQueryClient } from "@tanstack/react-query"; +import { formatToDateTime } from "@/utils/abp"; +import type { LogDto } from "#/management/auditing/loggings"; +import { LogLevel } from "#/management/auditing/loggings"; +import { getPagedListApi } from "@/api/management/auditing/loggings"; +import { SystemLogPermissions } from "@/constants/management/auditing/permissions"; +import { hasAccessByCodes, withAccessChecker } from "@/utils/abp/access-checker"; +import { antdOrderToAbpOrder } from "@/utils/abp/sort-order"; +import LoggingDrawer from "./logging-drawer"; + +const LoggingTable: React.FC = () => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const formRef = useRef(); + const queryClient = useQueryClient(); + + const [drawerVisible, setDrawerVisible] = useState(false); + const [selectedLogId, setSelectedLogId] = useState(undefined); + + // Log Level Options (mirrors the reactive array in Vue) + const logLevelOptions = [ + { color: "purple", label: "Trace", value: LogLevel.Trace }, + { color: "blue", label: "Debug", value: LogLevel.Debug }, + { color: "green", label: "Information", value: LogLevel.Information }, + { color: "orange", label: "Warning", value: LogLevel.Warning }, + { color: "red", label: "Error", value: LogLevel.Error }, + { color: "#f50", label: "Critical", value: LogLevel.Critical }, + { color: "", label: "None", value: LogLevel.None }, + ]; + + const openDrawer = (log: LogDto) => { + // Note: LogDto in the grid usually has 'fields.id' but sometimes just 'id' depending on DTO structure. + // The Vue code accessed `row.fields.id` for the update action. + setSelectedLogId(log.fields?.id); + setDrawerVisible(true); + }; + + const closeDrawer = () => { + setDrawerVisible(false); + setSelectedLogId(undefined); + }; + + const onFilter = (field: string, value: any) => { + if (formRef.current) { + formRef.current.setFieldsValue({ [field]: value }); + formRef.current.submit(); + } + }; + + const columns: ProColumns[] = [ + { + title: $t("AbpAuditLogging.Level"), + dataIndex: "level", + valueType: "select", + width: 120, + fieldProps: { + options: logLevelOptions, + }, + render: (_, record) => { + const option = logLevelOptions.find((opt) => opt.value === record.level); + return ( + + + + ); + }, + }, + { + title: $t("AbpAuditLogging.TimeStamp"), + dataIndex: "timeStamp", + valueType: "dateRange", + sorter: true, + width: 150, + render: (_, record) => formatToDateTime(record.timeStamp), + }, + { + title: $t("AbpAuditLogging.Message"), + dataIndex: "message", + sorter: true, + ellipsis: true, + width: 500, + hideInSearch: true, + }, + { + title: $t("AbpAuditLogging.ApplicationName"), + dataIndex: "application", + sorter: true, + width: 150, + render: (_, record) => record.fields?.application, + }, + { + title: $t("AbpAuditLogging.MachineName"), + dataIndex: "machineName", + sorter: true, + width: 140, + render: (_, record) => record.fields?.machineName, + }, + { + title: $t("AbpAuditLogging.Environment"), + dataIndex: "environment", + sorter: true, + width: 150, + render: (_, record) => record.fields?.environment, + }, + { + title: $t("AbpAuditLogging.RequestId"), + dataIndex: "requestId", + sorter: true, + width: 200, + render: (_, record) => record.fields?.requestId, + }, + { + title: $t("AbpAuditLogging.RequestPath"), + dataIndex: "requestPath", + sorter: true, + width: 300, + render: (_, record) => record.fields?.requestPath, + }, + { + title: $t("AbpAuditLogging.ConnectionId"), + dataIndex: "connectionId", + sorter: true, + width: 150, + hideInSearch: true, // Not in Vue schema + render: (_, record) => record.fields?.connectionId, + }, + { + title: $t("AbpAuditLogging.CorrelationId"), + dataIndex: "correlationId", + sorter: true, + width: 240, + render: (_, record) => record.fields?.correlationId, + }, + { + title: $t("AbpAuditLogging.HasException"), + dataIndex: "hasException", + valueType: "checkbox", + hideInTable: true, + renderFormItem: () => { + return ( + onFilter("hasException", e.target.checked)}> + {$t("AbpAuditLogging.HasException")} + + ); + }, + }, + hasAccessByCodes([SystemLogPermissions.Default]) + ? { + title: $t("AbpUi.Actions"), + key: "actions", + fixed: "right", + width: 180, + hideInSearch: true, + render: (_, record) => ( + + {withAccessChecker( + , + [SystemLogPermissions.Default], + )} + + ), + } + : {}, + ]; + + return ( + <> + + + + headerTitle={$t("AbpAuditLogging.Logging")} + actionRef={actionRef} + formRef={formRef} + rowKey={(record) => record.fields?.id} + columns={columns} + request={async (params, sorter) => { + const { current, pageSize, timeStamp, ...filters } = params; + const [startTime, endTime] = timeStamp || []; + + const query = await queryClient.fetchQuery({ + queryKey: ["loggings", params, sorter], + queryFn: () => + getPagedListApi({ + maxResultCount: pageSize, + skipCount: ((current || 1) - 1) * (pageSize || 0), + sorting: sorter + ? Object.keys(sorter) + .map((key) => `${key} ${antdOrderToAbpOrder(sorter[key])}`) + .join(", ") + : undefined, + startTime: startTime || undefined, + endTime: endTime || undefined, + ...filters, + }), + }); + return { + data: query.items, + total: query.totalCount, + success: true, + }; + }} + pagination={{ + showSizeChanger: true, + }} + search={{ + labelWidth: "auto", + defaultCollapsed: true, + }} + scroll={{ x: "max-content" }} + /> + + + + + + ); +}; + +export default LoggingTable; diff --git a/apps/react-admin/src/pages/management/notifications/definitions/groups/notification-group-definition-modal.tsx b/apps/react-admin/src/pages/management/notifications/definitions/groups/notification-group-definition-modal.tsx new file mode 100644 index 000000000..21c25ffbb --- /dev/null +++ b/apps/react-admin/src/pages/management/notifications/definitions/groups/notification-group-definition-modal.tsx @@ -0,0 +1,173 @@ +import type React from "react"; +import { useEffect, useState } from "react"; +import { Modal, Form, Input, Checkbox, Tabs } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { useMutation } from "@tanstack/react-query"; +import { createApi, updateApi, getApi } from "@/api/management/notifications/notification-group-definitions"; +import type { NotificationGroupDefinitionDto } from "#/notifications/groups"; +import type { PropertyInfo } from "@/components/abp/properties/types"; +import LocalizableInput from "@/components/abp/localizable-input/localizable-input"; +import PropertyTable from "@/components/abp/properties/property-table"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (data: NotificationGroupDefinitionDto) => void; + groupName?: string; +} + +const defaultModel: NotificationGroupDefinitionDto = {} as NotificationGroupDefinitionDto; + +const NotificationGroupDefinitionModal: React.FC = ({ visible, onClose, onChange, groupName }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + + const [activeTab, setActiveTab] = useState("basic"); + const [formModel, setFormModel] = useState({ ...defaultModel }); + const [loading, setLoading] = useState(false); + const [isEditModel, setIsEditModel] = useState(false); + + useEffect(() => { + if (visible) { + setActiveTab("basic"); + form.resetFields(); + if (groupName) { + fetchGroup(groupName); + setIsEditModel(true); + } else { + setFormModel({ ...defaultModel }); + setIsEditModel(false); + } + } + }, [visible, groupName]); + + const fetchGroup = async (name: string) => { + try { + setLoading(true); + const dto = await getApi(name); + setFormModel(dto); + form.setFieldsValue(dto); + } finally { + setLoading(false); + } + }; + + const { mutateAsync: createGroup, isPending: isCreating } = useMutation({ + mutationFn: createApi, + onSuccess: (res) => { + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(res); + onClose(); + }, + }); + + const { mutateAsync: updateGroup, isPending: isUpdating } = useMutation({ + mutationFn: (data: NotificationGroupDefinitionDto) => updateApi(data.name, data), + onSuccess: (res) => { + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(res); + onClose(); + }, + }); + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + const submitData = { + ...values, + extraProperties: formModel.extraProperties, + }; + + if (isEditModel) { + await updateGroup(submitData); + } else { + await createGroup(submitData); + } + } catch (error) { + console.error(error); + } + }; + + const handlePropChange = (prop: PropertyInfo) => { + setFormModel((prev) => ({ + ...prev, + extraProperties: { + ...prev.extraProperties, + [prop.key]: prop.value, + }, + })); + }; + + const handlePropDelete = (prop: PropertyInfo) => { + setFormModel((prev) => { + const newProps = { ...prev.extraProperties }; + delete newProps[prop.key]; + return { + ...prev, + extraProperties: newProps, + }; + }); + }; + + const modalTitle = isEditModel + ? `${$t("Notifications.GroupDefinitions")} - ${formModel.name}` + : $t("Notifications.GroupDefinitions:AddNew"); + + return ( + +
+ + + + + + + + + + + + + + + + + {$t("Notifications.DisplayName:AllowSubscriptionToClients")} + + + + + + + + + +
+ ); +}; + +export default NotificationGroupDefinitionModal; diff --git a/apps/react-admin/src/pages/management/notifications/definitions/groups/notification-group-definition-table.tsx b/apps/react-admin/src/pages/management/notifications/definitions/groups/notification-group-definition-table.tsx new file mode 100644 index 000000000..6952a29d7 --- /dev/null +++ b/apps/react-admin/src/pages/management/notifications/definitions/groups/notification-group-definition-table.tsx @@ -0,0 +1,187 @@ +import type React from "react"; +import { useRef, useState } from "react"; +import { Button, Card, Dropdown, Modal, Space } from "antd"; +import { EditOutlined, DeleteOutlined, PlusOutlined, EllipsisOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deleteApi, getListApi } from "@/api/management/notifications/notification-group-definitions"; +import type { NotificationGroupDefinitionDto } from "#/notifications/groups"; +import { GroupDefinitionsPermissions, NotificationDefinitionsPermissions } from "@/constants/notifications/permissions"; +import { hasAccessByCodes, withAccessChecker } from "@/utils/abp/access-checker"; +import { localizationSerializer } from "@/utils/abp/localization-serializer"; +import { useLocalizer } from "@/hooks/abp/use-localization"; +import { Iconify } from "@/components/icon"; + +import NotificationGroupDefinitionModal from "./notification-group-definition-modal"; +import NotificationDefinitionModal from "../notifications/notification-definition-modal"; + +const NotificationGroupDefinitionTable: React.FC = () => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const queryClient = useQueryClient(); + const { deserialize } = localizationSerializer(); + const { Lr } = useLocalizer(); + const [modal, contextHolder] = Modal.useModal(); + + // State + const [groupModalVisible, setGroupModalVisible] = useState(false); + const [definitionModalVisible, setDefinitionModalVisible] = useState(false); + const [selectedGroupName, setSelectedGroupName] = useState(); + + const { mutateAsync: deleteGroup } = useMutation({ + mutationFn: deleteApi, + onSuccess: () => { + toast.success($t("AbpUi.DeletedSuccessfully")); + queryClient.invalidateQueries({ queryKey: ["notificationGroups"] }); + actionRef.current?.reload(); + }, + }); + + const handleCreate = () => { + setSelectedGroupName(undefined); + setGroupModalVisible(true); + }; + + const handleUpdate = (row: NotificationGroupDefinitionDto) => { + setSelectedGroupName(row.name); + setGroupModalVisible(true); + }; + + const handleDelete = (row: NotificationGroupDefinitionDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessageWithFormat", { 0: row.name }), + onOk: () => deleteGroup(row.name), + }); + }; + + const handleMenuClick = (key: string, row: NotificationGroupDefinitionDto) => { + if (key === "definitions") { + setSelectedGroupName(row.name); + setDefinitionModalVisible(true); + } + }; + + const columns: ProColumns[] = [ + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("Notifications.DisplayName:Name"), + dataIndex: "name", + width: "auto", + sorter: true, + hideInSearch: true, + }, + { + title: $t("Notifications.DisplayName:DisplayName"), + dataIndex: "displayName", + width: "auto", + sorter: true, + hideInSearch: true, + render: (_, record) => { + const localizableString = deserialize(record.displayName); + return Lr(localizableString.resourceName, localizableString.name); + }, + }, + { + title: $t("AbpUi.Actions"), + width: 220, + fixed: "right", + hideInSearch: true, + render: (_, record) => ( + + {withAccessChecker( + , + [GroupDefinitionsPermissions.Update], + )} + + {!record.isStatic && ( + <> + {withAccessChecker( + , + [GroupDefinitionsPermissions.Delete], + )} + , + onClick: () => handleMenuClick("definitions", record), + } + : null, + ].filter(Boolean) as any, + }} + > + , + [GroupDefinitionsPermissions.Create], + ), + ]} + request={async (params) => { + const { current, pageSize, ...filters } = params; + const { items } = await getListApi({ + filter: filters.filter, + }); + + return { + data: items, + total: items.length, + success: true, + }; + }} + pagination={{ defaultPageSize: 10, showSizeChanger: true }} + /> + + setGroupModalVisible(false)} + onChange={() => actionRef.current?.reload()} + /> + + setDefinitionModalVisible(false)} + onChange={() => actionRef.current?.reload()} // Optional refresh + /> + + ); +}; + +export default NotificationGroupDefinitionTable; diff --git a/apps/react-admin/src/pages/management/notifications/definitions/notifications/notification-definition-modal.tsx b/apps/react-admin/src/pages/management/notifications/definitions/notifications/notification-definition-modal.tsx new file mode 100644 index 000000000..2bc15080f --- /dev/null +++ b/apps/react-admin/src/pages/management/notifications/definitions/notifications/notification-definition-modal.tsx @@ -0,0 +1,272 @@ +import type React from "react"; +import { useEffect, useState } from "react"; +import { Modal, Form, Input, Checkbox, Select, Tabs } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { useMutation } from "@tanstack/react-query"; +import { createApi, updateApi, getApi } from "@/api/management/notifications/notification-definitions"; +import { getListApi as getGroupDefinitionsApi } from "@/api/management/notifications/notification-group-definitions"; +import { getAssignableProvidersApi, getAssignableTemplatesApi } from "@/api/management/notifications/notifications"; +import type { + NotificationDefinitionDto, + NotificationProviderDto, + NotificationTemplateDto, +} from "#/notifications/definitions"; +import { NotificationContentType, NotificationLifetime, NotificationType } from "#/notifications"; // Adjust path +import type { PropertyInfo } from "@/components/abp/properties/types"; +import LocalizableInput from "@/components/abp/localizable-input/localizable-input"; +import PropertyTable from "@/components/abp/properties/property-table"; +import { localizationSerializer } from "@/utils/abp/localization-serializer"; +import { useLocalizer } from "@/hooks/abp/use-localization"; +import { useEnumMaps } from "./use-enum-maps"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (data: NotificationDefinitionDto) => void; + definitionName?: string; + groupName?: string; +} + +const defaultModel: NotificationDefinitionDto = { + allowSubscriptionToClients: false, + contentType: NotificationContentType.Text, + displayName: "", + extraProperties: {}, + groupName: "", + isStatic: false, + name: "", + notificationLifetime: NotificationLifetime.Persistent, + notificationType: NotificationType.Application, + providers: [], + template: undefined, +} as NotificationDefinitionDto; + +const NotificationDefinitionModal: React.FC = ({ visible, onClose, onChange, definitionName, groupName }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + const { deserialize } = localizationSerializer(); + const { Lr } = useLocalizer(); + const { notificationContentTypeOptions, notificationLifetimeOptions, notificationTypeOptions } = useEnumMaps(); + + const [activeTab, setActiveTab] = useState("basic"); + const [formModel, setFormModel] = useState({ ...defaultModel }); + const [isEditModel, setIsEditModel] = useState(false); + const [loading, setLoading] = useState(false); + + // Dropdown Data + const [providers, setProviders] = useState([]); + const [templates, setTemplates] = useState([]); + const [groups, setGroups] = useState<{ label: string; value: string }[]>([]); + + useEffect(() => { + if (visible) { + setActiveTab("basic"); + initData(); + } + }, [visible]); + + const initData = async () => { + try { + setLoading(true); + const [groupsRes, templatesRes, providersRes] = await Promise.all([ + getGroupDefinitionsApi({ filter: groupName }), + getAssignableTemplatesApi(), + getAssignableProvidersApi(), + ]); + + setProviders(providersRes.items); + setTemplates(templatesRes.items); + + const groupOptions = groupsRes.items.map((g) => { + const d = deserialize(g.displayName); + return { label: Lr(d.resourceName, d.name), value: g.name }; + }); + setGroups(groupOptions); + + if (definitionName) { + setIsEditModel(true); + const dto = await getApi(definitionName); + setFormModel(dto); + form.setFieldsValue(dto); + } else { + setIsEditModel(false); + const initial = { ...defaultModel }; + // If pre-filtered by group or only one group exists, auto-select + if (groupsRes.items.length === 1) initial.groupName = groupsRes.items[0].name; + else if (groupName) initial.groupName = groupName; + + setFormModel(initial); + form.setFieldsValue(initial); + } + } finally { + setLoading(false); + } + }; + + const { mutateAsync: createDef, isPending: isCreating } = useMutation({ + mutationFn: createApi, + onSuccess: (res) => { + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(res); + onClose(); + }, + }); + + const { mutateAsync: updateDef, isPending: isUpdating } = useMutation({ + mutationFn: (data: NotificationDefinitionDto) => updateApi(data.name, data), + onSuccess: (res) => { + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(res); + onClose(); + }, + }); + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + const submitData = { + ...formModel, + ...values, + extraProperties: formModel.extraProperties, + }; + + if (isEditModel) { + await updateDef(submitData); + } else { + await createDef(submitData); + } + } catch (error) { + console.error(error); + } + }; + + const handlePropChange = (prop: PropertyInfo) => { + setFormModel((prev) => ({ + ...prev, + extraProperties: { ...prev.extraProperties, [prop.key]: prop.value }, + })); + }; + + const handlePropDelete = (prop: PropertyInfo) => { + setFormModel((prev) => { + const next = { ...prev.extraProperties }; + delete next[prop.key]; + return { ...prev, extraProperties: next }; + }); + }; + + const modalTitle = isEditModel + ? `${$t("Notifications.NotificationDefinitions")} - ${formModel.name}` + : $t("Notifications.NotificationDefinitions:AddNew"); + + return ( + +
+ + + + + + + + + + + + + + + + + {$t("Notifications.DisplayName:AllowSubscriptionToClients")} + + + + + + + + + ({ label: p.name, value: p.name }))} + /> + + + +
+ ), + defaultExpandAllRows: true, + }} + request={async (params) => { + const { filter } = params; + const [groupRes, defRes] = await Promise.all([getGroupsApi({ filter }), getDefinitionsApi({ filter })]); + + const data: ExtendedGroupDto[] = groupRes.items.map((group) => { + const groupLocal = deserialize(group.displayName); + + const groupDefinitions = defRes.items + .filter((d) => d.groupName === group.name) + .map((d) => { + const dName = deserialize(d.displayName); + const dDesc = deserialize(d.description); + return { + ...d, + displayName: Lr(dName.resourceName, dName.name), + description: dDesc ? Lr(dDesc.resourceName, dDesc.name) : "", + }; + }); + + return { + ...group, + displayName: Lr(groupLocal.resourceName, groupLocal.name), + items: listToTree(groupDefinitions, { id: "name", pid: "parentName" }), + }; + }); + + return { + data: data, + success: true, + total: groupRes.items.length, + }; + }} + pagination={false} + /> + + setDefinitionModalVisible(false)} + onChange={() => actionRef.current?.reload()} + /> + + setSendModalVisible(false)} + /> + + ); +}; + +export default NotificationDefinitionTable; diff --git a/apps/react-admin/src/pages/management/notifications/definitions/notifications/notification-send-modal.tsx b/apps/react-admin/src/pages/management/notifications/definitions/notifications/notification-send-modal.tsx new file mode 100644 index 000000000..0d67273bf --- /dev/null +++ b/apps/react-admin/src/pages/management/notifications/definitions/notifications/notification-send-modal.tsx @@ -0,0 +1,132 @@ +import type React from "react"; +import { useEffect, useState } from "react"; +import { Modal, Form, Input, Select, Tabs } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { sendNotiferApi } from "@/api/management/notifications/notifications"; +import type { NotificationDefinitionDto } from "#/notifications/definitions"; +import { NotificationContentType } from "#/notifications"; +import PropertyTable from "@/components/abp/properties/property-table"; +import { useEnumMaps } from "./use-enum-maps"; + +// import MarkdownEditor from "@/components/markdown-editor"; //TODO + +interface Props { + visible: boolean; + onClose: () => void; + notification?: NotificationDefinitionDto; +} + +const NotificationSendModal: React.FC = ({ visible, onClose, notification }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + const { notificationSeverityOptions } = useEnumMaps(); + + const [activeTab, setActiveTab] = useState("basic"); + const [submitting, setSubmitting] = useState(false); + const [properties, setProperties] = useState>({}); + + useEffect(() => { + if (visible && notification) { + form.resetFields(); + setProperties({}); + setActiveTab("basic"); + } + }, [visible, notification]); + + const handleSubmit = async () => { + if (!notification) return; + try { + const values = await form.validateFields(); + setSubmitting(true); + + await sendNotiferApi({ + culture: values.culture, // Add culture input if needed + data: { + description: values.description, + message: values.message, + title: values.title, + ...properties, // Merge extra properties + }, + name: notification.name, + severity: values.severity, + }); + + toast.success($t("Notifications.SendSuccessfully")); + onClose(); + } catch (error) { + console.error(error); + } finally { + setSubmitting(false); + } + }; + + const isMarkdown = notification?.contentType === NotificationContentType.Markdown; + const isWeChat = notification?.providers?.some((p) => p.toLowerCase().includes("wechat")); + + return ( + + + + + + + + + {!notification?.template && ( + <> + + + + + + {isMarkdown ? ( + + // Replace with if available. + ) : ( + + )} + + + + + + + )} + + + + + {/* State Checkers Tab */} + + + + + + + {/* Properties Tab */} { const mainColumns: ProColumns[] = [ { - title: $t("abp.sequence"), + title: "", dataIndex: "index", valueType: "index", width: 50, diff --git a/apps/react-admin/src/pages/management/settings/settings/setting-form.tsx b/apps/react-admin/src/pages/management/settings/settings/setting-form.tsx index e1e040564..5327fb71c 100644 --- a/apps/react-admin/src/pages/management/settings/settings/setting-form.tsx +++ b/apps/react-admin/src/pages/management/settings/settings/setting-form.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Card, Form, Tabs, Collapse, Input, InputNumber, DatePicker, Select, Checkbox, Button } from "antd"; +import { Card, Form, Tabs, Collapse, Input, InputNumber, DatePicker, Select, Checkbox, Button, Space } from "antd"; import { useTranslation } from "react-i18next"; import { ValueType, @@ -17,10 +17,7 @@ interface Props { submitApi: (input: SettingsUpdateInput) => Promise; onChange: (data: SettingsUpdateInput) => void; slots?: { - [key: string]: React.FC<{ - detail: SettingDetail; - onChange: (setting: SettingDetail) => void; - }>; + [key: string]: React.FC; }; } @@ -171,22 +168,28 @@ const SettingForm: React.FC = ({ getApi, submitApi, onChange, slots }) => return null; } }; - + // const ToolbarSlot = slots?.["toolbar"]; + const ToolbarSlot = slots?.toolbar; return ( 0 && ( - - ) + + {/* Render the Toolbar Slot here */} + {ToolbarSlot && } + + {settingsUpdateInput.settings.length > 0 && ( + + )} + } > diff --git a/apps/react-admin/src/pages/management/settings/settings/system-setting.tsx b/apps/react-admin/src/pages/management/settings/settings/system-setting.tsx index 977ccd911..0a322a884 100644 --- a/apps/react-admin/src/pages/management/settings/settings/system-setting.tsx +++ b/apps/react-admin/src/pages/management/settings/settings/system-setting.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Button, Input, Modal } from "antd"; +import { Button, Form, Input, Modal } from "antd"; import { useTranslation } from "react-i18next"; import type { SettingsUpdateInput } from "#/management/settings/settings"; @@ -14,6 +14,9 @@ import SettingForm from "./setting-form"; import { toast } from "sonner"; import { isEmail } from "@/utils/abp/regex"; import { useApplication } from "@/store/abpCoreStore"; +import { hasAccessByCodes } from "@/utils/abp/access-checker"; +import FeatureManagementModal from "@/components/abp/features/feature-modal"; +import { SettingOutlined } from "@ant-design/icons"; const SystemSetting: React.FC = () => { const { t: $t } = useTranslation(); @@ -21,6 +24,9 @@ const SystemSetting: React.FC = () => { const [sending, setSending] = useState(false); const application = useApplication(); + // Modal State for Feature Management + const [featureModalVisible, setFeatureModalVisible] = useState(false); + const [modal, contextHolder] = Modal.useModal(); const handleGet = async () => { const api = application?.currentTenant.isAvailable ? getTenantSettingsApi : getGlobalSettingsApi; @@ -54,28 +60,47 @@ const SystemSetting: React.FC = () => { setSending(false); } }; + const handleFeatureManage = () => { + setFeatureModalVisible(true); + }; + + // -- Slots for SettingForm -- const SendTestEmailSlot: React.FC<{ detail: any; onChange: (setting: any) => void; }> = ({ detail, onChange }) => ( - { - detail.value = e.target.value; - onChange(detail); - }} - enterButton={ - - } - /> + + { + detail.value = e.target.value; + onChange(detail); + }} + enterButton={ + + } + /> + ); + const ToolbarSlot: React.FC = () => { + if (!hasAccessByCodes(["FeatureManagement.ManageHostFeatures"])) { + return null; + } + + return ( + + ); + }; + return ( <> {contextHolder} @@ -85,8 +110,16 @@ const SystemSetting: React.FC = () => { onChange={() => {}} slots={{ "send-test-email": SendTestEmailSlot, + toolbar: ToolbarSlot, }} /> + + {/* Feature Management Modal */} + setFeatureModalVisible(false)} + providerName="T" //Fixed to 'T' + /> ); }; diff --git a/apps/react-admin/src/pages/openiddict/applications/application-table.tsx b/apps/react-admin/src/pages/openiddict/applications/application-table.tsx index edd6739fd..a8f6d26d4 100644 --- a/apps/react-admin/src/pages/openiddict/applications/application-table.tsx +++ b/apps/react-admin/src/pages/openiddict/applications/application-table.tsx @@ -10,7 +10,7 @@ import { ApplicationsPermissions } from "@/constants/openiddict/permissions"; import { AuditLogPermissions } from "@/constants/management/auditing/permissions"; import { Iconify } from "@/components/icon"; import { deleteApi, getPagedListApi } from "@/api/openiddict/applications"; -import { useFeatures } from "@/hooks/abp/use-abp-feature"; +import { useFeatures } from "@/hooks/abp/fake-hooks/use-abp-feature"; import { toast } from "sonner"; import ApplicationModal from "./application-modal"; import ApplicationSecretModal from "./application-secret-modal"; diff --git a/apps/react-admin/src/pages/openiddict/applications/uri-table.tsx b/apps/react-admin/src/pages/openiddict/applications/uri-table.tsx index 51d190502..1e0648724 100644 --- a/apps/react-admin/src/pages/openiddict/applications/uri-table.tsx +++ b/apps/react-admin/src/pages/openiddict/applications/uri-table.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Button, Popconfirm } from "antd"; +import { Button, Card, Popconfirm } from "antd"; import { DeleteOutlined, PlusOutlined } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; import { ProTable, type ProColumns } from "@ant-design/pro-table"; @@ -51,19 +51,20 @@ const UriTable: React.FC = ({ title, uris = [], onChange, onDelet return ( <> - - headerTitle={title} - columns={columns} - dataSource={dataSource} - search={false} - pagination={false} - toolBarRender={() => [ - , - ]} - /> - + + + headerTitle={title} + columns={columns} + dataSource={dataSource} + search={false} + pagination={false} + toolBarRender={() => [ + , + ]} + /> + setModalVisible(false)} diff --git a/apps/react-admin/src/pages/openiddict/scopes/scope-table.tsx b/apps/react-admin/src/pages/openiddict/scopes/scope-table.tsx index 84d1d4130..9ab5d5ece 100644 --- a/apps/react-admin/src/pages/openiddict/scopes/scope-table.tsx +++ b/apps/react-admin/src/pages/openiddict/scopes/scope-table.tsx @@ -10,7 +10,7 @@ import { ScopesPermissions } from "@/constants/openiddict/permissions"; import { AuditLogPermissions } from "@/constants/management/auditing/permissions"; import { Iconify } from "@/components/icon"; import { deleteApi, getPagedListApi } from "@/api/openiddict/scopes"; -import { useFeatures } from "@/hooks/abp/use-abp-feature"; +import { useFeatures } from "@/hooks/abp/fake-hooks/use-abp-feature"; import { toast } from "sonner"; import ScopeModal from "./scope-modal"; import { EntityChangeDrawer } from "@/components/abp/auditing/entity-change-drawer"; diff --git a/apps/react-admin/src/pages/oss/containers/container-modal.tsx b/apps/react-admin/src/pages/oss/containers/container-modal.tsx new file mode 100644 index 000000000..022ae8b73 --- /dev/null +++ b/apps/react-admin/src/pages/oss/containers/container-modal.tsx @@ -0,0 +1,60 @@ +import { useEffect, useState } from "react"; +import { Modal, Form, Input } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { createApi } from "@/api/oss/containes"; +import type { OssContainerDto } from "#/oss/containes"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (data: OssContainerDto) => void; +} + +const ContainerModal: React.FC = ({ visible, onClose, onChange }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + if (visible) { + form.resetFields(); + } + }, [visible, form]); + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + setSubmitting(true); + + const dto = await createApi(values.name); + + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(dto); + onClose(); + } catch (error) { + console.error(error); + } finally { + setSubmitting(false); + } + }; + + return ( + + + + + + + + ); +}; + +export default ContainerModal; diff --git a/apps/react-admin/src/pages/oss/containers/container-table.tsx b/apps/react-admin/src/pages/oss/containers/container-table.tsx new file mode 100644 index 000000000..6c25f2450 --- /dev/null +++ b/apps/react-admin/src/pages/oss/containers/container-table.tsx @@ -0,0 +1,155 @@ +import { useRef, useState } from "react"; +import { Button, Modal, Space } from "antd"; +import { DeleteOutlined, PlusOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { useQueryClient, useMutation } from "@tanstack/react-query"; +import { formatToDateTime } from "@/utils/abp"; +import { deleteApi, getListApi } from "@/api/oss/containes"; +import type { OssContainerDto } from "#/oss/containes"; +import { antdOrderToAbpOrder } from "@/utils/abp/sort-order"; +import ContainerModal from "./container-modal"; +import Card from "@/components/card"; + +const ContainerTable: React.FC = () => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const queryClient = useQueryClient(); + const [modal, contextHolder] = Modal.useModal(); + + const [createModalVisible, setCreateModalVisible] = useState(false); + + const { mutateAsync: deleteContainer } = useMutation({ + mutationFn: deleteApi, + onSuccess: () => { + toast.success($t("AbpUi.DeletedSuccessfully")); + queryClient.invalidateQueries({ queryKey: ["oss-containers"], exact: false }); + }, + }); + + const handleCreate = () => { + setCreateModalVisible(true); + }; + + const handleDelete = (row: OssContainerDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessageWithFormat", { 0: row.name }), + onOk: async () => { + await deleteContainer(row.name); + }, + }); + }; + + const columns: ProColumns[] = [ + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("AbpOssManagement.DisplayName:Name"), + dataIndex: "name", + sorter: true, + minWidth: 150, + hideInSearch: true, + }, + { + title: $t("AbpOssManagement.DisplayName:CreationDate"), + dataIndex: "creationDate", + sorter: true, + minWidth: 150, + hideInSearch: true, + render: (_, row) => formatToDateTime(row.creationDate), + }, + { + title: $t("AbpOssManagement.DisplayName:LastModifiedDate"), + dataIndex: "lastModifiedDate", + sorter: true, + minWidth: 150, + hideInSearch: true, + render: (_, row) => formatToDateTime(row.lastModifiedDate), + }, + { + title: $t("AbpOssManagement.DisplayName:Size"), + dataIndex: "size", + sorter: true, + minWidth: 100, + hideInSearch: true, + }, + { + title: $t("AbpUi.Actions"), + valueType: "option", + fixed: "right", + width: 150, + render: (_, record) => ( + + + + ), + }, + ]; + + return ( + <> + {contextHolder} + + + headerTitle={$t("AbpOssManagement.Containers")} + actionRef={actionRef} + rowKey="name" + columns={columns} + search={{ + labelWidth: "auto", + }} + toolBarRender={() => [ + , + ]} + request={async (params, sorter) => { + const { current, pageSize, filter } = params; + const sorting = + sorter && Object.keys(sorter).length > 0 + ? Object.keys(sorter) + .map((key) => `${key} ${antdOrderToAbpOrder(sorter[key])}`) + .join(", ") + : undefined; + // that just how this api is. + const query = await queryClient.fetchQuery({ + queryKey: ["oss-containers", params, sorter], + queryFn: () => + getListApi({ + maxResultCount: pageSize, + skipCount: ((current || 1) - 1) * (pageSize || 0), + sorting: sorting, + prefix: filter, //? + }), + }); + + return { + data: query.containers, + total: query.maxKeys, + success: true, + }; + }} + pagination={{ + defaultPageSize: 10, + showSizeChanger: true, + }} + /> + + setCreateModalVisible(false)} + onChange={() => queryClient.invalidateQueries({ queryKey: ["oss-containers"], exact: false })} + /> + + ); +}; + +export default ContainerTable; diff --git a/apps/react-admin/src/pages/oss/objects/file-list.tsx b/apps/react-admin/src/pages/oss/objects/file-list.tsx new file mode 100644 index 000000000..86c608e18 --- /dev/null +++ b/apps/react-admin/src/pages/oss/objects/file-list.tsx @@ -0,0 +1,203 @@ +import type React from "react"; +import { useRef, useState, useEffect } from "react"; +import { Button, Card, Modal, Space } from "antd"; +import { DeleteOutlined, DownloadOutlined, UploadOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { getObjectsApi } from "@/api/oss/containes"; +import { deleteApi, generateUrlApi } from "@/api/oss/objects"; +import type { OssObjectDto } from "#/oss/objects"; +import { formatToDateTime } from "@/utils/abp"; +import { hasAccessByCodes } from "@/utils/abp/access-checker"; +import { OssObjectPermissions } from "@/constants/oss/permissions"; +import FileUploadModal from "./file-upload-modal"; +import { antdOrderToAbpOrder } from "@/utils/abp/sort-order"; + +interface Props { + bucket: string; + path: string; +} + +const FileList: React.FC = ({ bucket, path }) => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const [modal, contextHolder] = Modal.useModal(); + const [uploadModalVisible, setUploadModalVisible] = useState(false); + + // Reload table when bucket or path changes + useEffect(() => { + if (bucket) { + actionRef.current?.reload(); + } + }, [bucket, path]); + + const handleUpload = () => { + setUploadModalVisible(true); + }; + + const handleDelete = (row: OssObjectDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessageWithFormat", { 0: row.name }), + onOk: async () => { + await deleteApi({ + bucket, + object: row.name, + path: row.path, + }); + toast.success($t("AbpUi.DeletedSuccessfully")); + actionRef.current?.reload(); + }, + }); + }; + + const handleDownload = async (row: OssObjectDto) => { + const url = await generateUrlApi({ + bucket, + mD5: false, + object: row.name, + path: row.path, + }); + const link = document.createElement("a"); + link.style.display = "none"; + link.href = url; + link.setAttribute("download", row.name); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const formatSize = (size?: number) => { + if (!size) return ""; + const kb = 1024; + const mb = kb * 1024; + const gb = mb * 1024; + + if (size > gb) return `${(size / gb).toFixed(2)} GB`; + if (size > mb) return `${(size / mb).toFixed(2)} MB`; + return `${(size / kb).toFixed(2)} KB`; + }; + + const columns: ProColumns[] = [ + { + title: $t("AbpOssManagement.DisplayName:Name"), + dataIndex: "name", + minWidth: 150, + sorter: true, + }, + { + title: $t("AbpOssManagement.DisplayName:FileType"), + dataIndex: "isFolder", + minWidth: 150, + sorter: true, + render: (val) => (val ? $t("AbpOssManagement.DisplayName:Folder") : $t("AbpOssManagement.DisplayName:Standard")), + }, + { + title: $t("AbpOssManagement.DisplayName:Size"), + dataIndex: "size", + minWidth: 150, + sorter: true, + render: (_, row) => formatSize(row.size), + }, + { + title: $t("AbpOssManagement.DisplayName:CreationDate"), + dataIndex: "creationDate", + minWidth: 150, + sorter: true, + render: (_, row) => formatToDateTime(row.creationDate), + }, + { + title: $t("AbpOssManagement.DisplayName:LastModifiedDate"), + dataIndex: "lastModifiedDate", + minWidth: 150, + sorter: true, + render: (_, row) => formatToDateTime(row.lastModifiedDate), + }, + { + title: $t("AbpUi.Actions"), + valueType: "option", + fixed: "right", + width: 200, + render: (_, record) => ( + + {!record.isFolder && hasAccessByCodes([OssObjectPermissions.Download]) && ( + + )} + {hasAccessByCodes([OssObjectPermissions.Delete]) && ( + + )} + + ), + }, + ]; + + return ( + <> + {contextHolder} + + + headerTitle={$t("AbpOssManagement.FileList")} + actionRef={actionRef} + rowKey="name" + columns={columns} + search={false} + toolBarRender={() => [ + path && ( + + ), + ]} + request={async (params, sorter) => { + if (!bucket) return { data: [], success: true }; + + const sorting = + sorter && Object.keys(sorter).length > 0 + ? Object.keys(sorter) + .map((key) => `${key} ${antdOrderToAbpOrder(sorter[key])}`) + .join(", ") + : undefined; + + // Note: Prefix logic handles path filtering + // Logic for path: if path is ./ or empty, prefix might be empty or root logic + const prefix = path === "./" ? "" : path; + + const res = await getObjectsApi({ + bucket, + maxResultCount: params.pageSize, + skipCount: ((params.current || 1) - 1) * (params.pageSize || 10), + prefix: prefix, + sorting, + // You might need delimiter here if listing "current folder only" + // delimiter: '/' + }); + + return { + data: res.objects, + total: res.maxKeys, + success: true, + }; + }} + pagination={{ + defaultPageSize: 10, + showSizeChanger: true, + }} + /> + + setUploadModalVisible(false)} + onFileUploaded={() => actionRef.current?.reload()} + /> + + ); +}; + +export default FileList; diff --git a/apps/react-admin/src/pages/oss/objects/file-upload-modal.tsx b/apps/react-admin/src/pages/oss/objects/file-upload-modal.tsx new file mode 100644 index 000000000..cab1f04c4 --- /dev/null +++ b/apps/react-admin/src/pages/oss/objects/file-upload-modal.tsx @@ -0,0 +1,153 @@ +import type React from "react"; +import { useRef, useState } from "react"; +import { Modal, Button, Table, Tag, Tooltip, Progress } from "antd"; +import { DeleteOutlined, PauseOutlined, CaretRightOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { createApi } from "@/api/oss/objects"; + +interface Props { + visible: boolean; + onClose: () => void; + bucket: string; + path: string; + onFileUploaded: () => void; +} +//-------------------------TODO -------------- + +interface UploadFileItem { + id: string; + file: File; + name: string; + size: number; + progress: number; + status: "pending" | "uploading" | "paused" | "completed" | "error"; + errorMsg?: string; + xhr?: XMLHttpRequest; +} + +const FileUploadModal: React.FC = ({ visible, onClose, bucket, path, onFileUploaded }) => { + const { t: $t } = useTranslation(); + const fileInputRef = useRef(null); + const [fileList, setFileList] = useState([]); + const handleSelectFiles = () => { + fileInputRef.current?.click(); + }; + + const handleFileChange = (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + const newFiles: UploadFileItem[] = files.map((f) => ({ + id: Math.random().toString(36).substr(2, 9), + file: f, + name: f.name, + size: f.size, + progress: 0, + status: "pending", + })); + setFileList((prev) => [...prev, ...newFiles]); + // Clear input + if (fileInputRef.current) fileInputRef.current.value = ""; + }; + + // Simplified Upload Logic (Non-chunked for this example, as chunked requires complex lib integration) + // If your backend specifically requires simple-uploader.js protocol, you might need 'uploader' npm package wrapper for React. + const startUpload = (item: UploadFileItem) => { + // Logic to call createApi or custom XHR + // This uses the createApi from your previous request which uses FormData + setFileList((prev) => prev.map((f) => (f.id === item.id ? { ...f, status: "uploading" } : f))); + + createApi({ + bucket, + path, + fileName: item.name, + file: item.file, + overwrite: true, // Logic from previous code implied overwrite or new + }) + .then(() => { + setFileList((prev) => prev.map((f) => (f.id === item.id ? { ...f, status: "completed", progress: 100 } : f))); + onFileUploaded(); + }) + .catch((err) => { + setFileList((prev) => + prev.map((f) => (f.id === item.id ? { ...f, status: "error", errorMsg: err.message } : f)), + ); + }); + }; + + // In a real chunked implementation, resume/pause would manage XHR aborts/resumes. + // Here we just map the buttons to start for simplicity in this transformation scope. + + const handleAction = (item: UploadFileItem, action: "resume" | "pause" | "delete") => { + if (action === "delete") { + setFileList((prev) => prev.filter((f) => f.id !== item.id)); + } else if (action === "resume") { + startUpload(item); + } + // Pause not fully implemented in simple fetch example + }; + + const formatSize = (size: number) => { + if (size < 1024) return `${size.toFixed(0)} bytes`; + if (size < 1024 * 1024) return `${(size / 1024).toFixed(0)} KB`; + if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(1)} MB`; + return `${(size / 1024 / 1024 / 1024).toFixed(1)} GB`; + }; + + const columns = [ + { title: $t("AbpOssManagement.DisplayName:Name"), dataIndex: "name", key: "name" }, + { + title: $t("AbpOssManagement.DisplayName:Size"), + dataIndex: "size", + key: "size", + render: (size: number) => formatSize(size), + }, + { + title: $t("AbpOssManagement.DisplayName:Status"), + key: "status", + render: (_: any, record: UploadFileItem) => { + if (record.status === "completed") return {$t("AbpOssManagement.Upload:Completed")}; + if (record.status === "error") + return ( + + {$t("AbpOssManagement.Upload:Error")} + + ); + if (record.status === "uploading") return ; // Mock progress + return {$t("AbpOssManagement.Upload:Pause")}; // Pending/Paused + }, + }, + { + title: $t("AbpUi.Actions"), + key: "actions", + width: 100, + render: (_: any, record: UploadFileItem) => ( +
+ {record.status !== "completed" && record.status !== "uploading" && ( +
+ ), + }, + ]; + + return ( + +
+ + +
+
+ + ); +}; + +export default FileUploadModal; diff --git a/apps/react-admin/src/pages/oss/objects/folder-modal.tsx b/apps/react-admin/src/pages/oss/objects/folder-modal.tsx new file mode 100644 index 000000000..3267c8bb1 --- /dev/null +++ b/apps/react-admin/src/pages/oss/objects/folder-modal.tsx @@ -0,0 +1,67 @@ +import { useEffect, useState } from "react"; +import { Modal, Form, Input } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { createApi } from "@/api/oss/objects"; +import type { OssObjectDto } from "#/oss/objects"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (data: OssObjectDto) => void; + bucket: string; + path?: string; +} + +const FolderModal: React.FC = ({ visible, onClose, onChange, bucket, path }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + if (visible) { + form.resetFields(); + } + }, [visible, form]); + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + setSubmitting(true); + + const dto = await createApi({ + bucket: bucket, + fileName: values.name, + overwrite: false, + path: path, + }); + + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(dto); + onClose(); + } catch (error) { + console.error(error); + } finally { + setSubmitting(false); + } + }; + + return ( + +
+ + + + +
+ ); +}; + +export default FolderModal; diff --git a/apps/react-admin/src/pages/oss/objects/folder-tree.tsx b/apps/react-admin/src/pages/oss/objects/folder-tree.tsx new file mode 100644 index 000000000..30a7cf1e6 --- /dev/null +++ b/apps/react-admin/src/pages/oss/objects/folder-tree.tsx @@ -0,0 +1,210 @@ +import type React from "react"; +import { useState } from "react"; +import { Card, Select, Button, Tree, Empty, Spin } from "antd"; +import { useTranslation } from "react-i18next"; +import { useQuery } from "@tanstack/react-query"; +import { getListApi as getContainersApi, getObjectsApi } from "@/api/oss/containes"; +import FolderModal from "./folder-modal"; + +const { DirectoryTree } = Tree; + +interface FolderNode { + key: string; + title: string; + isLeaf?: boolean; + children?: FolderNode[]; + dataRef?: { path?: string; name: string }; +} + +interface Props { + onBucketChange: (bucket: string) => void; + onFolderChange: (path: string) => void; +} + +const FolderTree: React.FC = ({ onBucketChange, onFolderChange }) => { + const { t: $t } = useTranslation(); + + // -- State -- + const [bucket, setBucket] = useState(""); + const [treeData, setTreeData] = useState([]); + + // Controlled Tree State + const [expandedKeys, setExpandedKeys] = useState([]); + const [selectedKeys, setSelectedKeys] = useState([]); + const [loadedKeys, setLoadedKeys] = useState([]); + + // Modal State + const [modalVisible, setModalVisible] = useState(false); + const [selectedPathForCreate, setSelectedPathForCreate] = useState(""); + + // -- 1. React Query for Containers -- + const { data: containerData, isLoading: isContainersLoading } = useQuery({ + queryKey: ["oss-containers-list"], + queryFn: () => getContainersApi({ maxResultCount: 1000 }), + }); + + const containers = containerData?.containers || []; + + // -- Helpers -- + const rootNode: FolderNode = { + key: "./", + title: $t("AbpOssManagement.Objects:Root"), + isLeaf: false, + dataRef: { path: "", name: "./" }, + children: [], + }; + + // 3. FIX: Completely reset tree when bucket changes + const handleBucketChange = (val: string) => { + setBucket(val); + onBucketChange(val); + + // Reset ALL list states to prevent stale paths + setTreeData([rootNode]); + setExpandedKeys([]); + setLoadedKeys([]); + setSelectedKeys([]); + setSelectedPathForCreate(""); // Reset create path + onFolderChange(""); // Reset file list path + }; + + const getFolders = async (bucketName: string, prefix: string) => { + const { objects } = await getObjectsApi({ + bucket: bucketName, + delimiter: "/", + maxResultCount: 1000, + prefix: prefix, + }); + return objects + .filter((f) => f.isFolder) + .map((folder) => ({ + key: `${folder.path || ""}${folder.name}`, + title: folder.name, + isLeaf: false, + dataRef: folder, + children: [], + })); + }; + + const updateTreeData = (list: FolderNode[], key: React.Key, children: FolderNode[]): FolderNode[] => { + return list.map((node) => { + if (node.key === key) { + return { ...node, children }; + } + if (node.children) { + return { ...node, children: updateTreeData(node.children, key, children) }; + } + return node; + }); + }; + + // -- Tree Event Handlers -- + + const onLoadData = async ({ key, dataRef }: any) => { + if (!bucket) return; + + let path = ""; + if (dataRef?.path) path += dataRef.path; + if (dataRef?.name && dataRef.name !== "./") path += dataRef.name; + + try { + const childFolders = await getFolders(bucket, path); + setTreeData((origin) => updateTreeData(origin, key, childFolders)); + setLoadedKeys((prev) => [...prev, key]); + } catch (error) { + console.error(error); + setTreeData((origin) => updateTreeData(origin, key, [])); + } + }; + + const onExpand = (keys: React.Key[], info: any) => { + setExpandedKeys(keys); + if (!info.expanded) { + const nodeKey = info.node.key; + setLoadedKeys((prev) => prev.filter((k) => k !== nodeKey)); + } + }; + + const onSelect = (keys: React.Key[], info: any) => { + setSelectedKeys(keys); + if (keys.length === 1) { + const keyStr = keys[0].toString(); + // 1. Determine Path + const nodePath = keyStr === "./" ? "" : keyStr; + + // 2. Pass path to parent (File List) + onFolderChange(nodePath); + + // 3. FIX: Store specific path for "Create Folder" modal + setSelectedPathForCreate(nodePath); + } + }; + + const handleCreateFolder = () => { + // If nothing selected, it stays empty (root), which is handled by default state + setModalVisible(true); + }; + + const handleFolderCreated = () => { + handleBucketChange(bucket); // TODO just re-fetch entire tree for simplicity parent's other children (file-list.tsx) should also refresh (1), and the reverse is also true (2) and the same applies in reverse. + }; + + return ( + <> + +
+ {isContainersLoading ? ( +
+ +
+ ) : ( + ; + case ValueType.String: + return ; + default: + return ; + } + }; + + const modalTitle = item ? `${$t("AppPlatform.Data:EditItem")} - ${item.name}` : $t("AppPlatform.Data:AppendItem"); + + return ( + +
+ {/* Helper hidden field for ID if needed, though we use 'item' prop logic */} + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default DataDictionaryModal; diff --git a/apps/react-admin/src/pages/platform/data-dictionaries/data-dictionary-table.tsx b/apps/react-admin/src/pages/platform/data-dictionaries/data-dictionary-table.tsx new file mode 100644 index 000000000..de79c00c0 --- /dev/null +++ b/apps/react-admin/src/pages/platform/data-dictionaries/data-dictionary-table.tsx @@ -0,0 +1,181 @@ +import type React from "react"; +import { useRef, useState } from "react"; +import { Button, Dropdown, Space, Modal, Card } from "antd"; +import { PlusOutlined, EditOutlined, DeleteOutlined, EllipsisOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { deleteApi, getAllApi } from "@/api/platform/data-dictionaries"; +import type { DataDto } from "#/platform/data-dictionaries"; +import { DataDictionaryPermissions } from "@/constants/platform/permissions"; +import { hasAccessByCodes, withAccessChecker } from "@/utils/abp/access-checker"; +import { Iconify } from "@/components/icon"; + +import DataDictionaryModal from "./data-dictionary-modal"; +import DataDictionaryItemDrawer from "./data-dictionary-item-drawer"; +import { listToTree } from "@/utils/tree"; + +const DataDictionaryTable: React.FC = () => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const [modal, contextHolder] = Modal.useModal(); + + // State + const [modalVisible, setModalVisible] = useState(false); + const [drawerVisible, setDrawerVisible] = useState(false); + const [selectedData, setSelectedData] = useState(); + const [selectedParentId, setSelectedParentId] = useState(); + + // Handlers + const handleCreate = () => { + setSelectedData(undefined); + setSelectedParentId(undefined); + setModalVisible(true); + }; + + const handleUpdate = (record: DataDto) => { + setSelectedData(record); + setSelectedParentId(undefined); + setModalVisible(true); + }; + + const handleAddChild = (record: DataDto) => { + setSelectedData(undefined); + setSelectedParentId(record.id); + setModalVisible(true); + }; + + const handleManageItems = (record: DataDto) => { + setSelectedData(record); + setDrawerVisible(true); + }; + + const handleDelete = (record: DataDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessage"), + onOk: async () => { + await deleteApi(record.id); + toast.success($t("AbpUi.DeletedSuccessfully")); + actionRef.current?.reload(); + }, + }); + }; + + const columns: ProColumns[] = [ + { + title: $t("AppPlatform.DisplayName:Name"), + dataIndex: "name", + width: 200, + render: (_, record) => ( + + ), + }, + { + title: $t("AppPlatform.DisplayName:DisplayName"), + dataIndex: "displayName", + width: 200, + }, + { + title: $t("AppPlatform.DisplayName:Description"), + dataIndex: "description", + ellipsis: true, + }, + { + title: $t("AbpUi.Actions"), + key: "actions", + fixed: "right", + width: 250, + render: (_, record) => ( + + {withAccessChecker( + , + [DataDictionaryPermissions.Update], + )} + {withAccessChecker( + , + [DataDictionaryPermissions.Delete], + )} + , + onClick: () => handleAddChild(record), + disabled: !hasAccessByCodes([DataDictionaryPermissions.Create]), + }, + { + key: "items", + label: $t("AppPlatform.Data:Items"), + icon: , + onClick: () => handleManageItems(record), + disabled: !hasAccessByCodes([DataDictionaryPermissions.ManageItems]), + }, + ], + }} + > + , + [DataDictionaryPermissions.Create], + ), + ]} + request={async () => { + const { items } = await getAllApi(); + const tree = listToTree(items, { id: "id", pid: "parentId" }); + return { + data: tree, + success: true, + }; + }} + expandable={{ + defaultExpandAllRows: true, + }} + /> + + setModalVisible(false)} + onChange={() => actionRef.current?.reload()} + dataId={selectedData?.id} + parentId={selectedParentId} + /> + + setDrawerVisible(false)} + dataDto={selectedData} + /> + + ); +}; + +export default DataDictionaryTable; diff --git a/apps/react-admin/src/pages/platform/layouts/layout-modal.tsx b/apps/react-admin/src/pages/platform/layouts/layout-modal.tsx new file mode 100644 index 000000000..26bd40b48 --- /dev/null +++ b/apps/react-admin/src/pages/platform/layouts/layout-modal.tsx @@ -0,0 +1,150 @@ +import type React from "react"; +import { useEffect, useState } from "react"; +import { Modal, Form, Input } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { createApi, updateApi, getApi } from "@/api/platform/layouts"; +import { + getByNameApi as getDataDictionaryByNameApi, + getAllApi as getDataDictionariesApi, +} from "@/api/platform/data-dictionaries"; +import type { LayoutDto, LayoutCreateDto, LayoutUpdateDto } from "#/platform/layouts"; +import { listToTree } from "@/utils/tree"; +import ApiSelect from "@/components/abp/adapter/api-select"; +import ApiTreeSelect from "@/components/abp/adapter/api-tree-select"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (data: LayoutDto) => void; + layoutId?: string; +} + +const LayoutModal: React.FC = ({ visible, onClose, onChange, layoutId }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + const [submitting, setSubmitting] = useState(false); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (visible) { + form.resetFields(); + if (layoutId) { + fetchLayout(layoutId); + } + } + }, [visible, layoutId]); + + const fetchLayout = async (id: string) => { + try { + setLoading(true); + const dto = await getApi(id); + form.setFieldsValue(dto); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + setSubmitting(true); + + let res: LayoutDto; + if (layoutId) { + res = await updateApi(layoutId, values as LayoutUpdateDto); + } else { + res = await createApi(values as LayoutCreateDto); + } + + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(res); + onClose(); + } catch (error) { + console.error(error); + } finally { + setSubmitting(false); + } + }; + + const uiFrameworkApi = () => getDataDictionaryByNameApi("UI Framework"); + + const constraintsApi = async () => { + const { items } = await getDataDictionariesApi(); + return listToTree(items, { + id: "id", + pid: "parentId", + }); + }; + + const modalTitle = layoutId + ? `${$t("AppPlatform.Layout:Edit")} - ${form.getFieldValue("displayName") || ""}` + : $t("AppPlatform.Layout:AddNew"); + + return ( + +
+ {!layoutId && ( + <> + + + + + + + + + )} + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default LayoutModal; diff --git a/apps/react-admin/src/pages/platform/layouts/layout-table.tsx b/apps/react-admin/src/pages/platform/layouts/layout-table.tsx new file mode 100644 index 000000000..8d2f4d780 --- /dev/null +++ b/apps/react-admin/src/pages/platform/layouts/layout-table.tsx @@ -0,0 +1,192 @@ +import type React from "react"; +import { useRef, useState } from "react"; +import { Button, Space, Modal, Card } from "antd"; +import { PlusOutlined, EditOutlined, DeleteOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deleteApi, getPagedListApi } from "@/api/platform/layouts"; +import type { LayoutDto } from "#/platform/layouts"; +import { LayoutPermissions } from "@/constants/platform/permissions"; +import { hasAccessByCodes, withAccessChecker } from "@/utils/abp/access-checker"; +import { antdOrderToAbpOrder } from "@/utils/abp/sort-order"; + +import LayoutModal from "./layout-modal"; + +const LayoutTable: React.FC = () => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const queryClient = useQueryClient(); + const [modal, contextHolder] = Modal.useModal(); + + const [modalVisible, setModalVisible] = useState(false); + const [selectedLayoutId, setSelectedLayoutId] = useState(); + + const { mutateAsync: deleteLayout } = useMutation({ + mutationFn: deleteApi, + onSuccess: () => { + toast.success($t("AbpUi.DeletedSuccessfully")); + queryClient.invalidateQueries({ queryKey: ["layouts"] }); + actionRef.current?.reload(); + }, + }); + + const handleCreate = () => { + setSelectedLayoutId(undefined); + setModalVisible(true); + }; + + const handleUpdate = (row: LayoutDto) => { + setSelectedLayoutId(row.id); + setModalVisible(true); + }; + + const handleDelete = (row: LayoutDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessage"), + onOk: () => deleteLayout(row.id), + }); + }; + + const columns: ProColumns[] = [ + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("AppPlatform.DisplayName:Name"), + dataIndex: "name", + width: 180, + sorter: true, + fixed: "left", + hideInSearch: true, + }, + { + title: $t("AppPlatform.DisplayName:DisplayName"), + dataIndex: "displayName", + width: 150, + sorter: true, + hideInSearch: true, + }, + { + title: $t("AppPlatform.DisplayName:Path"), + dataIndex: "path", + width: 200, + align: "center", + sorter: true, + hideInSearch: true, + }, + { + title: $t("AppPlatform.DisplayName:UIFramework"), + dataIndex: "framework", + width: 180, + sorter: true, + hideInSearch: true, + }, + { + title: $t("AppPlatform.DisplayName:Description"), + dataIndex: "description", + width: 220, + sorter: true, + hideInSearch: true, + }, + { + title: $t("AppPlatform.DisplayName:Redirect"), + dataIndex: "redirect", + width: 160, + sorter: true, + hideInSearch: true, + }, + { + title: $t("AbpUi.Actions"), + valueType: "option", + fixed: "right", + width: 150, + hideInTable: !hasAccessByCodes([LayoutPermissions.Update, LayoutPermissions.Delete]), + render: (_, record) => ( + + {withAccessChecker( + , + [LayoutPermissions.Update], + )} + {withAccessChecker( + , + [LayoutPermissions.Delete], + )} + + ), + }, + ]; + + return ( + <> + {contextHolder} + + + headerTitle={$t("AppPlatform.DisplayName:Layout")} + actionRef={actionRef} + rowKey="id" + columns={columns} + search={{ labelWidth: "auto" }} + toolBarRender={() => [ + withAccessChecker( + , + [LayoutPermissions.Create], + ), + ]} + request={async (params, sorter) => { + const { current, pageSize, ...filters } = params; + const sorting = + sorter && Object.keys(sorter).length > 0 + ? Object.keys(sorter) + .map((key) => `${key} ${antdOrderToAbpOrder(sorter[key])}`) + .join(", ") + : undefined; + + const query = await queryClient.fetchQuery({ + queryKey: ["layouts", params, sorter], + queryFn: () => + getPagedListApi({ + maxResultCount: pageSize, + skipCount: ((current || 1) - 1) * (pageSize || 0), + sorting: sorting, + filter: filters.filter, + }), + }); + + return { + data: query.items, + total: query.totalCount, + success: true, + }; + }} + pagination={{ + defaultPageSize: 10, + showSizeChanger: true, + }} + /> + + setModalVisible(false)} + onChange={async () => { + await queryClient.invalidateQueries({ queryKey: ["layouts"] }); + actionRef.current?.reload(); + }} + /> + + ); +}; + +export default LayoutTable; diff --git a/apps/react-admin/src/pages/platform/menus/menu-allot-modal.tsx b/apps/react-admin/src/pages/platform/menus/menu-allot-modal.tsx new file mode 100644 index 000000000..6bc18e999 --- /dev/null +++ b/apps/react-admin/src/pages/platform/menus/menu-allot-modal.tsx @@ -0,0 +1,160 @@ +import type React from "react"; +import { useEffect, useState } from "react"; +import { Modal, Form, Tree, TreeSelect } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { getAllApi as getAllMenusApi } from "@/api/platform/menus"; +import { getAllApi as getUserMenusApi, setMenusApi as setUserMenusApi } from "@/api/platform/user-menus"; +import { getAllApi as getRoleMenusApi, setMenusApi as setRoleMenusApi } from "@/api/platform/role-menus"; +import { getByNameApi as getDataDictionaryByNameApi } from "@/api/platform/data-dictionaries"; +import type { MenuSubject } from "./types"; +import { listToTree } from "@/utils/tree"; +import ApiSelect from "@/components/abp/adapter/api-select"; + +interface Props { + visible: boolean; + onClose: () => void; + subject: MenuSubject; + identity: string; // userId or roleName +} + +const MenuAllotModal: React.FC = ({ visible, onClose, subject, identity }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + const [submitting, setSubmitting] = useState(false); + const [treeData, setTreeData] = useState([]); + const [checkedKeys, setCheckedKeys] = useState([]); + + // Watch framework to trigger tree data reload + const framework = Form.useWatch("framework", form); + + useEffect(() => { + if (visible) { + form.resetFields(); + setTreeData([]); + setCheckedKeys([]); + } + }, [visible, identity]); + + useEffect(() => { + if (visible && framework && identity) { + loadMenus(framework); + } + }, [framework, identity, visible]); + + const loadMenus = async (fw: string) => { + try { + // Fetch all menus for the framework to build the tree structure + const allMenusRes = await getAllMenusApi({ framework: fw }); + const tree = listToTree(allMenusRes.items, { id: "id", pid: "parentId" }); + setTreeData(tree); + + // Fetch currently assigned menus for the user/role + const assignedRes = + subject === "user" + ? await getUserMenusApi({ framework: fw, userId: identity }) + : await getRoleMenusApi({ framework: fw, role: identity }); + + const assignedIds = assignedRes.items.map((item) => item.id); + setCheckedKeys(assignedIds); + + // Set startup menu + const startupMenu = assignedRes.items.find((item) => item.startup); + if (startupMenu) { + form.setFieldValue("startupMenuId", startupMenu.id); + } + } catch (error) { + console.error(error); + } + }; + + const handleCheck = (checked: React.Key[] | { checked: React.Key[]; halfChecked: React.Key[] }) => { + // AntD Tree checkStrictly returns object, normally array + if (Array.isArray(checked)) { + setCheckedKeys(checked); + } else { + setCheckedKeys(checked.checked); + } + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + setSubmitting(true); + + const payload = { + framework: values.framework, + menuIds: checkedKeys as string[], + startupMenuId: values.startupMenuId, + }; + + if (subject === "user") { + await setUserMenusApi({ ...payload, userId: identity }); + } else { + await setRoleMenusApi({ ...payload, roleName: identity }); + } + + toast.success($t("AbpUi.SavedSuccessfully")); + onClose(); + } catch (error) { + console.error(error); + } finally { + setSubmitting(false); + } + }; + + // Helper for ApiSelect + const uiFrameworkApi = () => getDataDictionaryByNameApi("UI Framework"); + + return ( + +
+ + + + + {framework && ( + <> + + + + + +
+ +
+
+ + )} + +
+ ); +}; + +export default MenuAllotModal; diff --git a/apps/react-admin/src/pages/platform/menus/menu-drawer.tsx b/apps/react-admin/src/pages/platform/menus/menu-drawer.tsx new file mode 100644 index 000000000..d210508e5 --- /dev/null +++ b/apps/react-admin/src/pages/platform/menus/menu-drawer.tsx @@ -0,0 +1,304 @@ +import type React from "react"; +import { useEffect, useState } from "react"; +import { Drawer, Form, Input, Checkbox, Button, Steps, Card, TreeSelect, Select, InputNumber, DatePicker } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import dayjs from "dayjs"; +import { createApi, updateApi, getApi } from "@/api/platform/menus"; +import { getPagedListApi as getLayoutsApi, getApi as getLayoutApi } from "@/api/platform/layouts"; +import { getApi as getDataDictionaryApi } from "@/api/platform/data-dictionaries"; +import type { MenuDto, MenuCreateDto, MenuUpdateDto } from "#/platform/menus"; +import { ValueType } from "#/platform/data-dictionaries"; +import ApiSelect from "@/components/abp/adapter/api-select"; +import IconPicker from "@/components/abp/adapter/icon-picker"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (data: MenuDto) => void; + // Passing initial state via props instead of drawerApi.getData + editMenu?: { id?: string; parentId?: string; layoutId?: string }; + rootMenus: MenuDto[]; // Tree data for Parent selection +} + +const MenuDrawer: React.FC = ({ visible, onClose, onChange, editMenu, rootMenus }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); // Basic Info + const [metaForm] = Form.useForm(); // Meta Info + + const [currentStep, setCurrentStep] = useState(0); + const [submitting, setSubmitting] = useState(false); + const [loading, setLoading] = useState(false); + const [menuMetas, setMenuMetas] = useState([]); // Dynamic fields definition + + // Watch Layout ID to load meta definitions + const layoutId = Form.useWatch("layoutId", form); + + useEffect(() => { + if (visible) { + init(); + } + }, [visible, editMenu]); + + // Load Meta definitions when Layout changes + useEffect(() => { + if (layoutId && visible) { + loadMetaDefinitions(layoutId); + } else { + setMenuMetas([]); + } + }, [layoutId, visible]); + + const init = async () => { + setCurrentStep(0); + form.resetFields(); + metaForm.resetFields(); + setMenuMetas([]); + + // If ID exists, load data (Edit Mode) + if (editMenu?.id) { + setLoading(true); + try { + const dto = await getApi(editMenu.id); + form.setFieldsValue(dto); + // Meta values will be set after meta definitions are loaded via layoutId effect + // We might need a ref or state to hold meta values temporarily + setTimeout(() => setMetaValues(dto.meta, dto.layoutId), 500); + } finally { + setLoading(false); + } + } else { + // Create Mode + form.setFieldsValue({ + parentId: editMenu?.parentId, + layoutId: editMenu?.layoutId, + }); + // Handle path prefix based on parent + if (editMenu?.parentId) { + const parent = await getApi(editMenu.parentId); + // Antd Input addonBefore logic handled in render + } + } + }; + + const loadMetaDefinitions = async (lId: string) => { + try { + setLoading(true); + const layoutDto = await getLayoutApi(lId); + // Auto-fill component path if creating new + if (!editMenu?.id) { + form.setFieldValue("component", layoutDto.path); + } + + const dataDto = await getDataDictionaryApi(layoutDto.dataId); + setMenuMetas(dataDto.items?.sort() || []); + } finally { + setLoading(false); + } + }; + + const setMetaValues = (meta: Record, lId: string) => { + // This runs after definitions are loaded + // We need to map raw values to component values (e.g. string 'true' to boolean true) + // Since setMenuMetas is async/state based, robust implementation would map inside the render or a useEffect depending on menuMetas + // Simplified logic here assuming definitions loaded: + + // We need a way to parse values based on type which is in menuMetas state. + // This is tricky with async state updates. + // For now, let's rely on the form submission to format outgoing data, + // and try to set fields directly if possible. + if (!meta) return; + + // Quick parse attempt based on known logic + const parsedValues: any = { ...meta }; + // Refinement would require iterating over menuMetas to cast types (e.g. "true" -> true) + metaForm.setFieldsValue(parsedValues); + }; + + const handleParentChange = async (val: string) => { + if (val) { + const parent = await getApi(val); + // Store parent path for display/logic + } + }; + + const onSubmit = async () => { + try { + setSubmitting(true); + const basicValues = await form.validateFields(); + const metaValues = await metaForm.validateFields(); + + // Format meta values to strings/etc as expected by backend DTO + const formattedMeta: Record = {}; + Object.keys(metaValues).forEach((key) => { + const val = metaValues[key]; + if (val === undefined || val === null) return; + + // Simple casting logic based on value type check or metadata if available + if (dayjs.isDayjs(val)) formattedMeta[key] = val.format("YYYY-MM-DD HH:mm:ss"); + else if (Array.isArray(val)) formattedMeta[key] = val.join(","); + else formattedMeta[key] = String(val); + }); + + let path = basicValues.path; + if (!path.startsWith("/")) path = `/${path}`; + // Add parent path logic if needed + + const payload = { + ...basicValues, + path, + meta: formattedMeta, + }; + + let res: MenuDto; + if (basicValues.id) { + res = await updateApi(basicValues.id, payload as MenuUpdateDto); + } else { + res = await createApi(payload as MenuCreateDto); + } + + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(res); + onClose(); + } catch (e) { + console.error(e); + } finally { + setSubmitting(false); + } + }; + + const renderMetaInput = (item: any) => { + switch (item.valueType) { + case ValueType.Boolean: + return {item.displayName}; + case ValueType.Numeic: + return ; + case ValueType.Date: + return ; + case ValueType.DateTime: + return ; + case ValueType.Array: + return ; + default: + return ; + } + }; + + const drawerTitle = editMenu?.id + ? `${$t("AppPlatform.Menu:Edit")} - ${form.getFieldValue("name") || ""}` + : $t("AppPlatform.Menu:AddNew"); + + return ( + + {currentStep === 1 && } + {currentStep === 0 && ( + + )} + {currentStep === 1 && ( + + )} +
+ } + > + + + + + + {/* Basic Form */} +
+
+ + + + getLayoutsApi({ maxResultCount: 100 })} + labelField="displayName" + valueField="id" + resultField="items" + allowClear + /> + + + + {$t("AppPlatform.DisplayName:IsPublic")} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + {/* Meta Form */} +
+
+ {menuMetas.map((item) => ( + + {renderMetaInput(item)} + + ))} + {menuMetas.length === 0 &&
No Meta Fields Defined
} + +
+ + ); +}; + +export default MenuDrawer; diff --git a/apps/react-admin/src/pages/platform/menus/menu-table.tsx b/apps/react-admin/src/pages/platform/menus/menu-table.tsx new file mode 100644 index 000000000..de049218f --- /dev/null +++ b/apps/react-admin/src/pages/platform/menus/menu-table.tsx @@ -0,0 +1,174 @@ +import type React from "react"; +import { useRef, useState } from "react"; +import { Button, Space, Modal, Card } from "antd"; +import { PlusOutlined, EditOutlined, DeleteOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { useMutation } from "@tanstack/react-query"; +import { deleteApi, getAllApi } from "@/api/platform/menus"; +import { getPagedListApi as getLayoutsApi } from "@/api/platform/layouts"; +import type { MenuDto } from "#/platform/menus"; +import { MenuPermissions } from "@/constants/platform/permissions"; +import { withAccessChecker } from "@/utils/abp/access-checker"; +import { Iconify } from "@/components/icon"; + +import MenuDrawer from "./menu-drawer"; +import { listToTree } from "@/utils/tree"; + +const MenuTable: React.FC = () => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const [modal, contextHolder] = Modal.useModal(); + + const [drawerVisible, setDrawerVisible] = useState(false); + const [editMenuState, setEditMenuState] = useState< + { id?: string; parentId?: string; layoutId?: string } | undefined + >(); + const [menuTree, setMenuTree] = useState([]); + + const { mutateAsync: deleteMenu } = useMutation({ + mutationFn: deleteApi, + onSuccess: () => { + toast.success($t("AbpUi.DeletedSuccessfully")); + actionRef.current?.reload(); + }, + }); + + const handleCreate = (row?: MenuDto) => { + setEditMenuState({ + layoutId: row?.layoutId, + parentId: row?.id, + }); + setDrawerVisible(true); + }; + + const handleUpdate = (row: MenuDto) => { + setEditMenuState({ id: row.id }); + setDrawerVisible(true); + }; + + const handleDelete = (row: MenuDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessage"), + onOk: () => deleteMenu(row.id), + }); + }; + + const columns: ProColumns[] = [ + { + title: $t("AppPlatform.DisplayName:Layout"), + dataIndex: "layoutId", + valueType: "select", + hideInTable: true, + request: async ({ keyWords }) => { + const res = await getLayoutsApi({ filter: keyWords, maxResultCount: 20 }); + return res.items.map((l) => ({ label: l.displayName, value: l.id })); + }, + fieldProps: { showSearch: true, allowClear: true }, + }, + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("AppPlatform.DisplayName:Name"), + dataIndex: "name", + width: 250, + render: (_, row) => ( + + {row.meta?.icon && } + {row.name} + + ), + }, + { + title: $t("AppPlatform.DisplayName:DisplayName"), + dataIndex: "displayName", + width: 150, + }, + { + title: $t("AppPlatform.DisplayName:Description"), + dataIndex: "description", + ellipsis: true, + }, + { + title: $t("AbpUi.Actions"), + key: "actions", + fixed: "right", + width: 220, + render: (_, record) => ( + + {withAccessChecker( + , + [MenuPermissions.Create], + )} + {withAccessChecker( + , + [MenuPermissions.Update], + )} + {withAccessChecker( + , + [MenuPermissions.Delete], + )} + + ), + }, + ]; + + return ( + <> + {contextHolder} + + + headerTitle={$t("AppPlatform.DisplayName:Menus")} + actionRef={actionRef} + rowKey="id" + columns={columns} + search={{ labelWidth: "auto" }} + toolBarRender={() => [ + withAccessChecker( + , + [MenuPermissions.Create], + ), + ]} + request={async (params) => { + const { layoutId, filter } = params; + const { items } = await getAllApi({ layoutId, filter }); + const tree = listToTree(items, { id: "id", pid: "parentId" }); + + setMenuTree(tree); // Save for drawer + + return { + data: tree, + success: true, + total: items.length, + }; + }} + expandable={{ defaultExpandAllRows: true }} + pagination={false} + /> + + setDrawerVisible(false)} + onChange={() => actionRef.current?.reload()} + editMenu={editMenuState} + rootMenus={menuTree} + /> + + ); +}; + +export default MenuTable; diff --git a/apps/react-admin/src/pages/platform/menus/types.ts b/apps/react-admin/src/pages/platform/menus/types.ts new file mode 100644 index 000000000..6fb7dd792 --- /dev/null +++ b/apps/react-admin/src/pages/platform/menus/types.ts @@ -0,0 +1,16 @@ +import type { MenuDto } from "#/platform"; + +type MenuSubject = "role" | "user"; + +type EditMenu = { + id?: string; + layoutId?: string; + parentId?: string; +}; + +type MenuDrawerState = { + editMenu?: EditMenu; + rootMenus: MenuDto[]; +}; + +export type { MenuDrawerState, MenuSubject }; diff --git a/apps/react-admin/src/pages/platform/messages/email/email-message-modal.tsx b/apps/react-admin/src/pages/platform/messages/email/email-message-modal.tsx new file mode 100644 index 000000000..7d9110a08 --- /dev/null +++ b/apps/react-admin/src/pages/platform/messages/email/email-message-modal.tsx @@ -0,0 +1,35 @@ +import type React from "react"; +import { Modal } from "antd"; +import { useTranslation } from "react-i18next"; +import Editor from "@/components/editor"; + +interface Props { + visible: boolean; + onClose: () => void; + content?: string; +} + +const EmailMessageModal: React.FC = ({ visible, onClose, content }) => { + const { t: $t } = useTranslation(); + + return ( + +
+ {content ? ( + + ) : ( + {$t("AbpUi.NoData")} + )} +
+
+ ); +}; + +export default EmailMessageModal; diff --git a/apps/react-admin/src/pages/platform/messages/email/email-message-table.tsx b/apps/react-admin/src/pages/platform/messages/email/email-message-table.tsx new file mode 100644 index 000000000..2966a7553 --- /dev/null +++ b/apps/react-admin/src/pages/platform/messages/email/email-message-table.tsx @@ -0,0 +1,252 @@ +import type React from "react"; +import { useRef, useState } from "react"; +import { Button, Tag, Space, Modal, Dropdown, Card } from "antd"; +import { EditOutlined, DeleteOutlined, EllipsisOutlined, SendOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deleteApi, getPagedListApi, sendApi } from "@/api/platform/email-messages"; +import type { EmailMessageDto } from "#/platform/messages"; +import { MessageStatus } from "#/platform/messages"; +import { EmailMessagesPermissions } from "@/constants/platform/permissions"; +import { hasAccessByCodes, withAccessChecker } from "@/utils/abp/access-checker"; +import { formatToDateTime } from "@/utils/abp"; +import { antdOrderToAbpOrder } from "@/utils/abp/sort-order"; + +import EmailMessageModal from "./email-message-modal"; + +const EmailMessageTable: React.FC = () => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const queryClient = useQueryClient(); + const [modal, contextHolder] = Modal.useModal(); + + const [modalVisible, setModalVisible] = useState(false); + const [selectedContent, setSelectedContent] = useState(""); + + // Mutations + const { mutateAsync: deleteMessage } = useMutation({ + mutationFn: deleteApi, + onSuccess: () => { + toast.success($t("AbpUi.DeletedSuccessfully")); + actionRef.current?.reload(); + }, + }); + + const { mutateAsync: sendMessage } = useMutation({ + mutationFn: sendApi, + onSuccess: () => { + toast.success($t("AppPlatform.SuccessfullySent")); + actionRef.current?.reload(); + }, + }); + + // Handlers + const handleViewContent = (row: EmailMessageDto) => { + setSelectedContent(row.content); + setModalVisible(true); + }; + + const handleDelete = (row: EmailMessageDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessage"), + onOk: () => deleteMessage(row.id), + }); + }; + + const handleSend = (row: EmailMessageDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AppPlatform.MessageWillBeReSendWarningMessage"), + onOk: () => sendMessage(row.id), + }); + }; + + const statusOptions = [ + { label: $t("AppPlatform.MessageStatus:Pending"), value: MessageStatus.Pending, color: "warning" }, + { label: $t("AppPlatform.MessageStatus:Sent"), value: MessageStatus.Sent, color: "success" }, + { label: $t("AppPlatform.MessageStatus:Failed"), value: MessageStatus.Failed, color: "error" }, + ]; + + const columns: ProColumns[] = [ + { + title: $t("AppPlatform.DisplayName:Provider"), + dataIndex: "provider", + width: 150, + sorter: true, + }, + { + title: $t("AppPlatform.DisplayName:Status"), + dataIndex: "status", + width: 100, + sorter: true, + valueType: "select", + fieldProps: { + options: statusOptions, + }, + render: (_, row) => { + const option = statusOptions.find((o) => o.value === row.status); + return {option?.label}; + }, + }, + { + title: $t("AppPlatform.DisplayName:SendTime"), + dataIndex: "sendTime", + width: 180, + sorter: true, + valueType: "dateRange", + render: (_, row) => formatToDateTime(row.sendTime), + }, + { + title: $t("AppPlatform.DisplayName:SendCount"), + dataIndex: "sendCount", + width: 100, + sorter: true, + align: "center", + hideInSearch: true, + }, + { + title: $t("AppPlatform.DisplayName:Subject"), + dataIndex: "subject", + width: 200, + sorter: true, + ellipsis: true, + hideInSearch: true, + }, + { + title: $t("AppPlatform.DisplayName:Content"), + dataIndex: "content", + width: 200, + sorter: true, + ellipsis: true, + hideInSearch: true, + }, + { + title: $t("AppPlatform.DisplayName:From"), + dataIndex: "from", + width: 200, + sorter: true, + hideInSearch: true, + }, + { + title: $t("AppPlatform.DisplayName:Receiver"), + dataIndex: "emailAddress", // Search field name is usually mapped to 'emailAddress' in input + render: (_, row) => row.receiver, + width: 200, + sorter: true, + }, + { + title: $t("AppPlatform.DisplayName:CreationTime"), + dataIndex: "creationTime", + width: 180, + sorter: true, + valueType: "date", + hideInSearch: true, + render: (_, row) => formatToDateTime(row.creationTime), + }, + { + title: $t("AppPlatform.DisplayName:Reason"), + dataIndex: "reason", + width: 150, + sorter: true, + ellipsis: true, + hideInSearch: true, + }, + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("AbpUi.Actions"), + valueType: "option", + fixed: "right", + width: 220, + render: (_, record) => ( + + {/* Reuse Edit icon for 'View Content' to match Vue 'onUpdate' behavior which opens modal */} + + + {withAccessChecker( + , + [EmailMessagesPermissions.Delete], + )} + + , + onClick: () => handleSend(record), + } + : null, + ].filter(Boolean) as any, + }} + > + , + [SmsMessagesPermissions.Delete], + )} + + , + onClick: () => handleSend(record), + } + : null, + ].filter(Boolean) as any, + }} + > +
+ setModalVisible(true)} + onDelete={handleDeleteFavorite} + onClick={handleNav} + /> + + + + + + + + setModalVisible(false)} + onChange={initFavoriteMenus} + /> + + ); +}; + +export default WorkbenchPage; diff --git a/apps/react-admin/src/pages/saas/editions/edition-modal.tsx b/apps/react-admin/src/pages/saas/editions/edition-modal.tsx new file mode 100644 index 000000000..4c0073b73 --- /dev/null +++ b/apps/react-admin/src/pages/saas/editions/edition-modal.tsx @@ -0,0 +1,85 @@ +import { useEffect, useState } from "react"; +import { Modal, Form, Input } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { createApi, updateApi, getApi } from "@/api/saas/editions"; +import type { EditionDto } from "#/saas/editions"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (data: EditionDto) => void; + editionId?: string; +} + +const EditionModal: React.FC = ({ visible, onClose, onChange, editionId }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [concurrencyStamp, setConcurrencyStamp] = useState(undefined); + + useEffect(() => { + if (visible) { + form.resetFields(); + if (editionId) { + fetchEdition(editionId); + } else { + setConcurrencyStamp(undefined); + } + } + }, [visible, editionId, form]); + + const fetchEdition = async (id: string) => { + try { + setLoading(true); + const dto = await getApi(id); + form.setFieldsValue(dto); + setConcurrencyStamp(dto.concurrencyStamp); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + setSubmitting(true); + + const api = editionId ? updateApi(editionId, { ...values, concurrencyStamp }) : createApi(values); + + const res = await api; + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(res); + onClose(); + } catch (error) { + console.error(error); + } finally { + setSubmitting(false); + } + }; + + const modalTitle = editionId + ? `${$t("AbpSaas.Editions")} - ${form.getFieldValue("displayName") || ""}` + : $t("AbpSaas.NewEdition"); + + return ( + +
+ + + + +
+ ); +}; + +export default EditionModal; diff --git a/apps/react-admin/src/pages/saas/editions/edition-table.tsx b/apps/react-admin/src/pages/saas/editions/edition-table.tsx new file mode 100644 index 000000000..e6d79f588 --- /dev/null +++ b/apps/react-admin/src/pages/saas/editions/edition-table.tsx @@ -0,0 +1,221 @@ +import { useRef, useState } from "react"; +import { Button, Card, Dropdown, Modal, Space } from "antd"; +import { EditOutlined, DeleteOutlined, PlusOutlined, EllipsisOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deleteApi, getPagedListApi } from "@/api/saas/editions"; +import type { EditionDto } from "#/saas/editions"; +import { hasAccessByCodes, withAccessChecker } from "@/utils/abp/access-checker"; +import { EditionsPermissions } from "@/constants/saas/permissions"; // Assuming constants location +import { AuditLogPermissions } from "@/constants/management/auditing/permissions"; + +import { Iconify } from "@/components/icon"; +import { antdOrderToAbpOrder } from "@/utils/abp/sort-order"; + +import EditionModal from "./edition-modal"; +import FeatureModal from "@/components/abp/features/feature-modal"; +import { useFeatures } from "@/hooks/abp/fake-hooks/use-abp-feature"; +import { EntityChangeDrawer } from "@/components/abp/auditing/entity-change-drawer"; + +const EditionTable: React.FC = () => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const queryClient = useQueryClient(); + const [modal, contextHolder] = Modal.useModal(); + const featureChecker = useFeatures(); + + // Modal States + const [editionModalVisible, setEditionModalVisible] = useState(false); + const [featureModalVisible, setFeatureModalVisible] = useState(false); + const [entityChangeDrawerVisible, setEntityChangeDrawerVisible] = useState(false); + + const [selectedEditionId, setSelectedEditionId] = useState(); + const [selectedEditionName, setSelectedEditionName] = useState(); + + const { mutateAsync: deleteEdition } = useMutation({ + mutationFn: deleteApi, + onSuccess: async () => { + toast.success($t("AbpUi.DeletedSuccessfully")); + await queryClient.invalidateQueries({ queryKey: ["editions"], exact: false }); + actionRef.current?.reload(); + }, + }); + + const handleCreate = () => { + setSelectedEditionId(undefined); + setEditionModalVisible(true); + }; + + const handleUpdate = (row: EditionDto) => { + setSelectedEditionId(row.id); + setEditionModalVisible(true); + }; + + const handleDelete = (row: EditionDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpSaas.EditionDeletionConfirmationMessage", { 0: row.displayName }), + onOk: async () => { + await deleteEdition(row.id); + }, + }); + }; + + const handleMenuClick = (key: string, row: EditionDto) => { + if (key === "features") { + setSelectedEditionId(row.id); + setSelectedEditionName(row.displayName); + setFeatureModalVisible(true); + } else if (key === "entity-changes") { + setSelectedEditionId(row.id); + setSelectedEditionName(row.displayName); + setEntityChangeDrawerVisible(true); + } + }; + + const columns: ProColumns[] = [ + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("AbpSaas.DisplayName:EditionName"), + dataIndex: "displayName", + sorter: true, + hideInSearch: true, + }, + { + title: $t("AbpUi.Actions"), + valueType: "option", + fixed: "right", + width: 220, + hideInTable: !hasAccessByCodes([EditionsPermissions.Update, EditionsPermissions.Delete]), + render: (_, record) => ( + + {withAccessChecker( + , + [EditionsPermissions.Update], + )} + {withAccessChecker( + , + [EditionsPermissions.Delete], + )} + + , + } + : null, + hasAccessByCodes([EditionsPermissions.ManageFeatures]) + ? { + key: "features", + label: $t("AbpSaas.ManageFeatures"), + icon: , + } + : null, + ].filter(Boolean) as any, + onClick: ({ key }) => handleMenuClick(key, record), + }} + > + , + [EditionsPermissions.Create], + ), + ]} + request={async (params, sorter) => { + const { current, pageSize, ...filters } = params; + const sorting = + sorter && Object.keys(sorter).length > 0 + ? Object.keys(sorter) + .map((key) => `${key} ${antdOrderToAbpOrder(sorter[key])}`) + .join(", ") + : undefined; + + const query = await queryClient.fetchQuery({ + queryKey: ["editions", params, sorter], + queryFn: () => + getPagedListApi({ + maxResultCount: pageSize, + skipCount: ((current || 1) - 1) * (pageSize || 0), + sorting: sorting, + filter: filters.filter, + }), + }); + + return { + data: query.items, + total: query.totalCount, + success: true, + }; + }} + pagination={{ + defaultPageSize: 10, + showSizeChanger: true, + }} + /> + + setEditionModalVisible(false)} + onChange={async () => { + await queryClient.invalidateQueries({ queryKey: ["editions"], exact: false }); + actionRef.current?.reload(); + }} + /> + + setFeatureModalVisible(false)} + /> + + setEntityChangeDrawerVisible(false)} + /> + + ); +}; + +export default EditionTable; diff --git a/apps/react-admin/src/pages/saas/tenants/tenant-connection-string-modal.tsx b/apps/react-admin/src/pages/saas/tenants/tenant-connection-string-modal.tsx new file mode 100644 index 000000000..35dfdc67d --- /dev/null +++ b/apps/react-admin/src/pages/saas/tenants/tenant-connection-string-modal.tsx @@ -0,0 +1,75 @@ +import type React from "react"; +import { useEffect, useState } from "react"; +import { Modal, Form, Input, Select } from "antd"; +import { useTranslation } from "react-i18next"; +import { checkConnectionString } from "@/api/saas/tenants"; +import type { TenantConnectionStringDto } from "#/saas/tenants"; + +interface Props { + visible: boolean; + onClose: () => void; + onSubmit: (data: TenantConnectionStringDto) => Promise; + data?: TenantConnectionStringDto; + dataBaseOptions: { label: string; value: string }[]; +} + +const TenantConnectionStringModal: React.FC = ({ visible, onClose, onSubmit, data, dataBaseOptions }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + if (visible) { + form.resetFields(); + if (data) { + form.setFieldsValue({ + provider: "MySql", // Default fallback from Vue code + ...data, + }); + } else { + form.setFieldsValue({ provider: "MySql" }); + } + } + }, [visible, data, form]); + + const handleOk = async () => { + try { + const values = await form.validateFields(); + setSubmitting(true); + + // Validate connection string on server + await checkConnectionString({ + connectionString: values.value, + name: values.name, + provider: values.provider, + }); + + await onSubmit(values); + onClose(); + } catch (error) { + console.error(error); + } finally { + setSubmitting(false); + } + }; + + const title = data?.name ? `${$t("AbpSaas.ConnectionStrings")} - ${data.name}` : $t("AbpSaas.ConnectionStrings"); + + return ( + +
+ + + + + + + +
+ ); +}; + +export default TenantConnectionStringModal; diff --git a/apps/react-admin/src/pages/saas/tenants/tenant-connection-strings-list.tsx b/apps/react-admin/src/pages/saas/tenants/tenant-connection-strings-list.tsx new file mode 100644 index 000000000..e86bf4ade --- /dev/null +++ b/apps/react-admin/src/pages/saas/tenants/tenant-connection-strings-list.tsx @@ -0,0 +1,93 @@ +import type React from "react"; +import { useState } from "react"; +import { Button, Table, Space, Popconfirm } from "antd"; +import { EditOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import type { TenantConnectionStringDto } from "#/saas/tenants"; +import TenantConnectionStringModal from "./tenant-connection-string-modal"; + +interface Props { + data: TenantConnectionStringDto[]; + dataBaseOptions: { label: string; value: string }[]; + onAdd: (item: TenantConnectionStringDto) => Promise; + onDelete: (item: TenantConnectionStringDto) => Promise; + loading?: boolean; +} + +const TenantConnectionStringsList: React.FC = ({ data, dataBaseOptions, onAdd, onDelete, loading = false }) => { + const { t: $t } = useTranslation(); + const [modalVisible, setModalVisible] = useState(false); + const [editingItem, setEditingItem] = useState(); + + const handleCreate = () => { + setEditingItem(undefined); + setModalVisible(true); + }; + + const handleEdit = (record: TenantConnectionStringDto) => { + setEditingItem(record); + setModalVisible(true); + }; + + const columns = [ + { + title: $t("AbpSaas.DisplayName:Name"), + dataIndex: "name", + width: 150, + }, + { + title: $t("AbpSaas.DisplayName:Value"), + dataIndex: "value", + ellipsis: true, + }, + { + title: $t("AbpUi.Actions"), + width: 150, + align: "center" as const, + render: (_: any, record: TenantConnectionStringDto) => ( + + + onDelete(record)} + > + + + + ), + }, + ]; + + return ( +
+
+ +
+
+ setModalVisible(false)} + onSubmit={onAdd} + /> + + ); +}; + +export default TenantConnectionStringsList; diff --git a/apps/react-admin/src/pages/saas/tenants/tenant-connection-strings-modal.tsx b/apps/react-admin/src/pages/saas/tenants/tenant-connection-strings-modal.tsx new file mode 100644 index 000000000..476c67bd8 --- /dev/null +++ b/apps/react-admin/src/pages/saas/tenants/tenant-connection-strings-modal.tsx @@ -0,0 +1,74 @@ +import type React from "react"; +import { useEffect, useState } from "react"; +import { Modal } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { getConnectionStringsApi, setConnectionStringApi, deleteConnectionStringApi } from "@/api/saas/tenants"; +import type { TenantConnectionStringDto, TenantDto } from "#/saas/tenants"; +import TenantConnectionStringsList from "./tenant-connection-strings-list"; + +interface Props { + visible: boolean; + onClose: () => void; + tenant?: TenantDto; + dataBaseOptions: { label: string; value: string }[]; +} + +const TenantConnectionStringsModal: React.FC = ({ visible, onClose, tenant, dataBaseOptions }) => { + const { t: $t } = useTranslation(); + const [connectionStrings, setConnectionStrings] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (visible && tenant) { + fetchData(tenant.id); + } else { + setConnectionStrings([]); + } + }, [visible, tenant]); + + const fetchData = async (id: string) => { + try { + setLoading(true); + const { items } = await getConnectionStringsApi(id); + setConnectionStrings(items); + } finally { + setLoading(false); + } + }; + + const handleUpdate = async (data: TenantConnectionStringDto) => { + if (!tenant) return; + await setConnectionStringApi(tenant.id, data); + toast.success($t("AbpUi.SavedSuccessfully")); + await fetchData(tenant.id); + }; + + const handleDelete = async (data: TenantConnectionStringDto) => { + if (!tenant) return; + await deleteConnectionStringApi(tenant.id, data.name); + toast.success($t("AbpUi.DeletedSuccessfully")); + await fetchData(tenant.id); + }; + + return ( + + + + ); +}; + +export default TenantConnectionStringsModal; diff --git a/apps/react-admin/src/pages/saas/tenants/tenant-modal.tsx b/apps/react-admin/src/pages/saas/tenants/tenant-modal.tsx new file mode 100644 index 000000000..d14c50d1f --- /dev/null +++ b/apps/react-admin/src/pages/saas/tenants/tenant-modal.tsx @@ -0,0 +1,228 @@ +import type React from "react"; +import { useEffect, useState } from "react"; +import { Modal, Form, Input, Select, Checkbox, DatePicker, Tabs, Row, Col } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import dayjs from "dayjs"; +import { createApi, updateApi, getApi, checkConnectionString } from "@/api/saas/tenants"; +import { getPagedListApi as getEditionsApi } from "@/api/saas/editions"; +import type { TenantDto, TenantConnectionStringDto, TenantCreateDto } from "#/saas/tenants"; +import type { EditionDto } from "#/saas/editions"; +import TenantConnectionStringsList from "./tenant-connection-strings-list"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (data: TenantDto) => void; + tenantId?: string; + dataBaseOptions: { label: string; value: string }[]; +} + +const TenantModal: React.FC = ({ visible, onClose, onChange, tenantId, dataBaseOptions }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + + const [activeTab, setActiveTab] = useState("basic"); + const [loading, setLoading] = useState(false); + const [submitting, setSubmitting] = useState(false); + + const [editions, setEditions] = useState([]); + // Local state for connection strings when creating a new tenant + const [localConnectionStrings, setLocalConnectionStrings] = useState([]); + + useEffect(() => { + if (visible) { + setActiveTab("basic"); + form.resetFields(); + setLocalConnectionStrings([]); + fetchEditions(); + + if (tenantId) { + fetchTenant(tenantId); + } else { + form.setFieldsValue({ + isActive: true, + useSharedDatabase: true, + }); + } + } + }, [visible, tenantId]); + + const fetchTenant = async (id: string) => { + try { + setLoading(true); + const dto = await getApi(id); + form.setFieldsValue({ + ...dto, + enableTime: dto.enableTime ? dayjs(dto.enableTime) : null, + disableTime: dto.disableTime ? dayjs(dto.disableTime) : null, + }); + } finally { + setLoading(false); + } + }; + + const fetchEditions = async () => { + const { items } = await getEditionsApi({ maxResultCount: 100 }); + setEditions(items); + }; + + const handleNameChange = (e: React.ChangeEvent) => { + const name = e.target.value; + // Auto-fill admin email if creating and email not manually modified (simplified logic) + if (!tenantId) { + const currentEmail = form.getFieldValue("adminEmailAddress"); + if (!currentEmail || currentEmail.includes("@")) { + form.setFieldValue("adminEmailAddress", `admin@${name || "domain"}.com`); + } + } + }; + + const handleLocalConnectionAdd = async (data: TenantConnectionStringDto) => { + setLocalConnectionStrings((prev) => { + const exists = prev.find((x) => x.name === data.name); + if (exists) { + return prev.map((x) => (x.name === data.name ? { ...x, value: data.value } : x)); + } + return [...prev, data]; + }); + }; + + const handleLocalConnectionDelete = async (data: TenantConnectionStringDto) => { + setLocalConnectionStrings((prev) => prev.filter((x) => x.name !== data.name)); + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + setSubmitting(true); + + // Check default connection string if not shared + if (!tenantId && !values.useSharedDatabase) { + await checkConnectionString({ + connectionString: values.defaultConnectionString, + provider: values.provider, + }); + } + + const submitData = { + ...values, + enableTime: values.enableTime ? values.enableTime.format("YYYY-MM-DD") : null, + disableTime: values.disableTime ? values.disableTime.format("YYYY-MM-DD") : null, + }; + + // Add local connection strings if creating + if (!tenantId) { + (submitData as TenantCreateDto).connectionStrings = localConnectionStrings; + } + + const api = tenantId ? updateApi(tenantId, submitData) : createApi(submitData); + + const res = await api; + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(res); + onClose(); + } catch (e) { + console.error(e); + } finally { + setSubmitting(false); + } + }; + + // Watch for useSharedDatabase to conditionally render tab content + const useSharedDatabase = Form.useWatch("useSharedDatabase", form); + + return ( + +
+ + {$t("AbpSaas.DisplayName:IsActive")} + + + + + + + {!tenantId && ( + <> + + + + + + + + )} + + + + + + + + +
+

{$t("AbpSaas.ConnectionStrings")}

+ +
+ + )} + +
+ ); +}; + +export default TenantModal; diff --git a/apps/react-admin/src/pages/saas/tenants/tenant-switch.tsx b/apps/react-admin/src/pages/saas/tenants/tenant-switch.tsx new file mode 100644 index 000000000..7892a9c8f --- /dev/null +++ b/apps/react-admin/src/pages/saas/tenants/tenant-switch.tsx @@ -0,0 +1,95 @@ +import type React from "react"; +import { useState } from "react"; +import { Button, Input, Modal, Form } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { findTenantByNameApi } from "@/api/saas/multi-tenancy"; +import useAbpStore, { useApplication } from "@/store/abpCoreStore"; + +interface Props { + onChange: () => void; +} + +const TenantSwitch: React.FC = ({ onChange }) => { + const { t: $t } = useTranslation(); + const abpStore = useAbpStore(); + const application = useApplication(); + const currentTenant = application?.currentTenant; + + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [submitting, setSubmitting] = useState(false); + + const handleOpen = () => { + form.setFieldsValue({ name: currentTenant?.name }); + setVisible(true); + }; + + const handleSwitch = async () => { + try { + const values = await form.validateFields(); + setSubmitting(true); + + let tenantId: string | undefined = undefined; + // Clear tenant cookie to avoid conflicts + // cookies.remove('__tenant'); + document.cookie = "__tenant=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; + + if (values.name) { + const result = await findTenantByNameApi(values.name); + + if (!result.success) { + toast.warning($t("AbpUiMultiTenancy.GivenTenantIsNotExist", { 0: values.name })); + setSubmitting(false); + return; + } + + if (!result.isActive) { + toast.warning($t("AbpUiMultiTenancy.GivenTenantIsNotAvailable", { 0: values.name })); + setSubmitting(false); + return; + } + tenantId = result.tenantId; + } + + abpStore.actions.setTenantId(tenantId); + // console.log("Switching to tenant:", tenantId); + onChange(); + + setVisible(false); + } catch (e) { + console.error(e); + } finally { + setSubmitting(false); + } + }; + + return ( +
+ ({$t("AbpUiMultiTenancy.Switch")})} + onSearch={handleOpen} + /> + + setVisible(false)} + onOk={handleSwitch} + confirmLoading={submitting} + destroyOnClose + > +
+ + + + +
+
+ ); +}; + +export default TenantSwitch; diff --git a/apps/react-admin/src/pages/saas/tenants/tenant-table.tsx b/apps/react-admin/src/pages/saas/tenants/tenant-table.tsx new file mode 100644 index 000000000..7314743df --- /dev/null +++ b/apps/react-admin/src/pages/saas/tenants/tenant-table.tsx @@ -0,0 +1,263 @@ +import type React from "react"; +import { useRef, useState } from "react"; +import { Button, Card, Dropdown, Modal, Space } from "antd"; +import { + EditOutlined, + DeleteOutlined, + PlusOutlined, + EllipsisOutlined, + CheckOutlined, + CloseOutlined, +} from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deleteApi, getPagedListApi } from "@/api/saas/tenants"; +import type { TenantDto } from "#/saas/tenants"; +import { hasAccessByCodes, withAccessChecker } from "@/utils/abp/access-checker"; +import { TenantsPermissions } from "@/constants/saas/permissions"; // Adjust paths +import { Iconify } from "@/components/icon"; +import { antdOrderToAbpOrder } from "@/utils/abp/sort-order"; + +import TenantModal from "./tenant-modal"; +import TenantConnectionStringsModal from "./tenant-connection-strings-modal"; +import FeatureModal from "@/components/abp/features/feature-modal"; +import { useFeatures } from "@/hooks/abp/fake-hooks/use-abp-feature"; +import { AuditLogPermissions } from "@/constants/management/auditing"; +import { EntityChangeDrawer } from "@/components/abp/auditing/entity-change-drawer"; + +const TenantTable: React.FC = () => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const queryClient = useQueryClient(); + const featureChecker = useFeatures(); + + const [modal, contextHolder] = Modal.useModal(); + // Modal/Drawer States + const [tenantModalVisible, setTenantModalVisible] = useState(false); + const [connStrModalVisible, setConnStrModalVisible] = useState(false); + const [featureModalVisible, setFeatureModalVisible] = useState(false); + const [entityChangeVisible, setEntityChangeVisible] = useState(false); + + const [selectedTenantId, setSelectedTenantId] = useState(); + const [selectedTenant, setSelectedTenant] = useState(); + + // Data Base Options (Reactive constant from Vue example) + const dataBaseOptions = [ + { label: "MySql", value: "MySql" }, + { label: "Oracle", value: "Oracle" }, + { label: "Postgres", value: "Postgres" }, + { label: "Sqlite", value: "Sqlite" }, + { label: "SqlServer", value: "SqlServer" }, + ]; + + const { mutateAsync: deleteTenant } = useMutation({ + mutationFn: deleteApi, + onSuccess: () => { + toast.success($t("AbpUi.DeletedSuccessfully")); + actionRef.current?.reload(); + }, + }); + + const handleCreate = () => { + setSelectedTenantId(undefined); + setTenantModalVisible(true); + }; + + const handleUpdate = (row: TenantDto) => { + setSelectedTenantId(row.id); + setTenantModalVisible(true); + }; + + const handleDelete = (row: TenantDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpSaas.TenantDeletionConfirmationMessage", { 0: row.name }), + onOk: async () => { + await deleteTenant(row.id); + }, + }); + }; + + const handleMenuClick = (key: string, row: TenantDto) => { + setSelectedTenant(row); + if (key === "connection-strings") { + setConnStrModalVisible(true); + } else if (key === "features") { + setFeatureModalVisible(true); + } else if (key === "entity-changes") { + setEntityChangeVisible(true); + } + }; + + const columns: ProColumns[] = [ + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("AbpSaas.DisplayName:IsActive"), + dataIndex: "isActive", + align: "center", + width: 120, + render: (val) => + val ? : , + hideInSearch: true, + }, + { + title: $t("AbpSaas.DisplayName:Name"), + dataIndex: "name", + sorter: true, + hideInSearch: true, + }, + { + title: $t("AbpSaas.DisplayName:EditionName"), + dataIndex: "editionName", + width: 160, + hideInSearch: true, + }, + { + title: $t("AbpUi.Actions"), + valueType: "option", + fixed: "right", + width: 220, + render: (_, record) => ( + + {withAccessChecker( + , + [TenantsPermissions.Update], + )} + {withAccessChecker( + , + [TenantsPermissions.Delete], + )} + , + } + : null, + hasAccessByCodes([TenantsPermissions.ManageFeatures]) + ? { + key: "features", + label: $t("AbpSaas.ManageFeatures"), + icon: , + } + : null, + featureChecker.isEnabled("AbpAuditing.Logging.AuditLog") && + hasAccessByCodes([AuditLogPermissions.Default]) + ? { + key: "entity-changes", + label: $t("AbpAuditLogging.EntitiesChanged"), + icon: , + } + : null, + ].filter(Boolean) as any, + onClick: ({ key }) => handleMenuClick(key, record), + }} + > + , + [TenantsPermissions.Create], + ), + ]} + request={async (params, sorter) => { + const { current, pageSize, ...filters } = params; + const sorting = + sorter && Object.keys(sorter).length > 0 + ? Object.keys(sorter) + .map((key) => `${key} ${antdOrderToAbpOrder(sorter[key])}`) + .join(", ") + : undefined; + + const query = await queryClient.fetchQuery({ + queryKey: ["tenants", params, sorter], + queryFn: () => + getPagedListApi({ + maxResultCount: pageSize, + skipCount: ((current || 1) - 1) * (pageSize || 0), + sorting: sorting, + filter: filters.filter, + }), + }); + + return { + data: query.items, + total: query.totalCount, + success: true, + }; + }} + pagination={{ + defaultPageSize: 10, + showSizeChanger: true, + }} + /> + + setTenantModalVisible(false)} + onChange={() => actionRef.current?.reload()} + /> + + setConnStrModalVisible(false)} + /> + + {selectedTenant && ( + <> + setFeatureModalVisible(false)} + /> + setEntityChangeVisible(false)} + /> + + )} + + ); +}; + +export default TenantTable; diff --git a/apps/react-admin/src/pages/sys/login/LoginForm.tsx b/apps/react-admin/src/pages/sys/login/LoginForm.tsx index 53d9e155d..76a4d234b 100644 --- a/apps/react-admin/src/pages/sys/login/LoginForm.tsx +++ b/apps/react-admin/src/pages/sys/login/LoginForm.tsx @@ -1,33 +1,25 @@ import { Alert, Button, Checkbox, Col, Divider, Form, Input, Row } from "antd"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { AiFillGithub, AiFillGoogleCircle, AiFillWechat } from "react-icons/ai"; - import { DEFAULT_USER, TEST_USER } from "@/_mock/assets"; import type { SignInReq } from "@/api/services/userService"; -import { useSignIn } from "@/store/userStore"; +import { useExternalSignIn, useSignIn } from "@/store/userStore"; import { LoginStateEnum, useLoginStateContext } from "./providers/LoginStateProvider"; -// import { useQuery } from "@tanstack/react-query"; -// import { abpApiDefinitionGet } from "@/api/gen"; +import type { SignInRedirectResult } from "#/account"; +import TenantSwitch from "@/pages/saas/tenants/tenant-switch"; +import { getConfigApi } from "@/api/abp-core/abp"; +import useAbpStore from "@/store/abpCoreStore"; function LoginForm() { const { t } = useTranslation(); + const abpStore = useAbpStore(); const [loading, setLoading] = useState(false); - const { loginState, setLoginState } = useLoginStateContext(); - const signIn = useSignIn(); + const { loginState, setLoginState, setIsExternalLoginState } = useLoginStateContext(); - // const { data } = useQuery({ - // queryKey: ["test"], - // queryFn: () => abpApiDefinitionGet({ - // query:{ - // IncludeTypes:false - // } - // }), - // }); - // console.log(data) - if (loginState !== LoginStateEnum.LOGIN) return null; + const signIn = useSignIn(); const handleFinish = async ({ username, password }: SignInReq) => { setLoading(true); @@ -37,6 +29,79 @@ function LoginForm() { setLoading(false); } }; + + const loginWithProvider = async (provider: string) => { + const clientId = import.meta.env.VITE_GLOB_CLIENT_ID; // OpenIddict Client ID + const baseAddress = import.meta.env.VITE_EXTERNAL_LOGIN_ADDRESS; + window.location.href = `${baseAddress}?provider=${provider}&clientId=${clientId}`; + // window.location.href = `http://localhost:30001/connect/external/login?provider=${provider}&clientId=${clientId}`; + }; + + const handleRegister = (res: SignInRedirectResult) => { + setIsExternalLoginState(res.isExternalLogin); + if (res.needRegister) { + setLoginState(LoginStateEnum.REGISTER); + } + }; + const externalSignIn = useExternalSignIn(handleRegister); + + const handleExternalLogin = async () => { + setLoading(true); + try { + await externalSignIn(); + } finally { + setLoading(false); + } + }; + + async function onInit() { + // if (onlyOidc === true) { + // setTimeout(() => { + // Modal.confirm({ + // centered: true, + // title: $t('page.auth.oidcLogin'), + // content: $t('page.auth.oidcLoginMessage'), + // maskClosable: false, + // closable: false, + // cancelButtonProps: { + // disabled: true, + // }, + // async onOk() { + // await authStore.oidcLogin(); + // }, + // }); + // }, 300); + // return; + // } // TODO + const abpConfig = await getConfigApi(); + abpStore.actions.setApplication(abpConfig); + + // nextTick(() => { + // const formApi = login.value?.getFormApi(); + // formApi?.setFieldValue('tenant', abpConfig.currentTenant.name); + // }); + } + + useEffect(() => { + onInit(); + }, []); + + useEffect(() => { + const search = window.location.search; + // 创建 URLSearchParams 实例来解析查询字符串 + const params = new URLSearchParams(search); + + // 提取具体的参数 + const provider = params.get("provider") || ""; + const clientId = params.get("clientId") || ""; + + if (provider && clientId) { + handleExternalLogin(); + } + }, []); //TODO 添加更醒目的三方登录提示 + + if (loginState !== LoginStateEnum.LOGIN) return null; + return ( <>
{t("sys.login.signInFormTitle")}
@@ -70,6 +135,18 @@ function LoginForm() { /> + {abpStore.application?.multiTenancy.isEnabled && ( + + {/* TODO */} + { + await onInit(); + }} + /> + + )} + + {/* 用户名密码登录表单项 */} @@ -120,7 +197,7 @@ function LoginForm() { {t("sys.login.otherSignIn")}
- + loginWithProvider("gitHub-dotnet")} />
diff --git a/apps/react-admin/src/pages/sys/login/RegisterForm.tsx b/apps/react-admin/src/pages/sys/login/RegisterForm.tsx index 9a033c7b7..7f22eedb7 100644 --- a/apps/react-admin/src/pages/sys/login/RegisterForm.tsx +++ b/apps/react-admin/src/pages/sys/login/RegisterForm.tsx @@ -1,25 +1,30 @@ import { useMutation } from "@tanstack/react-query"; import { Button, Form, Input } from "antd"; import { useTranslation } from "react-i18next"; - -import userService from "@/api/services/userService"; - import { ReturnButton } from "./components/ReturnButton"; import { LoginStateEnum, useLoginStateContext } from "./providers/LoginStateProvider"; +import { externalSignUpApi } from "@/api/account"; +import { useExternalSignIn } from "@/store/userStore"; function RegisterForm() { const { t } = useTranslation(); - const signUpMutation = useMutation({ - mutationFn: userService.signup, + const externalSignIn = useExternalSignIn(() => {}); + const externalSignUpMutation = useMutation({ + mutationFn: externalSignUpApi, }); - const { loginState, backToLogin } = useLoginStateContext(); + const { loginState, backToLogin, isExternalLoginState } = useLoginStateContext(); if (loginState !== LoginStateEnum.REGISTER) return null; const onFinish = async (values: any) => { console.log("Received values of form: ", values); - await signUpMutation.mutateAsync(values); - backToLogin(); + await externalSignUpMutation.mutateAsync({ + userName: values.username, + emailAddress: values.email, + }); + + // 三方注册成功后直接登录 + await externalSignIn(); }; return ( @@ -32,28 +37,32 @@ function RegisterForm() { - - - - ({ - validator(_, value) { - if (!value || getFieldValue("password") === value) { - return Promise.resolve(); - } - return Promise.reject(new Error(t("sys.login.diffPwd"))); + {!isExternalLoginState && ( + + + + )} + {!isExternalLoginState && ( + - - + ({ getFieldValue }) => ({ + validator(_, value) { + if (!value || getFieldValue("password") === value) { + return Promise.resolve(); + } + return Promise.reject(new Error(t("sys.login.diffPwd"))); + }, + }), + ]} + > + + + )} + +
+ } + > + +
+ + + {!jobId && ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + + + + ) + } + + + + + + + + + + + + handleCultureChange(val, true)} + /> + + + + + + + + {/* Target Side (Editable) */} +
+ + + + + + + + + + + { + // Logic from Vue: if inline localized, clear resource name + if (e.target.checked) { + form.setFieldValue("localizationResourceName", undefined); + } + }} + > + {$t("AbpTextTemplating.DisplayName:IsInlineLocalized")} + + + + prev.isInlineLocalized !== curr.isInlineLocalized}> + {({ getFieldValue }) => + !getFieldValue("isInlineLocalized") ? ( + + + + ) + } + + + + { + if (e.target.checked) { + form.setFieldValue("layout", undefined); + } + }} + > + {$t("AbpTextTemplating.DisplayName:IsLayout")} + + + + prev.isLayout !== curr.isLayout}> + {({ getFieldValue }) => + !getFieldValue("isLayout") && ( + + + + + + + + + + + + + + + ); +}; + +export default WebhookGroupDefinitionModal; diff --git a/apps/react-admin/src/pages/webhooks/definitions/groups/webhook-group-definition-table.tsx b/apps/react-admin/src/pages/webhooks/definitions/groups/webhook-group-definition-table.tsx new file mode 100644 index 000000000..4fea08825 --- /dev/null +++ b/apps/react-admin/src/pages/webhooks/definitions/groups/webhook-group-definition-table.tsx @@ -0,0 +1,194 @@ +import type React from "react"; +import { useRef, useState } from "react"; +import { Button, Card, Dropdown, Modal, Space } from "antd"; +import { EditOutlined, DeleteOutlined, PlusOutlined, EllipsisOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deleteApi, getListApi } from "@/api/webhooks/webhook-group-definitions"; +import type { WebhookGroupDefinitionDto } from "#/webhooks/groups"; +import { GroupDefinitionsPermissions } from "@/constants/webhooks/permissions"; // Adjust path as needed +import { withAccessChecker } from "@/utils/abp/access-checker"; +import { localizationSerializer } from "@/utils/abp/localization-serializer"; +import { useLocalizer } from "@/hooks/abp/use-localization"; +import { Iconify } from "@/components/icon"; + +import WebhookGroupDefinitionModal from "./webhook-group-definition-modal"; +import WebhookDefinitionModal from "../webhooks/webhook-definition-modal"; + +const WebhookGroupDefinitionTable: React.FC = () => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const queryClient = useQueryClient(); + const { deserialize } = localizationSerializer(); + const { Lr } = useLocalizer(); + const [modal, contextHolder] = Modal.useModal(); + + // Modal States + const [groupModalVisible, setGroupModalVisible] = useState(false); + const [webhookModalVisible, setWebhookModalVisible] = useState(false); + const [selectedGroupName, setSelectedGroupName] = useState(); + + const { mutateAsync: deleteGroup } = useMutation({ + mutationFn: deleteApi, + onSuccess: () => { + toast.success($t("AbpUi.DeletedSuccessfully")); + queryClient.invalidateQueries({ queryKey: ["webhookGroups"] }); + actionRef.current?.reload(); + }, + }); + + const handleCreate = () => { + setSelectedGroupName(undefined); + setGroupModalVisible(true); + }; + + const handleUpdate = (row: WebhookGroupDefinitionDto) => { + setSelectedGroupName(row.name); + setGroupModalVisible(true); + }; + + const handleDelete = (row: WebhookGroupDefinitionDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessageWithFormat", { 0: row.name }), + onOk: () => deleteGroup(row.name), + }); + }; + + const handleMenuClick = (key: string, row: WebhookGroupDefinitionDto) => { + if (key === "webhooks") { + setSelectedGroupName(row.name); + setWebhookModalVisible(true); + } + }; + + const columns: ProColumns[] = [ + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("WebhooksManagement.DisplayName:Name"), + dataIndex: "name", + width: "auto", + sorter: true, + hideInSearch: true, + }, + { + title: $t("WebhooksManagement.DisplayName:DisplayName"), + dataIndex: "displayName", + width: "auto", + sorter: true, + hideInSearch: true, + render: (_, record) => { + const localizableString = deserialize(record.displayName); + return Lr(localizableString.resourceName, localizableString.name); + }, + }, + { + title: $t("AbpUi.Actions"), + width: 220, + fixed: "right", + hideInSearch: true, + render: (_, record) => ( + + {withAccessChecker( + , + [GroupDefinitionsPermissions.Update], + )} + + {!record.isStatic && + withAccessChecker( + , + [GroupDefinitionsPermissions.Delete], + )} + + , + }, + ], + onClick: ({ key }) => handleMenuClick(key, record), + }} + > + , + [GroupDefinitionsPermissions.Create], + ), + ]} + request={async (params) => { + const { current, pageSize, ...filters } = params; + + const { items } = await getListApi({ + filter: filters.filter, + }); + + return { + data: items, + total: items.length, + success: true, + }; + }} + pagination={{ + defaultPageSize: 10, + showSizeChanger: true, + }} + /> + + setGroupModalVisible(false)} + onChange={() => { + actionRef.current?.reload(); + queryClient.invalidateQueries({ queryKey: ["webhookGroups"] }); + }} + /> + + setWebhookModalVisible(false)} + onChange={() => { + // actionRef.current?.reload(); + }} + /> + + ); +}; + +export default WebhookGroupDefinitionTable; diff --git a/apps/react-admin/src/pages/webhooks/definitions/webhooks/webhook-definition-modal.tsx b/apps/react-admin/src/pages/webhooks/definitions/webhooks/webhook-definition-modal.tsx new file mode 100644 index 000000000..76e867216 --- /dev/null +++ b/apps/react-admin/src/pages/webhooks/definitions/webhooks/webhook-definition-modal.tsx @@ -0,0 +1,313 @@ +import type React from "react"; +import { useEffect, useMemo, useState } from "react"; +import { Modal, Form, Input, Select, Checkbox, TreeSelect, Tabs } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { useMutation } from "@tanstack/react-query"; +import { createApi, getApi, updateApi } from "@/api/webhooks/webhook-definitions"; +import { getListApi as getGroupDefinitionsApi } from "@/api/webhooks/webhook-group-definitions"; +import { getListApi as getFeaturesApi } from "@/api/management/features/feature-definitions"; +import { getListApi as getFeatureGroupsApi } from "@/api/management/features/feature-group-definitions"; +import type { WebhookDefinitionDto } from "#/webhooks/definitions"; +import type { WebhookGroupDefinitionDto } from "#/webhooks/groups"; +import type { FeatureDefinitionDto, FeatureGroupDefinitionDto } from "#/management/features"; +import type { PropertyInfo } from "@/components/abp/properties/types"; +import LocalizableInput from "@/components/abp/localizable-input/localizable-input"; +import PropertyTable from "@/components/abp/properties/property-table"; + +import { localizationSerializer } from "@/utils/abp/localization-serializer"; +import { useLocalizer } from "@/hooks/abp/use-localization"; +import { hasAccessByCodes } from "@/utils/abp/access-checker"; +import { listToTree } from "@/utils/tree"; +import { valueTypeSerializer } from "@/components/abp/string-value-type"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (data: WebhookDefinitionDto) => void; + groupName?: string; + definitionName?: string; +} + +const defaultModel: WebhookDefinitionDto = { + displayName: "", + extraProperties: {}, + groupName: "", + isEnabled: true, + isStatic: false, + name: "", + requiredFeatures: [], +} as WebhookDefinitionDto; + +const WebhookDefinitionModal: React.FC = ({ visible, onClose, onChange, groupName, definitionName }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + const { deserialize } = localizationSerializer(); + const { Lr } = useLocalizer(); + const [activeTab, setActiveTab] = useState("basic"); + const [formModel, setFormModel] = useState({ ...defaultModel }); + const [isEditModel, setIsEditModel] = useState(false); + const [loading, setLoading] = useState(false); + + // Data State + const [webhookGroups, setWebhookGroups] = useState([]); + const [features, setFeatures] = useState([]); + const [featureGroups, setFeatureGroups] = useState([]); + + useEffect(() => { + if (visible) { + setActiveTab("basic"); + initData(); + } + }, [visible]); + + const initData = async () => { + try { + setLoading(true); + const [groupRes, featureGroupRes, featureRes] = await Promise.all([ + getGroupDefinitionsApi({ filter: groupName }), + hasAccessByCodes(["FeatureManagement.GroupDefinitions"]) + ? getFeatureGroupsApi() + : Promise.resolve({ items: [] }), + hasAccessByCodes(["FeatureManagement.Definitions"]) ? getFeaturesApi() : Promise.resolve({ items: [] }), + ]); + + // 1. Process Webhook Groups + const formattedWebhookGroups = groupRes.items.map((g) => { + const d = deserialize(g.displayName); + return { ...g, displayName: Lr(d.resourceName, d.name) }; + }); + setWebhookGroups(formattedWebhookGroups); + + // 2. Process Feature Groups + const formattedFeatureGroups = featureGroupRes.items.map((g) => { + const d = deserialize(g.displayName); + return { ...g, displayName: Lr(d.resourceName, d.name) }; + }); + setFeatureGroups(formattedFeatureGroups); + + // 3. Process Features (Filter Boolean + Localize) + const formattedFeatures = featureRes.items + .filter((f) => { + if (f.valueType) { + try { + const vt = valueTypeSerializer.deserialize(f.valueType); + return vt.validator.name === "BOOLEAN"; + } catch { + return false; + } + } + return true; + }) + .map((f) => { + const d = deserialize(f.displayName); + return { + ...f, + displayName: Lr(d.resourceName, d.name), + // Add UI properties for AntD Tree + title: Lr(d.resourceName, d.name), + value: f.name, + key: f.name, + }; + }); + setFeatures(formattedFeatures); + + // 4. Set Form Data + if (definitionName) { + setIsEditModel(true); + const dto = await getApi(definitionName); + setFormModel(dto); + form.setFieldsValue(dto); + } else { + setIsEditModel(false); + const initial = { ...defaultModel }; + if (groupName) initial.groupName = groupName; + else if (formattedWebhookGroups.length === 1) initial.groupName = formattedWebhookGroups[0].name; + + setFormModel(initial); + form.setFieldsValue(initial); + } + } finally { + setLoading(false); + } + }; + + // --- Fix 1: Build Tree Data Correctly --- + const featureTreeData = useMemo(() => { + return featureGroups.map((group) => { + // Get features for this group + const groupFeatures = features.filter((f) => f.groupName === group.name); + + // Convert flat list to tree + const children = listToTree(groupFeatures, { id: "name", pid: "parentName" }); + + return { + title: group.displayName, + value: group.name, + key: group.name, + selectable: false, + checkable: false, // Groups are containers, not selectable features in this context + disableCheckbox: true, + children: children, + }; + }); + }, [features, featureGroups]); + + // --- Fix 2: Map String[] to Object[] for Strict TreeSelect --- + const requiredFeaturesValue = useMemo(() => { + return (formModel.requiredFeatures || []).map((name) => { + const feature = features.find((f) => f.name === name); + return { + label: feature?.displayName || name, + value: name, + }; + }); + }, [formModel.requiredFeatures, features]); + + const { mutateAsync: createDef, isPending: isCreating } = useMutation({ + mutationFn: createApi, + onSuccess: (res) => { + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(res); + onClose(); + }, + }); + + const { mutateAsync: updateDef, isPending: isUpdating } = useMutation({ + mutationFn: (data: WebhookDefinitionDto) => updateApi(data.name, data), + onSuccess: (res) => { + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(res); + onClose(); + }, + }); + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + const submitData = { + ...formModel, + ...values, + // Don't take values.requiredFeatures directly if using strict mode manual handling, + // or ensure it matches formModel.requiredFeatures + requiredFeatures: formModel.requiredFeatures, + }; + + if (isEditModel) { + await updateDef(submitData); + } else { + await createDef(submitData); + } + } catch (error) { + console.error(error); + } + }; + + // --- Fix 3: Handle TreeSelect Change --- + const handleFeaturesChange = (labeledValues: { label: React.ReactNode; value: string }[]) => { + const names = labeledValues.map((v) => v.value); + setFormModel((prev) => ({ ...prev, requiredFeatures: names })); + // Update form instance too for validation if needed, though we manage state manually + form.setFieldValue("requiredFeatures", names); + }; + + const handlePropChange = (prop: PropertyInfo) => { + setFormModel((prev) => ({ + ...prev, + extraProperties: { ...prev.extraProperties, [prop.key]: prop.value }, + })); + }; + + const handlePropDelete = (prop: PropertyInfo) => { + setFormModel((prev) => { + const next = { ...prev.extraProperties }; + delete next[prop.key]; + return { ...prev, extraProperties: next }; + }); + }; + + return ( + +
+ + + + {$t("WebhooksManagement.DisplayName:IsEnabled")} + + + + + + + + + + + + + + + {hasAccessByCodes(["FeatureManagement.GroupDefinitions", "FeatureManagement.Definitions"]) && ( + + + + )} + + + + + + + +
+ ); +}; + +export default WebhookDefinitionModal; diff --git a/apps/react-admin/src/pages/webhooks/definitions/webhooks/webhook-definition-table.tsx b/apps/react-admin/src/pages/webhooks/definitions/webhooks/webhook-definition-table.tsx new file mode 100644 index 000000000..021adc04b --- /dev/null +++ b/apps/react-admin/src/pages/webhooks/definitions/webhooks/webhook-definition-table.tsx @@ -0,0 +1,237 @@ +import type React from "react"; +import { useRef, useState } from "react"; +import { Button, Tag, Space, Modal, Table, Card } from "antd"; +import { EditOutlined, DeleteOutlined, PlusOutlined, CheckOutlined, CloseOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deleteApi, getListApi as getDefinitionsApi } from "@/api/webhooks/webhook-definitions"; +import { getListApi as getGroupsApi } from "@/api/webhooks/webhook-group-definitions"; +import type { WebhookDefinitionDto } from "#/webhooks/definitions"; +import type { WebhookGroupDefinitionDto } from "#/webhooks/groups"; +import { WebhookDefinitionsPermissions } from "@/constants/webhooks/permissions"; +import { hasAccessByCodes, withAccessChecker } from "@/utils/abp/access-checker"; +import { localizationSerializer } from "@/utils/abp/localization-serializer"; +import { useLocalizer } from "@/hooks/abp/use-localization"; + +import WebhookDefinitionModal from "./webhook-definition-modal"; +import { listToTree } from "@/utils/tree"; + +// Extended interface to hold the nested items for display +interface ExtendedGroupDto extends WebhookGroupDefinitionDto { + items: WebhookDefinitionDto[]; +} + +const WebhookDefinitionTable: React.FC = () => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const queryClient = useQueryClient(); + const { deserialize } = localizationSerializer(); + const { Lr } = useLocalizer(); + const [modal, contextHolder] = Modal.useModal(); + + const [modalVisible, setModalVisible] = useState(false); + const [selectedDefinitionName, setSelectedDefinitionName] = useState(); + + const { mutateAsync: deleteDef } = useMutation({ + mutationFn: deleteApi, + onSuccess: () => { + toast.success($t("AbpUi.DeletedSuccessfully")); + actionRef.current?.reload(); + }, + }); + + const handleCreate = () => { + setSelectedDefinitionName(undefined); + setModalVisible(true); + }; + + const handleUpdate = (row: WebhookDefinitionDto) => { + setSelectedDefinitionName(row.name); + setModalVisible(true); + }; + + const handleDelete = (row: WebhookDefinitionDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessageWithFormat", { 0: row.name }), + onOk: () => deleteDef(row.name), + }); + }; + + // --- Sub-Table Columns (Definitions) --- + const definitionColumns = [ + { + title: $t("WebhooksManagement.DisplayName:IsEnabled"), + dataIndex: "isEnabled", + width: 100, + align: "center" as const, + render: (val: boolean) => + val ? : , + }, + { + title: $t("WebhooksManagement.DisplayName:Name"), + dataIndex: "name", + width: 250, + }, + { + title: $t("WebhooksManagement.DisplayName:DisplayName"), + dataIndex: "displayName", + width: 200, + }, + { + title: $t("WebhooksManagement.DisplayName:Description"), + dataIndex: "description", + ellipsis: true, + }, + { + title: $t("WebhooksManagement.DisplayName:RequiredFeatures"), + dataIndex: "requiredFeatures", + width: 200, + render: (features: string[]) => ( + + {features?.map((f) => ( + + {f} + + ))} + + ), + }, + { + title: $t("AbpUi.Actions"), + key: "actions", + width: 150, + fixed: "right" as const, + render: (_: any, record: WebhookDefinitionDto) => ( + + {withAccessChecker( + , + [WebhookDefinitionsPermissions.Update], + )} + {!record.isStatic && + withAccessChecker( + , + [WebhookDefinitionsPermissions.Delete], + )} + + ), + }, + ]; + + // --- Main Table Columns (Groups) --- + const columns: ProColumns[] = [ + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("WebhooksManagement.DisplayName:Name"), + dataIndex: "name", + width: 200, + fixed: "left", + }, + { + title: $t("WebhooksManagement.DisplayName:DisplayName"), + dataIndex: "displayName", + }, + ]; + + return ( + <> + {contextHolder} + + + headerTitle={$t("WebhooksManagement.WebhookDefinitions")} + actionRef={actionRef} + rowKey="name" + columns={columns} + search={{ labelWidth: "auto" }} + toolBarRender={() => [ + withAccessChecker( + , + [WebhookDefinitionsPermissions.Create], + ), + ]} + // Nested Table Renderer + expandable={{ + expandedRowRender: (record) => ( +
+ ), + defaultExpandAllRows: true, // Expand groups by default to match Vue behavior often used for categorization + }} + request={async (params) => { + const { filter } = params; + + // 1. Fetch Groups and Definitions in parallel + const [groupRes, defRes] = await Promise.all([getGroupsApi({ filter }), getDefinitionsApi({ filter })]); + + // 2. Process and merge data + const data: ExtendedGroupDto[] = groupRes.items.map((group) => { + // Localize Group Name + const groupLocal = deserialize(group.displayName); + + // Filter definitions belonging to this group + const groupDefinitions = defRes.items + .filter((d) => d.groupName === group.name) + .map((d) => { + // Localize Definition fields + const dName = deserialize(d.displayName); + const dDesc = deserialize(d.description); + return { + ...d, + displayName: Lr(dName.resourceName, dName.name), + description: dDesc ? Lr(dDesc.resourceName, dDesc.name) : "", + }; + }); + + // Build Tree for definitions (handling parent/child relationships if any) + const definitionsTree = listToTree(groupDefinitions, { id: "name", pid: "parentName" }); + + return { + ...group, + displayName: Lr(groupLocal.resourceName, groupLocal.name), + items: definitionsTree, + }; + }); + + return { + data: data, + success: true, + total: groupRes.items.length, + }; + }} + pagination={false} + /> + + setModalVisible(false)} + onChange={() => actionRef.current?.reload()} + /> + + ); +}; + +export default WebhookDefinitionTable; diff --git a/apps/react-admin/src/pages/webhooks/send-attempts/webhook-send-attempt-drawer.tsx b/apps/react-admin/src/pages/webhooks/send-attempts/webhook-send-attempt-drawer.tsx new file mode 100644 index 000000000..111ee8292 --- /dev/null +++ b/apps/react-admin/src/pages/webhooks/send-attempts/webhook-send-attempt-drawer.tsx @@ -0,0 +1,196 @@ +import type React from "react"; +import { useEffect, useState } from "react"; +import { Drawer, Form, Input, Checkbox, Tabs, Tag } from "antd"; +import { useTranslation } from "react-i18next"; +import { getApi } from "@/api/webhooks/send-attempts"; +import { getApi as getSubscriptionApi } from "@/api/webhooks/subscriptions"; +import { getApi as getTenantApi } from "@/api/saas/tenants"; +import type { WebhookSendRecordDto } from "#/webhooks/send-attempts"; +import type { WebhookSubscriptionDto } from "#/webhooks/subscriptions"; +import type { TenantDto } from "#/saas/tenants"; +import { WebhookSubscriptionPermissions } from "@/constants/webhooks/permissions"; // Adjust path +import { hasAccessByCodes } from "@/utils/abp/access-checker"; +import { formatToDateTime } from "@/utils/abp"; + +import Editor from "@/components/editor"; +import { useHttpStatusCodeMap } from "@/hooks/abp/fake-hooks/use-http-status-code-map"; +import JsonEdit from "@/components/abp/common/json-edit"; + +interface Props { + visible: boolean; + onClose: () => void; + recordId?: string; +} + +const WebhookSendAttemptDrawer: React.FC = ({ visible, onClose, recordId }) => { + const { t: $t } = useTranslation(); + + const { getHttpStatusColor, httpStatusCodeMap } = useHttpStatusCodeMap(); + const [activeTab, setActiveTab] = useState("basic"); + const [loading, setLoading] = useState(false); + const [formModel, setFormModel] = useState(); + const [webhookSubscription, setWebhookSubscription] = useState(); + const [webhookTenant, setWebhookTenant] = useState(); + + useEffect(() => { + if (visible && recordId) { + initData(recordId); + } else { + setFormModel(undefined); + setWebhookSubscription(undefined); + setWebhookTenant(undefined); + } + }, [visible, recordId]); + + const initData = async (id: string) => { + try { + setLoading(true); + const dto = await getApi(id); + setFormModel(dto); + + const [sub, tenant] = await Promise.all([ + fetchSubscription(dto.webhookSubscriptionId), + fetchTenant(dto.tenantId), + ]); + setWebhookSubscription(sub); + setWebhookTenant(tenant); + } finally { + setLoading(false); + } + }; + + const fetchSubscription = async (id: string) => { + if (hasAccessByCodes([WebhookSubscriptionPermissions.Default])) { + return await getSubscriptionApi(id); + } + return undefined; + }; + + const fetchTenant = async (id?: string) => { + if (id && hasAccessByCodes(["AbpSaas.Tenants"])) { + return await getTenantApi(id); + } + return undefined; + }; + + return ( + + {formModel && ( +
+ + {/* Basic Info Tab */} + + {webhookTenant && ( + + + + )} + + + + {$t("WebhooksManagement.DisplayName:SendExactSameData")} + + + + + + + + + + + + {formModel.responseStatusCode && ( + + + {httpStatusCodeMap[formModel.responseStatusCode] || formModel.responseStatusCode} + + + )} + + + + + + + + {/* */} + + + + {/* Event Tab */} + + + + + + + + + + + + + + + + + + + {/* Subscriber Tab */} + {webhookSubscription && ( + + + + {$t("WebhooksManagement.DisplayName:IsActive")} + + + + + + + + + + + + + + + + + + + + + + + + + {webhookSubscription.webhooks?.map((w) => ( + + {w} + + ))} + + + + + + + )} + + + )} +
+ ); +}; + +export default WebhookSendAttemptDrawer; diff --git a/apps/react-admin/src/pages/webhooks/send-attempts/webhook-send-attempt-table.tsx b/apps/react-admin/src/pages/webhooks/send-attempts/webhook-send-attempt-table.tsx new file mode 100644 index 000000000..e749b5c8d --- /dev/null +++ b/apps/react-admin/src/pages/webhooks/send-attempts/webhook-send-attempt-table.tsx @@ -0,0 +1,278 @@ +import type React from "react"; +import { useRef, useState } from "react"; +import { Button, Tag, Dropdown, Modal, Space, Card } from "antd"; +import { EditOutlined, DeleteOutlined, EllipsisOutlined, SendOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deleteApi, getPagedListApi, reSendApi, bulkDeleteApi, bulkReSendApi } from "@/api/webhooks/send-attempts"; +import { getPagedListApi as getTenantsApi } from "@/api/saas/tenants"; +import type { WebhookSendRecordDto } from "#/webhooks/send-attempts"; +import { WebhooksSendAttemptsPermissions } from "@/constants/webhooks/permissions"; // Adjust +import { hasAccessByCodes, withAccessChecker } from "@/utils/abp/access-checker"; +import { formatToDateTime } from "@/utils/abp"; +import { antdOrderToAbpOrder } from "@/utils/abp/sort-order"; +import WebhookSendAttemptDrawer from "./webhook-send-attempt-drawer"; +import { useHttpStatusCodeMap } from "@/hooks/abp/fake-hooks/use-http-status-code-map"; +import { HttpStatusCode } from "@/constants/request/http-status"; + +const WebhookSendAttemptTable: React.FC = () => { + const { t: $t } = useTranslation(); + const { getHttpStatusColor, httpStatusCodeMap } = useHttpStatusCodeMap(); + const actionRef = useRef(); + const queryClient = useQueryClient(); + const [modal, contextHolder] = Modal.useModal(); + + const [drawerVisible, setDrawerVisible] = useState(false); + const [selectedRecordId, setSelectedRecordId] = useState(); + const [selectedKeys, setSelectedKeys] = useState([]); + + // Mutation for single resend + const { mutateAsync: resend } = useMutation({ + mutationFn: reSendApi, + onSuccess: () => { + toast.success($t("WebhooksManagement.SuccessfullySent")); + actionRef.current?.reload(); + }, + }); + + // Bulk mutations + const { mutateAsync: bulkResend } = useMutation({ + mutationFn: bulkReSendApi, + onSuccess: () => { + toast.success($t("WebhooksManagement.SuccessfullySent")); + actionRef.current?.reload(); + }, + }); + + const { mutateAsync: bulkDelete } = useMutation({ + mutationFn: bulkDeleteApi, + onSuccess: () => { + toast.success($t("AbpUi.DeletedSuccessfully")); + setSelectedKeys([]); + actionRef.current?.reload(); + }, + }); + + const handleShow = (row: WebhookSendRecordDto) => { + setSelectedRecordId(row.id); + setDrawerVisible(true); + }; + + const handleDelete = (row: WebhookSendRecordDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessageWithFormat", { 0: $t("WebhooksManagement.SelectedItems") }), + onOk: async () => { + await deleteApi(row.id); + toast.success($t("AbpUi.DeletedSuccessfully")); + actionRef.current?.reload(); + }, + }); + }; + + const handleSend = (row: WebhookSendRecordDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("WebhooksManagement.ItemWillBeResendMessageWithFormat", { + 0: $t("WebhooksManagement.SelectedItems"), + }), + onOk: () => resend(row.id), + }); + }; + + const handleBulkSend = () => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("WebhooksManagement.ItemWillBeResendMessageWithFormat", { + 0: $t("WebhooksManagement.SelectedItems"), + }), + onOk: () => bulkResend({ recordIds: selectedKeys as string[] }), + }); + }; + + const handleBulkDelete = () => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessageWithFormat", { 0: $t("WebhooksManagement.SelectedItems") }), + onOk: () => bulkDelete({ recordIds: selectedKeys as string[] }), + }); + }; + + const httpStatusOptions = Object.keys(httpStatusCodeMap).map((key) => ({ + label: httpStatusCodeMap[Number(key)], + value: key, + })); + + const columns: ProColumns[] = [ + { + title: $t("WebhooksManagement.DisplayName:TenantId"), + dataIndex: "tenantId", + width: 200, + sorter: true, + valueType: "select", + request: async ({ keyWords }) => { + // Simple search logic for ProTable Select + const res = await getTenantsApi({ filter: keyWords, maxResultCount: 20 }); + return res.items.map((t) => ({ label: t.name, value: t.id })); + }, + fieldProps: { + showSearch: true, + allowClear: true, + }, + }, + { + title: $t("WebhooksManagement.DisplayName:ResponseStatusCode"), + dataIndex: "responseStatusCode", + width: 150, + sorter: true, + valueType: "select", + fieldProps: { options: httpStatusOptions }, + render: (_, row) => ( + + {httpStatusCodeMap[row.responseStatusCode || HttpStatusCode.InternalServerError] || row.responseStatusCode} + + ), + }, + { + title: $t("WebhooksManagement.DisplayName:CreationTime"), + dataIndex: "creationTime", + width: 180, + sorter: true, + valueType: "dateRange", + render: (_, row) => formatToDateTime(row.creationTime), + }, + { + title: $t("WebhooksManagement.DisplayName:Response"), + dataIndex: "response", + width: 300, + sorter: true, + ellipsis: true, + hideInSearch: true, + }, + { + title: $t("WebhooksManagement.DisplayName:State"), + dataIndex: "state", // Mapped to filter + hideInTable: true, + valueType: "select", + valueEnum: { + true: { text: $t("WebhooksManagement.ResponseState:Successed"), status: "Success" }, + false: { text: $t("WebhooksManagement.ResponseState:Failed"), status: "Error" }, + }, + }, + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("AbpUi.Actions"), + valueType: "option", + fixed: "right", + width: 150, + render: (_, record) => ( + + {withAccessChecker( + , + [WebhooksSendAttemptsPermissions.Default], + )} + {withAccessChecker( + , + [WebhooksSendAttemptsPermissions.Delete], + )} + , + onClick: () => handleSend(record), + } + : null, + ].filter(Boolean) as any, + }} + > + + ), + selectedKeys.length > 0 && hasAccessByCodes([WebhooksSendAttemptsPermissions.Delete]) && ( + + ), + ]} + request={async (params, sorter) => { + const { current, pageSize, creationTime, ...filters } = params; + const [beginCreationTime, endCreationTime] = creationTime || []; + + const sorting = + sorter && Object.keys(sorter).length > 0 + ? Object.keys(sorter) + .map((key) => `${key} ${antdOrderToAbpOrder(sorter[key])}`) + .join(", ") + : undefined; + + const query = await queryClient.fetchQuery({ + queryKey: ["sendAttempts", params, sorter], + queryFn: () => + getPagedListApi({ + maxResultCount: pageSize, + skipCount: ((current || 1) - 1) * (pageSize || 0), + sorting: sorting, + beginCreationTime, + endCreationTime, + ...filters, + }), + }); + + return { + data: query.items, + total: query.totalCount, + success: true, + }; + }} + pagination={{ defaultPageSize: 10, showSizeChanger: true }} + /> + + setDrawerVisible(false)} + /> + + ); +}; + +export default WebhookSendAttemptTable; diff --git a/apps/react-admin/src/pages/webhooks/subscriptions/webhook-subscription-modal.tsx b/apps/react-admin/src/pages/webhooks/subscriptions/webhook-subscription-modal.tsx new file mode 100644 index 000000000..7c2f69cfc --- /dev/null +++ b/apps/react-admin/src/pages/webhooks/subscriptions/webhook-subscription-modal.tsx @@ -0,0 +1,263 @@ +import type React from "react"; +import { useEffect, useState } from "react"; +import { Modal, Form, Input, Checkbox, Select, Tabs, Tooltip } from "antd"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { useMutation } from "@tanstack/react-query"; +import { createApi, updateApi, getApi, getAllAvailableWebhooksApi } from "@/api/webhooks/subscriptions"; +import { getPagedListApi as getTenantsApi } from "@/api/saas/tenants"; +import type { WebhookSubscriptionDto, WebhookAvailableGroupDto } from "#/webhooks/subscriptions"; +import type { TenantDto } from "#/saas/tenants"; +import { hasAccessByCodes } from "@/utils/abp/access-checker"; +import type { PropertyInfo } from "@/components/abp/properties/types"; +import PropertyTable from "@/components/abp/properties/property-table"; +import debounce from "lodash.debounce"; + +interface Props { + visible: boolean; + onClose: () => void; + onChange: (data: WebhookSubscriptionDto) => void; + subscriptionId?: string; +} + +// 1. Define a local type that includes the missing property +interface LocalWebhookSubscriptionDto extends WebhookSubscriptionDto { + isStatic?: boolean; +} + +// 2. Use the local type for the default model +const defaultModel: LocalWebhookSubscriptionDto = { + creationTime: new Date(), + displayName: "", + extraProperties: {}, + headers: {}, + id: "", + isActive: true, + isStatic: false, + webhooks: [], + webhookUri: "", +} as LocalWebhookSubscriptionDto; + +const WebhookSubscriptionModal: React.FC = ({ visible, onClose, onChange, subscriptionId }) => { + const { t: $t } = useTranslation(); + const [form] = Form.useForm(); + + const [activeTab, setActiveTab] = useState("basic"); + // 3. State uses the local type + const [formModel, setFormModel] = useState({ ...defaultModel }); + const [loading, setLoading] = useState(false); + const [isEditModel, setIsEditModel] = useState(false); + + const [webhookGroups, setWebhookGroups] = useState([]); + const [tenants, setTenants] = useState([]); + + useEffect(() => { + if (visible) { + setActiveTab("basic"); + form.resetFields(); + initData(); + + if (subscriptionId) { + fetchSubscription(subscriptionId); + setIsEditModel(true); + } else { + setFormModel({ ...defaultModel }); + setIsEditModel(false); + } + } + }, [visible, subscriptionId]); + + const initData = async () => { + try { + const [groupRes, tenantRes] = await Promise.all([getAllAvailableWebhooksApi(), fetchTenants()]); + setWebhookGroups(groupRes.items); + setTenants(tenantRes); + } catch (error) { + console.error(error); + } + }; + + const fetchTenants = async (filter?: string) => { + if (!hasAccessByCodes(["AbpSaas.Tenants"])) { + return []; + } + const { items } = await getTenantsApi({ filter, maxResultCount: 50 }); + return items; + }; + + const fetchSubscription = async (id: string) => { + try { + setLoading(true); + const dto = await getApi(id); + // 4. Cast the API response to the local type + setFormModel(dto as LocalWebhookSubscriptionDto); + form.setFieldsValue(dto); + } finally { + setLoading(false); + } + }; + + const { mutateAsync: createSub, isPending: isCreating } = useMutation({ + mutationFn: createApi, + onSuccess: (res) => { + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(res); + onClose(); + }, + }); + + const { mutateAsync: updateSub, isPending: isUpdating } = useMutation({ + mutationFn: (data: WebhookSubscriptionDto) => updateApi(data.id, data), + onSuccess: (res) => { + toast.success($t("AbpUi.SavedSuccessfully")); + onChange(res); + onClose(); + }, + }); + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + const submitData = { + ...formModel, + ...values, + }; + + if (isEditModel) { + await updateSub(submitData); + } else { + await createSub(submitData); + } + } catch (error) { + console.error(error); + } + }; + + const handleTenantsSearch = debounce(async (input: string) => { + const res = await fetchTenants(input); + setTenants(res); + }, 500); + + const handlePropChange = (prop: PropertyInfo) => { + setFormModel((prev) => ({ + ...prev, + headers: { + ...prev.headers, + [prop.key]: prop.value, + }, + })); + }; + + const handlePropDelete = (prop: PropertyInfo) => { + setFormModel((prev) => { + const next = { ...prev.headers }; + delete next[prop.key]; + return { ...prev, headers: next }; + }); + }; + + const modalTitle = isEditModel + ? $t("WebhooksManagement.Subscriptions:Edit") + : $t("WebhooksManagement.Subscriptions:AddNew"); + + return ( + +
+ + + + {$t("WebhooksManagement.DisplayName:IsActive")} + + + + + + + + ({ label: t.name, value: t.id }))} + /> + + )} + + + + + + + + + + + + + + + +
+ ); +}; + +export default WebhookSubscriptionModal; diff --git a/apps/react-admin/src/pages/webhooks/subscriptions/webhook-subscription-table.tsx b/apps/react-admin/src/pages/webhooks/subscriptions/webhook-subscription-table.tsx new file mode 100644 index 000000000..d4d3e5694 --- /dev/null +++ b/apps/react-admin/src/pages/webhooks/subscriptions/webhook-subscription-table.tsx @@ -0,0 +1,211 @@ +import type React from "react"; +import { useRef, useState } from "react"; +import { Button, Tag, Space, Modal, Card } from "antd"; +import { EditOutlined, DeleteOutlined, PlusOutlined, CheckOutlined, CloseOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { ProTable, type ProColumns, type ActionType } from "@ant-design/pro-table"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deleteApi, getPagedListApi, getAllAvailableWebhooksApi } from "@/api/webhooks/subscriptions"; +import { getPagedListApi as getTenantsApi } from "@/api/saas/tenants"; +import type { WebhookSubscriptionDto } from "#/webhooks/subscriptions"; +import { WebhookSubscriptionPermissions } from "@/constants/webhooks/permissions"; // Adjust path +import { hasAccessByCodes } from "@/utils/abp/access-checker"; +import { formatToDateTime } from "@/utils/abp"; +import { antdOrderToAbpOrder } from "@/utils/abp/sort-order"; + +import WebhookSubscriptionModal from "./webhook-subscription-modal"; + +const WebhookSubscriptionTable: React.FC = () => { + const { t: $t } = useTranslation(); + const actionRef = useRef(); + const queryClient = useQueryClient(); + const [modal, contextHolder] = Modal.useModal(); + + const [modalVisible, setModalVisible] = useState(false); + const [selectedSubscriptionId, setSelectedSubscriptionId] = useState(); + + const { mutateAsync: deleteSub } = useMutation({ + mutationFn: deleteApi, + onSuccess: () => { + toast.success($t("AbpUi.DeletedSuccessfully")); + actionRef.current?.reload(); + }, + }); + + const handleCreate = () => { + setSelectedSubscriptionId(undefined); + setModalVisible(true); + }; + + const handleUpdate = (row: WebhookSubscriptionDto) => { + setSelectedSubscriptionId(row.id); + setModalVisible(true); + }; + + const handleDelete = (row: WebhookSubscriptionDto) => { + modal.confirm({ + title: $t("AbpUi.AreYouSure"), + content: $t("AbpUi.ItemWillBeDeletedMessageWithFormat", { 0: row.webhookUri }), // Use URI or suitable display field + onOk: () => deleteSub(row.id), + }); + }; + + // Helper for init webhooks for search filter + const fetchWebhookOptions = async () => { + if (hasAccessByCodes([WebhookSubscriptionPermissions.Default])) { + const { items } = await getAllAvailableWebhooksApi(); + return items.flatMap((g) => g.webhooks.map((w) => ({ label: w.displayName, value: w.name }))); + } + return []; + }; + + const columns: ProColumns[] = [ + { + title: $t("WebhooksManagement.DisplayName:IsActive"), + dataIndex: "isActive", + width: 100, + hideInSearch: true, + align: "center", + valueType: "checkbox", + render: (_, record) => + record.isActive ? : , + }, + { + title: $t("WebhooksManagement.DisplayName:TenantId"), + dataIndex: "tenantId", + width: 200, + valueType: "select", + request: async ({ keyWords }) => { + const res = await getTenantsApi({ filter: keyWords, maxResultCount: 20 }); + return res.items.map((t) => ({ label: t.name, value: t.id })); + }, + fieldProps: { + showSearch: true, + allowClear: true, + }, + }, + { + title: $t("WebhooksManagement.DisplayName:WebhookUri"), + dataIndex: "webhookUri", + width: 300, + sorter: true, + }, + { + title: $t("WebhooksManagement.DisplayName:Description"), + dataIndex: "description", + width: 200, + sorter: true, + hideInSearch: true, + }, + { + title: $t("WebhooksManagement.DisplayName:CreationTime"), + dataIndex: "creationTime", + width: 150, + sorter: true, + valueType: "dateRange", + render: (_, row) => formatToDateTime(row.creationTime), + }, + { + title: $t("WebhooksManagement.DisplayName:Webhooks"), + dataIndex: "webhooks", // This might need custom filter mapping if backend expects list + width: 300, + render: (_, row) => ( + + {row.webhooks?.map((w) => ( + + {w} + + ))} + + ), + valueType: "select", + request: fetchWebhookOptions, + fieldProps: { + showSearch: true, + mode: "multiple", + }, + }, + { + title: $t("AbpUi.Search"), + dataIndex: "filter", + valueType: "text", + hideInTable: true, + }, + { + title: $t("AbpUi.Actions"), + valueType: "option", + fixed: "right", + width: 150, + render: (_, record) => ( + + + + + ), + }, + ]; + + return ( + <> + {contextHolder} + + + headerTitle={$t("WebhooksManagement.Subscriptions")} + actionRef={actionRef} + rowKey="id" + columns={columns} + search={{ labelWidth: "auto", defaultCollapsed: true }} + toolBarRender={() => [ + , + ]} + request={async (params, sorter) => { + const { current, pageSize, creationTime, ...filters } = params; + const [beginCreationTime, endCreationTime] = creationTime || []; + + const sorting = + sorter && Object.keys(sorter).length > 0 + ? Object.keys(sorter) + .map((key) => `${key} ${antdOrderToAbpOrder(sorter[key])}`) + .join(", ") + : undefined; + + const query = await queryClient.fetchQuery({ + queryKey: ["subscriptions", params, sorter], + queryFn: () => + getPagedListApi({ + maxResultCount: pageSize, + skipCount: ((current || 1) - 1) * (pageSize || 0), + sorting: sorting, + beginCreationTime, + endCreationTime, + ...filters, + }), + }); + + return { + data: query.items, + total: query.totalCount, + success: true, + }; + }} + pagination={{ defaultPageSize: 10, showSizeChanger: true }} + /> + + setModalVisible(false)} + onChange={() => actionRef.current?.reload()} + /> + + ); +}; + +export default WebhookSubscriptionTable; diff --git a/apps/react-admin/src/router/hooks/use-permission-routes.tsx b/apps/react-admin/src/router/hooks/use-permission-routes.tsx index cf3e510fa..cfd7e0a40 100644 --- a/apps/react-admin/src/router/hooks/use-permission-routes.tsx +++ b/apps/react-admin/src/router/hooks/use-permission-routes.tsx @@ -4,8 +4,6 @@ import { Navigate, Outlet } from "react-router"; import { Iconify } from "@/components/icon"; import { CircleLoading } from "@/components/loading"; -import { useUserPermission } from "@/store/userStore"; -import { flattenTrees } from "@/utils/tree"; import type { Permission } from "#/entity"; import { BasicStatus, PermissionType } from "#/enum"; diff --git a/apps/react-admin/src/router/routes/modules/dashboard.tsx b/apps/react-admin/src/router/routes/modules/dashboard.tsx index 077208a22..f70f8a3bb 100644 --- a/apps/react-admin/src/router/routes/modules/dashboard.tsx +++ b/apps/react-admin/src/router/routes/modules/dashboard.tsx @@ -6,7 +6,8 @@ import { CircleLoading } from "@/components/loading"; import type { AppRouteObject } from "#/router"; -const HomePage = lazy(() => import("@/pages/dashboard/workbench")); +// const HomePage = lazy(() => import("@/pages/dashboard/workbench")); +const HomePage = lazy(() => import("@/pages/platform/workbench/workbench-page")); const Analysis = lazy(() => import("@/pages/dashboard/analysis")); const dashboard: AppRouteObject = { diff --git a/apps/react-admin/src/router/routes/modules/management.tsx b/apps/react-admin/src/router/routes/modules/management.tsx index 2b5893200..696c5319e 100644 --- a/apps/react-admin/src/router/routes/modules/management.tsx +++ b/apps/react-admin/src/router/routes/modules/management.tsx @@ -6,13 +6,6 @@ import { CircleLoading } from "@/components/loading"; import type { AppRouteObject } from "#/router"; -// const ProfilePage = lazy(() => import("@/pages/management/user/profile")); -// const AccountPage = lazy(() => import("@/pages/management/user/account")); - -// const OrganizationPage = lazy(() => import("@/pages/management/system/organization")); -// const PermissioPage = lazy(() => import("@/pages/management/system/permission")); - -// const Blog = lazy(() => import("@/pages/management/blog")); // Identity const Users = lazy(() => import("@/pages/management/identity/users/user-table")); @@ -29,15 +22,36 @@ const PermissionGroupDefinition = lazy( const PermissionDefinitions = lazy( () => import("@/pages/management/permissions/permissions/permission-definition-table"), ); + +// Features +const FeatureGroupDefinition = lazy( + () => import("@/pages/management/features/definitions-groups/feature-group-definition-table"), +); +const FeatureDefinitions = lazy( + () => import("@/pages/management/features/definitions-features/feature-definition-table"), +); + // Auditing logs const AuditingAuditLogs = lazy(() => import("@/pages/management/audit-logs/audit-log-table")); +const Loggings = lazy(() => import("@/pages/management/loggings/logging-table")); // settings const SettingDefinitions = lazy(() => import("@/pages/management/settings/definitions/setting-definition-table")); const SystemSettings = lazy(() => import("@/pages/management/settings/settings/system-setting.tsx")); // notifications -const MyNotifications = lazy(() => import("@/pages/management/notifications/my-notification-table")); +const MyNotifications = lazy(() => import("@/pages/management/notifications/my-notifications/my-notification-table")); +const NotificationsGroupDefinition = lazy( + () => import("@/pages/management/notifications/definitions/groups/notification-group-definition-table"), +); +const NotificationsDefinition = lazy( + () => import("@/pages/management/notifications/definitions/notifications/notification-definition-table"), +); + +// Localization +const Languages = lazy(() => import("@/pages/management/localization/languages/localization-language-table")); +const Resources = lazy(() => import("@/pages/management/localization/resources/localization-resource-table")); +const Texts = lazy(() => import("@/pages/management/localization/texts/localization-text-table")); const management: AppRouteObject = { order: 2, @@ -57,59 +71,6 @@ const management: AppRouteObject = { index: true, element: , }, - // { - // path: "user", - // meta: { label: "sys.menu.user.index", key: "/management/user" }, - // children: [ - // { - // index: true, - // element: , - // }, - // { - // path: "profile", - // element: , - // meta: { - // label: "sys.menu.user.profile", - // key: "/management/user/profile", - // }, - // }, - // { - // path: "account", - // element: , - // meta: { - // label: "sys.menu.user.account", - // key: "/management/user/account", - // }, - // }, - // ], - // }, - // { - // path: "system", - // meta: { label: "sys.menu.system.index", key: "/management/system" }, - // children: [ - // { - // path: "organization", - // element: , - // meta: { - // label: "sys.menu.system.organization", - // key: "/management/system/organization", - // }, - // }, - // { - // path: "permission", - // element: , - // meta: { - // label: "sys.menu.system.permission", - // key: "/management/system/permission", - // }, - // }, - // ], - // }, - // { - // path: "blog", - // element: , - // meta: { label: "sys.menu.blog", key: "/management/blog" }, - // }, { path: "identity", meta: { @@ -178,6 +139,47 @@ const management: AppRouteObject = { }, ], }, + { + path: "localization", + meta: { + label: "abp.manage.localization.title", + key: "/management/localization", + icon: , + }, + children: [ + { + index: true, + element: , + }, + { + path: "resources", + element: , + meta: { + label: "abp.manage.localization.resources", + key: "/management/localization/resources", + icon: , + }, + }, + { + path: "languages", + element: , + meta: { + label: "abp.manage.localization.languages", + key: "/management/localization/languages", + icon: , + }, + }, + { + path: "texts", + element: , + meta: { + label: "abp.manage.localization.texts", + key: "/management/localization/texts", + icon: , + }, + }, + ], + }, { path: "permissions", meta: { @@ -242,15 +244,56 @@ const management: AppRouteObject = { }, ], }, + { + path: "features", + meta: { + label: "abp.manage.features.title", + key: "/management/features", + icon: , + }, + children: [ + { + index: true, + element: , + }, + { + path: "definitions", + element: , + meta: { + label: "abp.manage.features.definitions", + key: "/management/features/definitions", + icon: , + }, + }, + { + path: "groups", + element: , + meta: { + label: "abp.manage.features.groups", + key: "/management/features/groups", + icon: , + }, + }, + ], + }, { path: "audit-logs", element: , meta: { - label: "abp.manage.identity.auditLogs", + label: "abp.manage.auditLogs", key: "/management/audit-logs", icon: , }, }, + { + path: "sys-logs", + element: , + meta: { + label: "abp.manage.loggings", + key: "/management/sys-logs", + icon: , + }, + }, { path: "notifications", meta: { @@ -261,17 +304,35 @@ const management: AppRouteObject = { children: [ { index: true, - element: , + element: , }, { - path: "my-notifilers", + path: "my-notifications", element: , meta: { label: "abp.manage.notifications.myNotifilers", - key: "/management/notifications/my-notifilers", + key: "/management/notifications/my-notifications", icon: , }, }, + { + path: "groups", + element: , + meta: { + label: "abp.manage.notifications.groups", + key: "/management/notifications/groups", + icon: , + }, + }, + { + path: "definitions", + element: , + meta: { + label: "abp.manage.notifications.definitions", + key: "/management/notifications/definitions", + icon: , + }, + }, ], }, ], diff --git a/apps/react-admin/src/router/routes/modules/oss.tsx b/apps/react-admin/src/router/routes/modules/oss.tsx new file mode 100644 index 000000000..10e428f9b --- /dev/null +++ b/apps/react-admin/src/router/routes/modules/oss.tsx @@ -0,0 +1,50 @@ +import { Suspense, lazy } from "react"; + +import { Iconify } from "@/components/icon"; +import { CircleLoading } from "@/components/loading"; + +import type { AppRouteObject } from "#/router"; +import { Outlet } from "react-router"; + +const OssContainer = lazy(() => import("@/pages/oss/containers/container-table")); +const OssObjects = lazy(() => import("@/pages/oss/objects/oss-management")); + +const oss: AppRouteObject[] = [ + { + order: 3, + path: "oss", + element: ( + }> + + + ), + meta: { + label: "abp.oss.title", + icon: , + key: "/oss", + }, + + children: [ + { + path: "containers", + element: , + meta: { + icon: , + label: "abp.oss.containers", + key: "/oss/containers", + }, + }, + { + path: "objects", + element: , + meta: { + icon: , + label: "abp.oss.objects", + key: "/oss/objects", + }, + }, + ], + }, +]; + +export default oss; diff --git a/apps/react-admin/src/router/routes/modules/platform.tsx b/apps/react-admin/src/router/routes/modules/platform.tsx new file mode 100644 index 000000000..70e092855 --- /dev/null +++ b/apps/react-admin/src/router/routes/modules/platform.tsx @@ -0,0 +1,94 @@ +import { Suspense, lazy } from "react"; + +import { Iconify } from "@/components/icon"; +import { CircleLoading } from "@/components/loading"; + +import type { AppRouteObject } from "#/router"; +import { Navigate, Outlet } from "react-router"; + +const PlatformDataDictionaries = lazy(() => import("@/pages/platform/data-dictionaries/data-dictionary-table")); + +const PlatformLayouts = lazy(() => import("@/pages/platform/layouts/layout-table")); + +const PlatformMenus = lazy(() => import("@/pages/platform/menus/menu-table")); +const PlatformEmailMessages = lazy(() => import("@/pages/platform/messages/email/email-message-table")); +const PlatformSMSMessages = lazy(() => import("@/pages/platform/messages/sms/sms-message-table")); +const platform: AppRouteObject[] = [ + { + order: 4, + path: "platform", + element: ( + }> + + + ), + meta: { + label: "abp.platform.title", + icon: , + key: "/platform", + }, + children: [ + { + path: "data-dictionaries", + element: , + meta: { + icon: , + label: "abp.platform.dataDictionaries", + key: "/platform/data-dictionaries", + }, + }, + { + path: "layouts", + element: , + meta: { + icon: , + label: "abp.platform.layouts", + key: "/platform/layouts", + }, + }, + { + path: "menus", + element: , + meta: { + icon: , + label: "abp.platform.menus", + key: "/platform/menus", + }, + }, + { + path: "messages", + meta: { + icon: , + label: "abp.platform.messages.title", + key: "/platform/messages", + }, + children: [ + { + index: true, + element: , + }, + { + path: "email", + element: , + meta: { + label: "abp.platform.messages.email", + key: "/platform/messages/email", + icon: , + }, + }, + { + path: "sms", + element: , + meta: { + label: "abp.platform.messages.sms", + key: "/platform/messages/sms", + icon: , + }, + }, + ], + }, + ], + }, +]; + +export default platform; diff --git a/apps/react-admin/src/router/routes/modules/sass.tsx b/apps/react-admin/src/router/routes/modules/sass.tsx new file mode 100644 index 000000000..f8a5baaa2 --- /dev/null +++ b/apps/react-admin/src/router/routes/modules/sass.tsx @@ -0,0 +1,50 @@ +import { Suspense, lazy } from "react"; + +import { Iconify } from "@/components/icon"; +import { CircleLoading } from "@/components/loading"; + +import type { AppRouteObject } from "#/router"; +import { Outlet } from "react-router"; + +const SassEdition = lazy(() => import("@/pages/saas/editions/edition-table")); +const SassTalents = lazy(() => import("@/pages/saas/tenants/tenant-table")); + +const sass: AppRouteObject[] = [ + { + order: 6, + path: "/saas", + element: ( + }> + + + ), + meta: { + label: "abp.saas.title", + icon: , + key: "/saas", + }, + + children: [ + { + path: "editions", + element: , + meta: { + icon: , + label: "abp.saas.editions", + key: "/saas/editions", + }, + }, + { + path: "tenants", + element: , + meta: { + icon: , + label: "abp.saas.tenants", + key: "/saas/tenants", + }, + }, + ], + }, +]; + +export default sass; diff --git a/apps/react-admin/src/router/routes/modules/tasks.tsx b/apps/react-admin/src/router/routes/modules/tasks.tsx new file mode 100644 index 000000000..cbddf208e --- /dev/null +++ b/apps/react-admin/src/router/routes/modules/tasks.tsx @@ -0,0 +1,40 @@ +import { Suspense, lazy } from "react"; + +import { Iconify } from "@/components/icon"; +import { CircleLoading } from "@/components/loading"; + +import type { AppRouteObject } from "#/router"; +import { Outlet } from "react-router"; + +const BackgroundJobs = lazy(() => import("@/pages/tasks/job-info-table")); + +const tasks: AppRouteObject[] = [ + { + order: 4, + path: "task-management", + element: ( + }> + + + ), + meta: { + label: "abp.tasks.title", + icon: , + key: "/task-management", + }, + + children: [ + { + path: "background-jobs", + element: , + meta: { + icon: , + label: "abp.tasks.jobInfo.title", + key: "/task-management/background-jobs", + }, + }, + ], + }, +]; + +export default tasks; diff --git a/apps/react-admin/src/router/routes/modules/text-templating.tsx b/apps/react-admin/src/router/routes/modules/text-templating.tsx new file mode 100644 index 000000000..2e6ba2a14 --- /dev/null +++ b/apps/react-admin/src/router/routes/modules/text-templating.tsx @@ -0,0 +1,40 @@ +import { Suspense, lazy } from "react"; + +import { Iconify } from "@/components/icon"; +import { CircleLoading } from "@/components/loading"; + +import type { AppRouteObject } from "#/router"; +import { Outlet } from "react-router"; + +const TextTemplatingDefinitions = lazy(() => import("@/pages/text-templating/template-definition-table")); + +const textTemplating: AppRouteObject[] = [ + { + order: 7, + path: "text-templating", + element: ( + }> + + + ), + meta: { + label: "abp.textTemplating.title", + icon: , + key: "/text-templating", + }, + + children: [ + { + path: "definitions", + element: , + meta: { + icon: , + label: "abp.textTemplating.definitions", + key: "/text-templating/definitions", + }, + }, + ], + }, +]; + +export default textTemplating; diff --git a/apps/react-admin/src/router/routes/modules/webhooks.tsx b/apps/react-admin/src/router/routes/modules/webhooks.tsx new file mode 100644 index 000000000..11cc11cff --- /dev/null +++ b/apps/react-admin/src/router/routes/modules/webhooks.tsx @@ -0,0 +1,70 @@ +import { Suspense, lazy } from "react"; + +import { Iconify } from "@/components/icon"; +import { CircleLoading } from "@/components/loading"; + +import type { AppRouteObject } from "#/router"; +import { Outlet } from "react-router"; + +const WebhookGroupDefine = lazy(() => import("@/pages/webhooks/definitions/groups/webhook-group-definition-table")); + +const WebhookDefine = lazy(() => import("@/pages/webhooks/definitions/webhooks/webhook-definition-table")); + +const Subscriptions = lazy(() => import("@/pages/webhooks/subscriptions/webhook-subscription-table")); +const SendAttempts = lazy(() => import("@/pages/webhooks/send-attempts/webhook-send-attempt-table")); +const webhooks: AppRouteObject[] = [ + { + order: 5, + path: "webhooks", + element: ( + }> + + + ), + meta: { + label: "abp.webhooks.title", + icon: , + key: "/webhooks", + }, + children: [ + { + path: "groups", + element: , + meta: { + icon: , + label: "abp.webhooks.groups", + key: "/webhooks/groups", + }, + }, + { + path: "definitions", + element: , + meta: { + icon: , + label: "abp.webhooks.definitions", + key: "/webhooks/definitions", + }, + }, + { + path: "subscriptions", + element: , + meta: { + icon: , + label: "abp.webhooks.subscriptions", + key: "/webhooks/subscriptions", + }, + }, + { + path: "send-attempts", + element: , + meta: { + icon: , + label: "abp.webhooks.sendAttempts", + key: "/webhooks/send-attempts", + }, + }, + ], + }, +]; + +export default webhooks; diff --git a/apps/react-admin/src/store/abpCoreStore.ts b/apps/react-admin/src/store/abpCoreStore.ts index 1e34f835f..5fb77ec10 100644 --- a/apps/react-admin/src/store/abpCoreStore.ts +++ b/apps/react-admin/src/store/abpCoreStore.ts @@ -4,9 +4,12 @@ import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; type AbpStore = { + tenantId: string | undefined; + xsrfToken: string | undefined; application: ApplicationConfigurationDto | undefined; localization: ApplicationLocalizationDto | undefined; actions: { + setTenantId: (val?: string) => void; getI18nLocales: () => Record; setApplication: (val: ApplicationConfigurationDto) => void; setLocalization: (val: ApplicationLocalizationDto) => void; @@ -17,6 +20,8 @@ type AbpStore = { const useAbpStore = create()( persist( (set, get) => ({ + tenantId: undefined, + xsrfToken: undefined, application: undefined, localization: undefined, actions: { @@ -50,8 +55,16 @@ const useAbpStore = create()( }); return abpLocales; }, + setTenantId: (val) => set({ tenantId: val }), // 设置 application 数据 - setApplication: (val) => set({ application: val }), + setApplication: (val) => { + set({ application: val }); + const match = document.cookie.match(new RegExp("(^| )XSRF-TOKEN=([^;]+)")); + const xsrfToken = match ? match[2] : undefined; + set({ xsrfToken }); + // console.log("--------- xsrfToken set in store:", get().xsrfToken); + // 提取并设置 xsrfToken + }, // 设置 localization 数据 setLocalization: (val) => set({ localization: val }), // 重置 store diff --git a/apps/react-admin/src/store/abpSettingStore.ts b/apps/react-admin/src/store/abpSettingStore.ts new file mode 100644 index 000000000..2fec12b22 --- /dev/null +++ b/apps/react-admin/src/store/abpSettingStore.ts @@ -0,0 +1,90 @@ +import { create } from "zustand"; +import useAbpStore from "./abpCoreStore"; + +// Interface +export interface SettingValue { + name: string; + value: string | null; +} + +type AbpSettingStore = { + actions: { + get: (name: string) => string | undefined; + getOrEmpty: (name: string) => string; + getNumber: (name: string, defaultValue?: number) => number; + isTrue: (name: string) => boolean; + getAll: (...names: string[]) => SettingValue[]; + }; +}; + +const useAbpSettingStore = create()(() => ({ + actions: { + /** + * Gets a setting value by name. + */ + get: (name: string) => { + const values = useAbpStore.getState().application?.setting?.values; + return values ? values[name] : undefined; + }, + + /** + * Gets a setting value or returns an empty string if not found. + */ + getOrEmpty: (name: string) => { + const values = useAbpStore.getState().application?.setting?.values; + return values?.[name] ?? ""; + }, + + /** + * Gets a setting value as a number. + */ + getNumber: (name: string, defaultValue = 0) => { + const values = useAbpStore.getState().application?.setting?.values; + const value = values?.[name]; + + if (value === undefined || value === null) { + return defaultValue; + } + + const num = Number(value); + return Number.isNaN(num) ? defaultValue : num; + }, + + /** + * Checks if a setting value is "true" (case-insensitive). + */ + isTrue: (name: string) => { + const values = useAbpStore.getState().application?.setting?.values; + const value = values?.[name]; + return value?.toLowerCase() === "true"; + }, + + /** + * Gets all settings, optionally filtered by specific names. + * Returns them as an array of objects { name, value } to match Vue behavior. + */ + getAll: (...names: string[]) => { + const values = useAbpStore.getState().application?.setting?.values; + if (!values) return []; + + const settingsSet: SettingValue[] = Object.keys(values).map((key) => ({ + name: key, + value: values[key], + })); + + if (names.length > 0) { + return settingsSet.filter((setting) => names.includes(setting.name)); + } + + return settingsSet; + }, + }, +})); + +// Export actions directly for ease of use +export const useAbpSettings = () => useAbpSettingStore((state) => state.actions); + +// Optional: specific hooks if it need reactive re-renders in components +export const useSettingValue = (name: string) => useAbpStore((state) => state.application?.setting?.values?.[name]); + +export default useAbpSettingStore; diff --git a/apps/react-admin/src/store/userStore.ts b/apps/react-admin/src/store/userStore.ts index 43745568c..8be196979 100644 --- a/apps/react-admin/src/store/userStore.ts +++ b/apps/react-admin/src/store/userStore.ts @@ -6,12 +6,13 @@ import { createJSONStorage, persist } from "zustand/middleware"; import { toast } from "sonner"; import type { UserInfo, UserToken } from "#/entity"; import { StorageEnum } from "#/enum"; -import { getUserInfoApi, loginApi } from "@/api/account"; -import type { PasswordTokenRequestModel } from "#/account"; +import { externalLoginApi, getUserInfoApi, loginApi } from "@/api/account"; +import type { PasswordTokenRequestModel, SignInRedirectResult, TokenResult } from "#/account"; import { getConfigApi } from "@/api/abp-core"; import useAbpStore from "./abpCoreStore"; import { useEventBus } from "@/utils/abp/useEventBus"; import { Events } from "@/constants/abp-core"; +import { getPictureApi } from "@/api/account/profile"; const { VITE_APP_HOMEPAGE: HOMEPAGE } = import.meta.env; @@ -50,37 +51,108 @@ const useUserStore = create()( publish(Events.UserLogout); set({ userInfo: {}, userToken: {} }); }, + // fetchAndSetUser: async () => { + // let userInfo: ({ [key: string]: any } & UserInfo) | null = null; + + // try { + // const userInfoRes = await getUserInfoApi(); + // const abpConfig = await getConfigApi(); + // const picture = await getPictureApi(); + // userInfo = { + // id: userInfoRes.sub, //额外加的 + // userId: userInfoRes.sub, + // username: userInfoRes.uniqueName ?? abpConfig.currentUser.userName, + // realName: userInfoRes.name ?? abpConfig.currentUser.name, + // // avatar: userInfoRes.avatarUrl ?? userInfoRes.picture, + // avatar: URL.createObjectURL(picture) ?? "", + // desc: userInfoRes.uniqueName ?? userInfoRes.name, + // email: userInfoRes.email ?? userInfoRes.email, + // emailVerified: userInfoRes.emailVerified ?? abpConfig.currentUser.emailVerified, + // phoneNumber: userInfoRes.phoneNumber ?? abpConfig.currentUser.phoneNumber, + // phoneNumberVerified: userInfoRes.phoneNumberVerified ?? abpConfig.currentUser.phoneNumberVerified, + // token: "", + // roles: abpConfig.currentUser.roles, + // homePath: "/", + // }; + + // // 更新一系列到 zustand store 中 + // set({ userInfo }); + + // useAbpStore.getState().actions.setApplication(abpConfig); + + // set({ accessCodes: Object.keys(abpConfig.auth.grantedPolicies) }); + // } catch (err) { + // console.error("Failed to fetch user info:", err); + // } + + // return userInfo; + // }, fetchAndSetUser: async () => { let userInfo: ({ [key: string]: any } & UserInfo) | null = null; + // 1. Run all requests in parallel. + const [userInfoResult, configResult, pictureResult] = await Promise.allSettled([ + getUserInfoApi(), + getConfigApi(), + getPictureApi(), + ]); + + // 2. Check Critical Dependency + if (configResult.status === "rejected") { + console.error("Critical: Failed to fetch ABP Config", configResult.reason); + return null; + } + const abpConfig = configResult.value; + + // 3. Handle User Info (Fix applied here) + const userInfoRes = userInfoResult.status === "fulfilled" ? userInfoResult.value : ({} as any); + + if (userInfoResult.status === "rejected") { + console.warn("Non-Critical: Failed to fetch /connect/userinfo", userInfoResult.reason); + } + + // 4. Handle Picture + let avatarUrl = ""; + if (pictureResult.status === "fulfilled" && pictureResult.value) { + try { + avatarUrl = URL.createObjectURL(pictureResult.value); + } catch (e) { + console.warn("Failed to create object URL for avatar", e); + } + } + + // 5. Construct UserInfo + const currentUser = abpConfig.currentUser || {}; + + userInfo = { + // Now these accessors will not throw TS errors because userInfoRes is typed as 'any' in the fallback case + id: userInfoRes.sub ?? currentUser.id ?? "", + userId: userInfoRes.sub ?? currentUser.id ?? "", + username: userInfoRes.uniqueName ?? currentUser.userName ?? "", + realName: userInfoRes.name ?? currentUser.name ?? "", + desc: userInfoRes.uniqueName ?? userInfoRes.name ?? currentUser.userName ?? "", + email: userInfoRes.email ?? currentUser.email ?? "", + emailVerified: userInfoRes.emailVerified ?? currentUser.emailVerified ?? false, + phoneNumber: userInfoRes.phoneNumber ?? currentUser.phoneNumber ?? "", + phoneNumberVerified: userInfoRes.phoneNumberVerified ?? currentUser.phoneNumberVerified ?? false, + roles: currentUser.roles ?? [], + token: "", + homePath: "/", + avatar: avatarUrl, + }; + + // 6. Update Stores try { - const userInfoRes = await getUserInfoApi(); - const abpConfig = await getConfigApi(); - - userInfo = { - id: userInfoRes.sub, //额外加的 - userId: userInfoRes.sub, - username: userInfoRes.uniqueName ?? abpConfig.currentUser.userName, - realName: userInfoRes.name ?? abpConfig.currentUser.name, - avatar: userInfoRes.avatarUrl ?? userInfoRes.picture, - desc: userInfoRes.uniqueName ?? userInfoRes.name, - email: userInfoRes.email ?? userInfoRes.email, - emailVerified: userInfoRes.emailVerified ?? abpConfig.currentUser.emailVerified, - phoneNumber: userInfoRes.phoneNumber ?? abpConfig.currentUser.phoneNumber, - phoneNumberVerified: userInfoRes.phoneNumberVerified ?? abpConfig.currentUser.phoneNumberVerified, - token: "", - roles: abpConfig.currentUser.roles, - homePath: "/", - }; - - // 更新一系列到 zustand store 中 set({ userInfo }); - useAbpStore.getState().actions.setApplication(abpConfig); - set({ accessCodes: Object.keys(abpConfig.auth.grantedPolicies) }); - } catch (err) { - console.error("Failed to fetch user info:", err); + if (abpConfig.auth?.grantedPolicies) { + set({ accessCodes: Object.keys(abpConfig.auth.grantedPolicies) }); + } else { + set({ accessCodes: [] }); + } + } catch (storeError) { + console.error("Failed to update app state", storeError); } return userInfo; @@ -135,5 +207,43 @@ export const useSignIn = () => { return signIn; }; +// 添加类型守卫 +function isTokenResult(res: TokenResult | SignInRedirectResult): res is TokenResult { + return "accessToken" in res; +} +export const useExternalSignIn = (handleRegister: (res: SignInRedirectResult) => void) => { + const navigatge = useNavigate(); + const { setUserToken, fetchAndSetUser } = useUserActions(); + + const externalSignInMutation = useMutation({ + mutationFn: externalLoginApi, + retry: 0, + }); + + const externalSignIn = async () => { + try { + const res = await externalSignInMutation.mutateAsync(); + + if (isTokenResult(res)) { + const { tokenType, accessToken, refreshToken } = res; + // 如果成功获取到 accessToken + if (accessToken) { + setUserToken({ accessToken: `${tokenType} ${accessToken}`, refreshToken }); + + await fetchAndSetUser(); + + navigatge(HOMEPAGE, { replace: true }); + toast.success("Sign in success!"); + } + } else { + handleRegister(res); + } + } catch (err) { + console.error(err.message); + } + }; + + return externalSignIn; +}; export default useUserStore; diff --git a/apps/react-admin/src/utils/abp/is.ts b/apps/react-admin/src/utils/abp/is.ts new file mode 100644 index 000000000..fcec907fb --- /dev/null +++ b/apps/react-admin/src/utils/abp/is.ts @@ -0,0 +1,22 @@ +export { default as isDate } from "lodash.isdate"; +export { default as isNumber } from "lodash.isnumber"; + +export function isNullAndUnDef(val: unknown): val is null | undefined { + return isUnDef(val) && isNull(val); +} + +export function isNullOrUnDef(val: unknown): val is null | undefined { + return isUnDef(val) || isNull(val); +} + +export function isDef(val?: T): val is T { + return val !== undefined; +} + +export function isUnDef(val?: T): val is T { + return !isDef(val); +} + +export function isNull(value: any): value is null { + return value === null; +} diff --git a/apps/react-admin/src/utils/inference.ts b/apps/react-admin/src/utils/inference.ts new file mode 100644 index 000000000..2da647dc2 --- /dev/null +++ b/apps/react-admin/src/utils/inference.ts @@ -0,0 +1,111 @@ +/** + * 检查传入的值是否为undefined。 + * + * @param {unknown} value 要检查的值。 + * @returns {boolean} 如果值是undefined,返回true,否则返回false。 + */ +function isUndefined(value?: unknown): value is undefined { + return value === undefined; +} + +/** + * 检查传入的值是否为boolean + * @param value + * @returns 如果值是布尔值,返回true,否则返回false。 + */ +function isBoolean(value: unknown): value is boolean { + return typeof value === "boolean"; +} + +/** + * 检查传入的字符串是否为有效的HTTP或HTTPS URL。 + * + * @param {string} url 要检查的字符串。 + * @return {boolean} 如果字符串是有效的HTTP或HTTPS URL,返回true,否则返回false。 + */ +function isHttpUrl(url?: string): boolean { + if (!url) { + return false; + } + // 使用正则表达式测试URL是否以http:// 或 https:// 开头 + const httpRegex = /^https?:\/\/.*$/; + return httpRegex.test(url); +} + +/** + * 检查传入的值是否为window对象。 + * + * @param {any} value 要检查的值。 + * @returns {boolean} 如果值是window对象,返回true,否则返回false。 + */ +function isWindow(value: any): value is Window { + return typeof window !== "undefined" && value !== null && value === value.window; +} + +/** + * 检查当前运行环境是否为Mac OS。 + * + * 这个函数通过检查navigator.userAgent字符串来判断当前运行环境。 + * 如果userAgent字符串中包含"macintosh"或"mac os x"(不区分大小写),则认为当前环境是Mac OS。 + * + * @returns {boolean} 如果当前环境是Mac OS,返回true,否则返回false。 + */ +function isMacOs(): boolean { + const macRegex = /macintosh|mac os x/i; + return macRegex.test(navigator.userAgent); +} + +/** + * 检查当前运行环境是否为Windows OS。 + * + * 这个函数通过检查navigator.userAgent字符串来判断当前运行环境。 + * 如果userAgent字符串中包含"windows"或"win32"(不区分大小写),则认为当前环境是Windows OS。 + * + * @returns {boolean} 如果当前环境是Windows OS,返回true,否则返回false。 + */ +function isWindowsOs(): boolean { + const windowsRegex = /windows|win32/i; + return windowsRegex.test(navigator.userAgent); +} + +/** + * 检查传入的值是否为数字 + * @param value + */ +function isNumber(value: any): value is number { + return typeof value === "number" && Number.isFinite(value); +} + +/** + * Returns the first value in the provided list that is neither `null` nor `undefined`. + * + * This function iterates over the input values and returns the first one that is + * not strictly equal to `null` or `undefined`. If all values are either `null` or + * `undefined`, it returns `undefined`. + * + * @template T - The type of the input values. + * @param {...(T | null | undefined)[]} values - A list of values to evaluate. + * @returns {T | undefined} - The first value that is not `null` or `undefined`, or `undefined` if none are found. + * + * @example + * // Returns 42 because it is the first non-null, non-undefined value. + * getFirstNonNullOrUndefined(undefined, null, 42, 'hello'); // 42 + * + * @example + * // Returns 'hello' because it is the first non-null, non-undefined value. + * getFirstNonNullOrUndefined(null, undefined, 'hello', 123); // 'hello' + * + * @example + * // Returns undefined because all values are either null or undefined. + * getFirstNonNullOrUndefined(undefined, null); // undefined + */ +function getFirstNonNullOrUndefined(...values: (null | T | undefined)[]): T | undefined { + for (const value of values) { + if (value !== undefined && value !== null) { + return value; + } + } + return undefined; +} + +export { getFirstNonNullOrUndefined, isBoolean, isHttpUrl, isMacOs, isNumber, isUndefined, isWindow, isWindowsOs }; diff --git a/apps/react-admin/types/abp-core/global.ts b/apps/react-admin/types/abp-core/global.ts index 4f1d4d9ba..363181678 100644 --- a/apps/react-admin/types/abp-core/global.ts +++ b/apps/react-admin/types/abp-core/global.ts @@ -43,6 +43,7 @@ interface IHasExtraProperties { } /** 选择项 */ interface ISelectionStringValueItem { + [key: string]: any; /** 选择项显示文本多语言对象 */ displayText: LocalizableStringInfo; /** 选择项值 */ @@ -211,11 +212,12 @@ interface IHasSimpleStateCheckers stateCheckers: ISimpleStateChecker[]; } -type SimpleStateRecord, TValue> = { - [P in keyof TState]: TValue; -}; +// type SimpleStateRecord, TValue> = { +// [P in keyof TState]: TValue; +// }; -type SimpleStateCheckerResult> = SimpleStateRecord; +// type SimpleStateCheckerResult> = SimpleStateRecord; +type SimpleStateCheckerResult = Map; interface SimpleStateCheckerContext> { state: TState; diff --git a/apps/react-admin/types/abp-core/index.ts b/apps/react-admin/types/abp-core/index.ts index b53a11785..cc1cf2809 100644 --- a/apps/react-admin/types/abp-core/index.ts +++ b/apps/react-admin/types/abp-core/index.ts @@ -1,3 +1,6 @@ export * from "./dto"; export * from "./global"; export * from "./table"; +export * from "./permissions"; +export * from "./rules"; +export * from "./validations"; diff --git a/apps/react-admin/types/abp-core/permissions.ts b/apps/react-admin/types/abp-core/permissions.ts new file mode 100644 index 000000000..85afe3cb1 --- /dev/null +++ b/apps/react-admin/types/abp-core/permissions.ts @@ -0,0 +1,6 @@ +interface IPermissionChecker { + authorize(name: string | string[]): void; + isGranted(name: string | string[], requiresAll?: boolean): boolean; +} + +export type { IPermissionChecker }; diff --git a/apps/react-admin/types/abp-core/rules.ts b/apps/react-admin/types/abp-core/rules.ts new file mode 100644 index 000000000..5eb6ab8eb --- /dev/null +++ b/apps/react-admin/types/abp-core/rules.ts @@ -0,0 +1,52 @@ +import type { + Field, + FieldBeetWeen, + FieldContains, + FieldDefineValidator, + FieldLength, + FieldMatch, + FieldRange, + FieldRegular, + FieldValidator, + Rule, +} from "./validations"; + +/** 规则创建器 */ +interface RuleCreator { + /** 自定义验证器 */ + defineValidator(field: FieldDefineValidator): Rule[]; + /** input 与 value 是否匹配 */ + doNotMatch(field: FieldMatch): Rule[]; + /** 字段{0}不是有效的信用卡号码 */ + fieldDoNotValidCreditCardNumber(field: Field): Rule[]; + /** 字段{0}不是有效的邮箱地址 */ + fieldDoNotValidEmailAddress(field: Field): Rule[]; + /** 字段{0}不是有效的完全限定的http,https或ftp URL. */ + fieldDoNotValidFullyQualifiedUrl(field: Field): Rule[]; + /** 字段{0}不是有效的手机号码 */ + fieldDoNotValidPhoneNumber(field: Field): Rule[]; + /** 字段是无效值 */ + fieldInvalid(field: FieldValidator): Rule[]; + /** 验证未通过 */ + fieldIsNotValid(field: FieldValidator): Rule[]; + /** 字段{0}值必须在{1}和{2}范围内 */ + fieldMustBeetWeen(field: FieldBeetWeen): Rule[]; + /** 字段{0}必须是最大长度为'{1}'的字符串或数组 */ + fieldMustBeStringOrArrayWithMaximumLength(field: FieldLength): Rule[]; + /** 字段{0}必须是最小长度为'{1}'的字符串或数组 */ + fieldMustBeStringOrArrayWithMinimumLength(field: FieldLength): Rule[]; + /** 字段{0}必须是最大长度为{1}的字符串 */ + fieldMustBeStringWithMaximumLength(field: FieldLength): Rule[]; + /** 字段{0}必须是最小长度为{2}并且最大长度{1}的字符串 */ + fieldMustBeStringWithMinimumLengthAndMaximumLength(field: FieldRange): Rule[]; + /** 字段{0}与正则表达式不匹配 */ + fieldMustMatchRegularExpression(field: FieldRegular): Rule[]; + /** {0}字段只允许以下扩展名的文件: {1} */ + fieldOnlyAcceptsFilesExtensions(field: FieldContains): Rule[]; + /** 字段{0}不可为空 */ + fieldRequired(field: Field): Rule[]; + /** 获取一个错误枚举验证消息 */ + mapEnumValidMessage(enumName: string, args?: any[] | Record | undefined): string; +} + +export type { RuleCreator }; diff --git a/apps/react-admin/types/abp-core/validations.ts b/apps/react-admin/types/abp-core/validations.ts new file mode 100644 index 000000000..32bce6a4d --- /dev/null +++ b/apps/react-admin/types/abp-core/validations.ts @@ -0,0 +1,106 @@ +type RuleType = + | "boolean" + | "date" + | "email" + | "enum" + | "float" + | "hex" + | "integer" + | "method" + | "number" + | "object" + | "regexp" + | "string" + | "url"; + +interface Rule { + [key: string]: any; + trigger?: "blur" | "change" | ["change", "blur"]; + type?: "array" | RuleType; +} + +interface Field extends Rule { + /** 连接符 + * @description 用于本地化字段名称时的连接字符 + * @example . => L('ResourceName.DisplayName.Field') + * @example : => L('ResourceName.DisplayName:Field') + */ + connector?: string; + /** 字段名称 */ + name?: string; + /** 字段前缀 + * @description 用于本地化字段名称 + * @example DisplayName => L('ResourceName.DisplayName:Field') + */ + prefix?: string; + /** 本地化资源 */ + resourceName?: string; +} + +interface FieldRequired extends Field { + /** 是否必须 */ + required?: boolean; +} + +interface FieldBeetWeen extends Field { + /** 结束值 */ + end: number; + /** 起始值 */ + start: number; +} + +interface FieldLength extends Field { + /** 长度 */ + length: number; +} + +interface FieldRange extends Field { + /** 最大数值 */ + maximum: number; + /** 最小数值 */ + minimum: number; +} + +interface FieldRegular extends Field { + /** 正则表达式 */ + expression: string; +} + +interface FieldMatch extends FieldRequired { + /** 对比字段 */ + matchField: string; + /** 对比字段值 */ + matchValue: string; + /** 字段名称 */ + name: string; +} + +interface FieldContains extends Field { + /** 验证的值中是否包含在定义的值中 */ + value: string; +} + +interface FieldValidator extends FieldRequired { + /** 值是否有效验证器 */ + validator: (value: any) => boolean; +} + +interface FieldDefineValidator extends FieldRequired { + message?: string; + validator: (rule: any, value: any, callback: any, source?: any, options?: any) => Promise | void; +} + +export type { + Field, + FieldBeetWeen, + FieldContains, + FieldDefineValidator, + FieldLength, + FieldMatch, + FieldRange, + FieldRegular, + FieldRequired, + FieldValidator, + Rule, + RuleType, +}; diff --git a/apps/react-admin/types/account/account.ts b/apps/react-admin/types/account/account.ts index 4d488a145..4d4471125 100644 --- a/apps/react-admin/types/account/account.ts +++ b/apps/react-admin/types/account/account.ts @@ -14,4 +14,22 @@ interface SendPhoneSigninCodeDto { type TwoFactorProvider = NameValue; -export type { GetTwoFactorProvidersInput, SendEmailSigninCodeDto, SendPhoneSigninCodeDto, TwoFactorProvider }; +interface ExternalSignUpApiDto { + userName: string; + emailAddress: string; +} + +interface PhoneResetPasswordDto { + code: string; + newPassword: string; + phoneNumber: string; +} + +export type { + GetTwoFactorProvidersInput, + SendEmailSigninCodeDto, + SendPhoneSigninCodeDto, + PhoneResetPasswordDto, + TwoFactorProvider, + ExternalSignUpApiDto, +}; diff --git a/apps/react-admin/types/account/bind.ts b/apps/react-admin/types/account/bind.ts new file mode 100644 index 000000000..cff42adb9 --- /dev/null +++ b/apps/react-admin/types/account/bind.ts @@ -0,0 +1,17 @@ +import type { ButtonType } from "antd/es/button"; + +interface BindButton { + click: () => Promise | void; + title: string; + type?: ButtonType; +} + +interface BindItem { + buttons?: BindButton[]; + description?: string; + enable?: boolean; + slot?: string; + title: string; +} + +export type { BindItem }; diff --git a/apps/react-admin/types/account/external-logins.ts b/apps/react-admin/types/account/external-logins.ts new file mode 100644 index 000000000..21f7d94ed --- /dev/null +++ b/apps/react-admin/types/account/external-logins.ts @@ -0,0 +1,32 @@ +interface UserLoginInfoDto { + loginProvider: string; + providerDisplayName: string; + providerKey: string; +} + +interface ExternalLoginInfoDto { + displayName: string; + name: string; +} + +interface WorkWeixinLoginBindInput { + code: string; +} + +interface ExternalLoginResultDto { + externalLogins: ExternalLoginInfoDto[]; + userLogins: UserLoginInfoDto[]; +} + +interface RemoveExternalLoginInput { + loginProvider: string; + providerKey: string; +} + +export type { + ExternalLoginInfoDto, + ExternalLoginResultDto, + RemoveExternalLoginInput, + UserLoginInfoDto, + WorkWeixinLoginBindInput, +}; diff --git a/apps/react-admin/types/account/profile.ts b/apps/react-admin/types/account/profile.ts index 2777c3f58..72e9b3c51 100644 --- a/apps/react-admin/types/account/profile.ts +++ b/apps/react-admin/types/account/profile.ts @@ -36,6 +36,19 @@ interface ChangePasswordInput { newPassword: string; } +interface ChangePhoneNumberInput { + code: string; + newPhoneNumber: string; +} + +interface SendChangePhoneNumberCodeInput { + newPhoneNumber: string; +} + +interface ChangePictureInput { + file: File; +} + interface TwoFactorEnabledDto { /** 是否启用二次认证 */ enabled: boolean; @@ -69,8 +82,11 @@ export type { AuthenticatorDto, AuthenticatorRecoveryCodeDto, ChangePasswordInput, + ChangePhoneNumberInput, + ChangePictureInput, ConfirmEmailInput, ProfileDto, + SendChangePhoneNumberCodeInput, SendEmailConfirmCodeDto, TwoFactorEnabledDto, UpdateProfileDto, diff --git a/apps/react-admin/types/account/qrcode.ts b/apps/react-admin/types/account/qrcode.ts new file mode 100644 index 000000000..f9fadf98f --- /dev/null +++ b/apps/react-admin/types/account/qrcode.ts @@ -0,0 +1,28 @@ +export enum QrCodeStatus { + /** 已确认 */ + Confirmed = 10, + /** 创建 */ + Created = 0, + /** 无效 */ + Invalid = -1, + /** 已扫描 */ + Scaned = 5, +} + +interface GenerateQrCodeResult { + key: string; +} + +interface QrCodeInfoResult { + key: string; + status: QrCodeStatus; +} + +interface QrCodeUserInfoResult extends QrCodeInfoResult { + picture?: string; + tenantId?: string; + userId?: string; + userName?: string; +} + +export type { GenerateQrCodeResult, QrCodeUserInfoResult }; diff --git a/apps/react-admin/types/account/token.ts b/apps/react-admin/types/account/token.ts index 859647a7f..39a287ff4 100644 --- a/apps/react-admin/types/account/token.ts +++ b/apps/react-admin/types/account/token.ts @@ -14,17 +14,36 @@ interface PasswordTokenRequest extends TokenRequest { /** 用户名 */ userName: string; } +/** 手机号授权请求数据模型 */ +interface PhoneNumberTokenRequest { + [key: string]: any; + /** 验证码 */ + code: string; + /** 手机号 */ + phoneNumber: string; +} +/** 扫码登录授权请求数据模型 */ +interface QrCodeTokenRequest { + [key: string]: any; + /** 二维码Key */ + key: string; + /** 租户Id */ + tenantId?: string; +} /** 用户密码授权请求数据模型 */ interface PasswordTokenRequestModel { + [key: string]: any; /** 用户密码 */ password: string; /** 用户名 */ username: string; } -/** 用户刷新令牌请求数据模型 */ -interface RefreshTokenRequestModel { - /** 用户密码 */ - refreshToken: string; +/** 令牌撤销请求数据类型 */ +interface RevokeTokenRequest { + /** 令牌 */ + token: string; + /** 令牌类型 */ + tokenType?: "access_token" | "refresh_token"; } /** 令牌返回数据模型 */ interface TokenResult { @@ -37,6 +56,10 @@ interface TokenResult { /** 令牌类型 */ tokenType: string; } +interface OAuthTokenRefreshModel { + /** 刷新令牌 */ + refreshToken: string; +} /** oauth标准令牌返回结构 */ interface OAuthTokenResult { /** 访问令牌 */ @@ -49,11 +72,50 @@ interface OAuthTokenResult { token_type: string; } +interface OAuthError { + /** 错误类型 */ + error: string; + /** 错误描述 */ + error_description: string; + /** 错误描述链接 */ + error_uri?: string; +} + +interface TwoFactorError extends OAuthError { + twoFactorToken: string; + userId: string; +} + +interface ShouldChangePasswordError extends OAuthError { + changePasswordToken: string; + userId: string; +} + +/** 用户刷新令牌请求数据模型 */ +interface RefreshTokenRequestModel { + /** 用户密码 */ + refreshToken: string; +} + +interface SignInRedirectResult { + isExternalLogin: boolean; + needRegister: boolean; + redirectUrl?: string; +} + export type { + OAuthError, + OAuthTokenRefreshModel, OAuthTokenResult, PasswordTokenRequest, PasswordTokenRequestModel, + PhoneNumberTokenRequest, + QrCodeTokenRequest, + RevokeTokenRequest, + ShouldChangePasswordError, TokenRequest, TokenResult, + TwoFactorError, RefreshTokenRequestModel, + SignInRedirectResult, }; diff --git a/apps/react-admin/types/management/auditing/index.ts b/apps/react-admin/types/management/auditing/index.ts index 1c76005b1..9d859e749 100644 --- a/apps/react-admin/types/management/auditing/index.ts +++ b/apps/react-admin/types/management/auditing/index.ts @@ -1,2 +1,3 @@ export * from "./audit-logs"; export * from "./entity-changes"; +export * from "./loggings"; diff --git a/apps/react-admin/types/management/auditing/loggings.ts b/apps/react-admin/types/management/auditing/loggings.ts new file mode 100644 index 000000000..f18f3d64f --- /dev/null +++ b/apps/react-admin/types/management/auditing/loggings.ts @@ -0,0 +1,67 @@ +import type { PagedAndSortedResultRequestDto } from "#/abp-core/dto"; + +interface LogExceptionDto { + class?: string; + depth?: number; + helpUrl?: string; + hResult?: number; + message?: string; + source?: string; + stackTrace?: string; +} + +interface LogFieldDto { + actionId?: string; + actionName?: string; + application?: string; + clientId?: string; + connectionId?: string; + context?: string; + correlationId?: string; + environment?: string; + id: string; + machineName?: string; + processId?: number; + requestId?: string; + requestPath?: string; + threadId?: number; + userId?: string; +} + +enum LogLevel { + Critical = 5, + Debug = 1, + Error = 4, + Information = 2, + None = 6, + Trace = 0, + Warning = 3, +} + +interface LogDto { + exceptions: LogExceptionDto[]; + fields: LogFieldDto; + level: LogLevel; + message: string; + timeStamp: Date; +} + +interface LogGetListInput extends PagedAndSortedResultRequestDto { + application?: string; + context?: string; + correlationId?: string; + endTime?: Date; + environment?: string; + hasException?: boolean; + level?: LogLevel; + machineName?: string; + processId?: number; + requestId?: string; + requestPath?: string; + startTime?: Date; + threadId?: number; +} + +export type { LogDto, LogExceptionDto, LogFieldDto, LogGetListInput }; + +export { LogLevel }; diff --git a/apps/react-admin/types/management/features/definitions.ts b/apps/react-admin/types/management/features/definitions.ts new file mode 100644 index 000000000..bc876e5e1 --- /dev/null +++ b/apps/react-admin/types/management/features/definitions.ts @@ -0,0 +1,44 @@ +import type { IHasConcurrencyStamp, IHasExtraProperties } from "#/abp-core"; + +interface FeatureDefinitionDto extends IHasExtraProperties { + allowedProviders: string[]; + defaultValue?: string; + description?: string; + displayName: string; + groupName: string; + isAvailableToHost: boolean; + isStatic: boolean; + isVisibleToClients: boolean; + name: string; + parentName?: string; + valueType: string; +} + +interface FeatureDefinitionGetListInput { + filter?: string; + groupName?: string; +} + +interface FeatureDefinitionCreateOrUpdateDto extends IHasExtraProperties { + allowedProviders: string[]; + defaultValue?: string; + description?: string; + displayName: string; + isAvailableToHost: boolean; + isVisibleToClients: boolean; + parentName?: string; + valueType: string; +} + +interface FeatureDefinitionUpdateDto extends FeatureDefinitionCreateOrUpdateDto, IHasConcurrencyStamp {} +interface FeatureDefinitionCreateDto extends FeatureDefinitionCreateOrUpdateDto { + groupName: string; + name: string; +} + +export type { + FeatureDefinitionCreateDto, + FeatureDefinitionDto, + FeatureDefinitionGetListInput, + FeatureDefinitionUpdateDto, +}; diff --git a/apps/react-admin/types/management/features/features.ts b/apps/react-admin/types/management/features/features.ts new file mode 100644 index 000000000..c1d878f47 --- /dev/null +++ b/apps/react-admin/types/management/features/features.ts @@ -0,0 +1,63 @@ +import type { Dictionary, NameValue } from "#/abp-core"; + +interface FeatureProvider { + providerKey?: string; + providerName: string; +} + +interface IValueValidator { + [key: string]: any; + isValid(value?: any): boolean; + name: string; + properties: Dictionary; +} + +interface IStringValueType { + [key: string]: any; + name: string; + properties: Dictionary; + validator: IValueValidator; +} + +interface FeatureProviderDto { + key: string; + name: string; +} + +interface FeatureDto { + depth: number; + description?: string; + displayName: string; + name: string; + parentName?: string; + provider: FeatureProviderDto; + value?: any; + valueType: IStringValueType; +} + +interface FeatureGroupDto { + displayName: string; + features: FeatureDto[]; + name: string; +} + +interface GetFeatureListResultDto { + groups: FeatureGroupDto[]; +} + +type UpdateFeatureDto = NameValue; + +interface UpdateFeaturesDto { + features: UpdateFeatureDto[]; +} + +export type { + FeatureDto, + FeatureGroupDto, + FeatureProvider, + GetFeatureListResultDto, + IStringValueType, + IValueValidator, + UpdateFeatureDto, + UpdateFeaturesDto, +}; diff --git a/apps/react-admin/types/management/features/groups.ts b/apps/react-admin/types/management/features/groups.ts new file mode 100644 index 000000000..8a50b9efb --- /dev/null +++ b/apps/react-admin/types/management/features/groups.ts @@ -0,0 +1,28 @@ +import type { IHasConcurrencyStamp, IHasExtraProperties } from "#/abp-core"; + +interface FeatureGroupDefinitionDto extends IHasExtraProperties { + displayName: string; + isStatic: boolean; + name: string; +} + +interface FeatureGroupDefinitionGetListInput { + filter?: string; +} + +interface FeatureGroupDefinitionCreateOrUpdateDto extends IHasExtraProperties { + displayName: string; +} + +interface FeatureGroupDefinitionUpdateDto extends FeatureGroupDefinitionCreateOrUpdateDto, IHasConcurrencyStamp {} + +interface FeatureGroupDefinitionCreateDto extends FeatureGroupDefinitionCreateOrUpdateDto { + name: string; +} + +export type { + FeatureGroupDefinitionCreateDto, + FeatureGroupDefinitionDto, + FeatureGroupDefinitionGetListInput, + FeatureGroupDefinitionUpdateDto, +}; diff --git a/apps/react-admin/types/management/features/index.ts b/apps/react-admin/types/management/features/index.ts new file mode 100644 index 000000000..19710ccd1 --- /dev/null +++ b/apps/react-admin/types/management/features/index.ts @@ -0,0 +1,3 @@ +export * from "./definitions"; +export * from "./features"; +export * from "./groups"; diff --git a/apps/react-admin/types/management/identity/claim-types.ts b/apps/react-admin/types/management/identity/claim-types.ts index 9afa3b1ca..816e1d655 100644 --- a/apps/react-admin/types/management/identity/claim-types.ts +++ b/apps/react-admin/types/management/identity/claim-types.ts @@ -36,28 +36,9 @@ interface GetIdentityClaimTypePagedListInput extends PagedAndSortedResultRequest filter?: string; } -interface IdentityClaimDto { - claimType: string; - claimValue: string; -} - -interface IdentityClaimDeleteDto { - claimType: string; - claimValue: string; -} - -type IdentityClaimCreateDto = IdentityClaimDeleteDto; - -interface IdentityClaimUpdateDto extends IdentityClaimDeleteDto { - newClaimValue: string; -} - export type { GetIdentityClaimTypePagedListInput, - IdentityClaimCreateDto, - IdentityClaimDto, IdentityClaimTypeCreateDto, IdentityClaimTypeDto, IdentityClaimTypeUpdateDto, - IdentityClaimUpdateDto, }; diff --git a/apps/react-admin/types/management/localization/index.ts b/apps/react-admin/types/management/localization/index.ts new file mode 100644 index 000000000..754888541 --- /dev/null +++ b/apps/react-admin/types/management/localization/index.ts @@ -0,0 +1,3 @@ +export * from "./languages"; +export * from "./resources"; +export * from "./texts"; diff --git a/apps/react-admin/types/management/localization/languages.ts b/apps/react-admin/types/management/localization/languages.ts new file mode 100644 index 000000000..9a0b1ad56 --- /dev/null +++ b/apps/react-admin/types/management/localization/languages.ts @@ -0,0 +1,25 @@ +import type { AuditedEntityDto } from "#/abp-core"; + +interface LanguageDto extends AuditedEntityDto { + cultureName: string; + displayName: string; + twoLetterISOLanguageName?: string; + uiCultureName: string; +} + +interface LanguageGetListInput { + filter?: string; +} + +interface LanguageCreateOrUpdateDto { + displayName: string; +} + +interface LanguageCreateDto extends LanguageCreateOrUpdateDto { + cultureName: string; + uiCultureName?: string; +} + +type LanguageUpdateDto = LanguageCreateOrUpdateDto; + +export type { LanguageCreateDto, LanguageDto, LanguageGetListInput, LanguageUpdateDto }; diff --git a/apps/react-admin/types/management/localization/resources.ts b/apps/react-admin/types/management/localization/resources.ts new file mode 100644 index 000000000..5444f3ad9 --- /dev/null +++ b/apps/react-admin/types/management/localization/resources.ts @@ -0,0 +1,28 @@ +import type { AuditedEntityDto } from "#/abp-core"; + +interface ResourceDto extends AuditedEntityDto { + defaultCultureName?: string; + description?: string; + displayName: string; + enable: boolean; + name: string; +} + +interface ResourceCreateOrUpdateDto { + defaultCultureName?: string; + description?: string; + displayName: string; + enable: boolean; +} + +interface ResourceCreateDto extends ResourceCreateOrUpdateDto { + name: string; +} + +type ResourceUpdateDto = ResourceCreateOrUpdateDto; + +interface ResourceGetListInput { + filter?: string; +} + +export type { ResourceCreateDto, ResourceDto, ResourceGetListInput, ResourceUpdateDto }; diff --git a/apps/react-admin/types/management/localization/texts.ts b/apps/react-admin/types/management/localization/texts.ts new file mode 100644 index 000000000..f6918c01f --- /dev/null +++ b/apps/react-admin/types/management/localization/texts.ts @@ -0,0 +1,44 @@ +interface GetTextByKeyInput { + cultureName: string; + key: string; + resourceName: string; +} + +interface GetTextsInput { + cultureName: string; + filter?: string; + onlyNull?: boolean; + resourceName?: string; + targetCultureName: string; +} + +interface SetTextInput { + cultureName: string; + key: string; + resourceName: string; + value: string; +} + +interface RestoreDefaultTextInput { + cultureName: string; + key: string; + resourceName: string; +} + +interface TextDifferenceDto { + cultureName: string; + key: string; + resourceName: string; + targetCultureName: string; + targetValue: string; + value: string; +} + +interface TextDto { + cultureName: string; + key: string; + resourceName: string; + value: string; +} + +export type { GetTextByKeyInput, GetTextsInput, RestoreDefaultTextInput, SetTextInput, TextDifferenceDto, TextDto }; diff --git a/apps/react-admin/types/notifications/definitions.ts b/apps/react-admin/types/notifications/definitions.ts index e1b79f78d..1a8cd750c 100644 --- a/apps/react-admin/types/notifications/definitions.ts +++ b/apps/react-admin/types/notifications/definitions.ts @@ -1,4 +1,6 @@ -import type { NotificationLifetime, NotificationType } from "./notifications"; +import type { ExtensibleObject, IHasExtraProperties } from "#/abp-core"; + +import type { NotificationContentType, NotificationLifetime, NotificationType } from "./notifications"; interface NotificationDto { description: string; @@ -14,6 +16,10 @@ interface NotificationGroupDto { notifications: NotificationDto[]; } +interface NotificationProviderDto { + name: string; +} + interface NotificationTemplateDto { content?: string; culture?: string; @@ -22,4 +28,55 @@ interface NotificationTemplateDto { title: string; } -export type { NotificationDto, NotificationGroupDto, NotificationTemplateDto }; +interface NotificationDefinitionDto extends ExtensibleObject { + allowSubscriptionToClients: boolean; + contentType: NotificationContentType; + description?: string; + displayName: string; + groupName: string; + isStatic: boolean; + name: string; + notificationLifetime: NotificationLifetime; + notificationType: NotificationType; + providers?: string[]; + template?: string; +} + +interface NotificationDefinitionGetListInput { + allowSubscriptionToClients?: boolean; + contentType?: NotificationContentType; + filter?: string; + groupName?: string; + notificationLifetime?: NotificationLifetime; + notificationType?: NotificationType; + template?: string; +} + +interface NotificationDefinitionCreateOrUpdateDto extends IHasExtraProperties { + allowSubscriptionToClients?: boolean; + contentType?: NotificationContentType; + description?: string; + displayName: string; + notificationLifetime?: NotificationLifetime; + notificationType?: NotificationType; + providers?: string[]; + template?: string; +} + +type NotificationDefinitionUpdateDto = NotificationDefinitionCreateOrUpdateDto; + +interface NotificationDefinitionCreateDto extends NotificationDefinitionCreateOrUpdateDto { + groupName?: string; + name: string; +} + +export type { + NotificationDefinitionCreateDto, + NotificationDefinitionDto, + NotificationDefinitionGetListInput, + NotificationDefinitionUpdateDto, + NotificationDto, + NotificationGroupDto, + NotificationProviderDto, + NotificationTemplateDto, +}; diff --git a/apps/react-admin/types/notifications/groups.ts b/apps/react-admin/types/notifications/groups.ts new file mode 100644 index 000000000..9d42bb9e5 --- /dev/null +++ b/apps/react-admin/types/notifications/groups.ts @@ -0,0 +1,32 @@ +import type { ExtensibleObject, IHasExtraProperties } from "#/abp-core"; + +interface NotificationGroupDefinitionDto extends ExtensibleObject { + allowSubscriptionToClients: boolean; + description?: string; + displayName: string; + isStatic: boolean; + name: string; +} + +interface NotificationGroupDefinitionGetListInput { + filter?: string; +} + +interface NotificationGroupDefinitionCreateOrUpdateDto extends IHasExtraProperties { + allowSubscriptionToClients: boolean; + description?: string; + displayName: string; +} + +interface NotificationGroupDefinitionCreateDto extends NotificationGroupDefinitionCreateOrUpdateDto { + name: string; +} + +type NotificationGroupDefinitionUpdateDto = NotificationGroupDefinitionCreateOrUpdateDto; + +export type { + NotificationGroupDefinitionCreateDto, + NotificationGroupDefinitionDto, + NotificationGroupDefinitionGetListInput, + NotificationGroupDefinitionUpdateDto, +}; diff --git a/apps/react-admin/types/notifications/index.ts b/apps/react-admin/types/notifications/index.ts index 2548123ba..d4257bf35 100644 --- a/apps/react-admin/types/notifications/index.ts +++ b/apps/react-admin/types/notifications/index.ts @@ -1,3 +1,5 @@ export * from "./my-notifilers"; export * from "./notifications"; export * from "./definitions"; +export * from "./groups"; +export * from "./subscribes"; diff --git a/apps/react-admin/types/notifications/subscribes.ts b/apps/react-admin/types/notifications/subscribes.ts new file mode 100644 index 000000000..37dba1cc0 --- /dev/null +++ b/apps/react-admin/types/notifications/subscribes.ts @@ -0,0 +1,13 @@ +import type { PagedAndSortedResultRequestDto } from "#/abp-core"; + +interface UserSubscreNotification { + name: string; +} + +interface UserSubscriptionsResult { + isSubscribed: boolean; +} + +type GetSubscriptionsPagedListInput = PagedAndSortedResultRequestDto; + +export type { GetSubscriptionsPagedListInput, UserSubscreNotification, UserSubscriptionsResult }; diff --git a/apps/react-admin/types/oss/containes.ts b/apps/react-admin/types/oss/containes.ts new file mode 100644 index 000000000..c638f7ebe --- /dev/null +++ b/apps/react-admin/types/oss/containes.ts @@ -0,0 +1,33 @@ +import type { PagedAndSortedResultRequestDto } from "#/abp-core"; + +interface OssContainerDto { + creationDate: Date; + lastModifiedDate?: Date; + metadata: Record; + name: string; + size: number; +} + +interface OssContainersResultDto { + containers: OssContainerDto[]; + marker?: string; + maxKeys?: number; + nextMarker?: string; + prefix?: string; +} + +interface GetOssContainersInput extends PagedAndSortedResultRequestDto { + marker?: string; + prefix?: string; +} + +interface GetOssObjectsInput extends PagedAndSortedResultRequestDto { + bucket: string; // container name + delimiter?: string; + encodingType?: string; + marker?: string; + mD5?: string; + prefix?: string; +} + +export type { GetOssContainersInput, GetOssObjectsInput, OssContainerDto, OssContainersResultDto }; diff --git a/apps/react-admin/types/oss/index.ts b/apps/react-admin/types/oss/index.ts new file mode 100644 index 000000000..9c0d890bf --- /dev/null +++ b/apps/react-admin/types/oss/index.ts @@ -0,0 +1,2 @@ +export * from "./containes"; +export * from "./objects"; diff --git a/apps/react-admin/types/oss/objects.ts b/apps/react-admin/types/oss/objects.ts new file mode 100644 index 000000000..4c01004b8 --- /dev/null +++ b/apps/react-admin/types/oss/objects.ts @@ -0,0 +1,44 @@ +interface OssObjectDto { + creationDate: Date; + isFolder: boolean; + lastModifiedDate?: Date; + mD5?: string; + metadata: Record; + name: string; + path: string; + size: number; +} + +interface CreateOssObjectInput { + bucket: string; + expirationTime?: string; + file?: File; + fileName: string; + overwrite: boolean; + path?: string; +} + +interface GetOssObjectInput { + bucket: string; + mD5: boolean; + object: string; + path?: string; +} + +interface BulkDeleteOssObjectInput { + bucket: string; + object: string; + path?: string; +} + +interface OssObjectsResultDto { + bucket: string; + delimiter?: string; + marker?: string; + maxKeys: number; + nextMarker?: string; + objects: OssObjectDto[]; + prefix?: string; +} + +export type { BulkDeleteOssObjectInput, CreateOssObjectInput, GetOssObjectInput, OssObjectDto, OssObjectsResultDto }; diff --git a/apps/react-admin/types/platform/data-dictionaries.ts b/apps/react-admin/types/platform/data-dictionaries.ts new file mode 100644 index 000000000..483250dcc --- /dev/null +++ b/apps/react-admin/types/platform/data-dictionaries.ts @@ -0,0 +1,76 @@ +import type { EntityDto, PagedAndSortedResultRequestDto } from "#/abp-core"; + +enum ValueType { + Array = 5, + Boolean = 2, + Date = 3, + DateTime = 4, + Numeic = 1, + Object = 6, + String = 0, +} + +interface DataItemDto extends EntityDto { + allowBeNull: boolean; + defaultValue?: string; + description?: string; + displayName: string; + name: string; + valueType: ValueType; +} + +interface DataDto extends EntityDto { + code: string; + description?: string; + displayName: string; + items: DataItemDto[]; + name: string; + parentId?: string; +} + +interface DataCreateOrUpdateDto { + description?: string; + displayName: string; + name: string; +} + +interface DataCreateDto extends DataCreateOrUpdateDto { + parentId?: string; +} + +interface DataItemCreateOrUpdateDto { + allowBeNull: boolean; + defaultValue?: string; + description?: string; + displayName: string; + valueType: ValueType; +} + +interface DataItemCreateDto extends DataItemCreateOrUpdateDto { + name: string; +} + +interface GetDataListInput extends PagedAndSortedResultRequestDto { + filter?: string; +} + +interface DataMoveDto { + parentId?: string; +} + +type DataUpdateDto = DataCreateOrUpdateDto; + +type DataItemUpdateDto = DataItemCreateOrUpdateDto; + +export { ValueType }; + +export type { + DataCreateDto, + DataDto, + DataItemCreateDto, + DataItemDto, + DataItemUpdateDto, + DataMoveDto, + DataUpdateDto, + GetDataListInput, +}; diff --git a/apps/react-admin/types/platform/favorites.ts b/apps/react-admin/types/platform/favorites.ts new file mode 100644 index 000000000..c4ab59c08 --- /dev/null +++ b/apps/react-admin/types/platform/favorites.ts @@ -0,0 +1,28 @@ +import type { AuditedEntityDto, IHasConcurrencyStamp } from "#/abp-core"; + +interface UserFavoriteMenuDto extends AuditedEntityDto { + aliasName?: string; + color?: string; + displayName: string; + framework: string; + icon?: string; + menuId: string; + name: string; + path?: string; + userId: string; +} + +interface UserFavoriteMenuCreateOrUpdateDto { + aliasName?: string; + color?: string; + icon?: string; + menuId: string; +} + +interface UserFavoriteMenuCreateDto extends UserFavoriteMenuCreateOrUpdateDto { + framework: string; +} + +interface UserFavoriteMenuUpdateDto extends IHasConcurrencyStamp, UserFavoriteMenuCreateOrUpdateDto {} + +export type { UserFavoriteMenuCreateDto, UserFavoriteMenuDto, UserFavoriteMenuUpdateDto }; diff --git a/apps/react-admin/types/platform/index.ts b/apps/react-admin/types/platform/index.ts new file mode 100644 index 000000000..cd02c40db --- /dev/null +++ b/apps/react-admin/types/platform/index.ts @@ -0,0 +1,5 @@ +export * from "./data-dictionaries"; +export * from "./favorites"; +export * from "./layouts"; +export * from "./menus"; +export * from "./messages"; diff --git a/apps/react-admin/types/platform/layouts.ts b/apps/react-admin/types/platform/layouts.ts new file mode 100644 index 000000000..63e766704 --- /dev/null +++ b/apps/react-admin/types/platform/layouts.ts @@ -0,0 +1,30 @@ +import type { PagedAndSortedResultRequestDto } from "#/abp-core"; + +import type { RouteDto } from "./routes"; + +interface LayoutDto extends RouteDto { + dataId: string; + framework: string; +} + +interface LayoutCreateOrUpdateDto { + description?: string; + displayName: string; + name: string; + path: string; + redirect?: string; +} + +interface LayoutCreateDto extends LayoutCreateOrUpdateDto { + dataId: string; + framework: string; +} + +type LayoutUpdateDto = LayoutCreateOrUpdateDto; + +interface LayoutGetPagedListInput extends PagedAndSortedResultRequestDto { + filter?: string; + framework?: string; +} + +export type { LayoutCreateDto, LayoutDto, LayoutGetPagedListInput, LayoutUpdateDto }; diff --git a/apps/react-admin/types/platform/menus.ts b/apps/react-admin/types/platform/menus.ts new file mode 100644 index 000000000..0480fbccb --- /dev/null +++ b/apps/react-admin/types/platform/menus.ts @@ -0,0 +1,89 @@ +import type { RouteDto } from "./routes"; + +interface MenuDto extends RouteDto { + code: string; + component: string; + framework: string; + isPublic: boolean; + layoutId: string; + parentId?: string; + startup: boolean; +} + +interface MenuCreateOrUpdateDto { + component: string; + description?: string; + displayName: string; + isPublic: boolean; + meta: Record; + name: string; + parentId?: string; + path: string; + redirect?: string; +} + +interface MenuCreateDto extends MenuCreateOrUpdateDto { + layoutId: string; +} + +interface MenuGetInput { + framework?: string; +} + +interface MenuGetByUserInput { + framework: string; + userId: string; +} + +interface MenuGetByRoleInput { + framework: string; + role: string; +} + +interface SetUserMenuInput { + framework?: string; + menuIds: string[]; + startupMenuId?: string; + userId: string; +} + +interface SetUserMenuStartupInput { + framework?: string; + userId: string; +} + +interface SetRoleMenuInput { + framework?: string; + menuIds: string[]; + roleName: string; + startupMenuId?: string; +} + +interface SetRoleMenuStartupInput { + framework?: string; + roleName: string; +} + +type MenuUpdateDto = MenuCreateOrUpdateDto; + +interface MenuGetAllInput { + filter?: string; + framework?: string; + layoutId?: string; + parentId?: string; + sorting?: string; +} + +export type { + MenuCreateDto, + MenuDto, + MenuGetAllInput, + MenuGetByRoleInput, + MenuGetByUserInput, + MenuGetInput, + MenuUpdateDto, + SetRoleMenuInput, + SetRoleMenuStartupInput, + SetUserMenuInput, + SetUserMenuStartupInput, +}; diff --git a/apps/react-admin/types/platform/messages.ts b/apps/react-admin/types/platform/messages.ts new file mode 100644 index 000000000..ea60f1d62 --- /dev/null +++ b/apps/react-admin/types/platform/messages.ts @@ -0,0 +1,89 @@ +import type { AuditedEntityDto, PagedAndSortedResultRequestDto } from "#/abp-core"; + +/** 消息状态 */ +export enum MessageStatus { + /** 发送失败 */ + Failed = 10, + /** 未发送 */ + Pending = -1, + /** 已发送 */ + Sent = 0, +} +/** 邮件优先级 */ +export enum MailPriority { + /** 高 */ + High = 2, + /** 低 */ + Low = 1, + /** 普通 */ + Normal = 0, +} + +interface MessageDto extends AuditedEntityDto { + /** 消息内容 */ + content: string; + /** 消息发布者 */ + provider?: string; + /** 错误原因 */ + reason?: string; + /** 接收方 */ + receiver: string; + /** 发送次数 */ + sendCount: number; + /** 发送人 */ + sender?: string; + /** 发送时间 */ + sendTime?: Date; + /** 状态 */ + status: MessageStatus; + /** 发送人Id */ + userId?: string; +} +/** 邮件附件 */ +interface EmailMessageAttachmentDto { + /** 附件存储名称 */ + blobName: string; + /** 附件名称 */ + name: string; + /** 附件大小 */ + size: number; +} +/** 邮件标头 */ +interface EmailMessageHeaderDto { + /** 键名 */ + key: string; + /** 键值 */ + value: string; +} +/** 邮件消息 */ +interface EmailMessageDto extends MessageDto { + attachments: EmailMessageAttachmentDto[]; + cc?: string; + from?: string; + headers: EmailMessageHeaderDto[]; + isBodyHtml: boolean; + normalize: boolean; + priority?: MailPriority; + subject?: string; +} + +interface EmailMessageGetListInput extends PagedAndSortedResultRequestDto { + beginSendTime?: Date; + content?: string; + emailAddress?: string; + endSendTime?: Date; + from?: string; + priority?: MailPriority; + subject?: string; +} + +type SmsMessageDto = MessageDto; + +interface SmsMessageGetListInput extends PagedAndSortedResultRequestDto { + beginSendTime?: Date; + content?: string; + endSendTime?: Date; + phoneNumber?: string; +} + +export type { EmailMessageDto, EmailMessageGetListInput, SmsMessageDto, SmsMessageGetListInput }; diff --git a/apps/react-admin/types/platform/routes.ts b/apps/react-admin/types/platform/routes.ts new file mode 100644 index 000000000..188885ef5 --- /dev/null +++ b/apps/react-admin/types/platform/routes.ts @@ -0,0 +1,12 @@ +import type { EntityDto } from "#/abp-core"; + +interface RouteDto extends EntityDto { + description?: string; + displayName: string; + meta: Record; + name: string; + path: string; + redirect?: string; +} + +export type { RouteDto }; diff --git a/apps/react-admin/types/saas/editions.ts b/apps/react-admin/types/saas/editions.ts new file mode 100644 index 000000000..56e74da6e --- /dev/null +++ b/apps/react-admin/types/saas/editions.ts @@ -0,0 +1,21 @@ +import type { ExtensibleAuditedEntityDto, IHasConcurrencyStamp, PagedAndSortedResultRequestDto } from "#/abp-core"; + +interface EditionDto extends ExtensibleAuditedEntityDto, IHasConcurrencyStamp { + /** 显示名称 */ + displayName: string; +} + +interface EditionCreateOrUpdateBase { + /** 显示名称 */ + displayName: string; +} + +type EditionCreateDto = EditionCreateOrUpdateBase; + +interface EditionUpdateDto extends EditionCreateOrUpdateBase, IHasConcurrencyStamp {} + +interface GetEditionPagedListInput extends PagedAndSortedResultRequestDto { + filter?: string; +} + +export type { EditionCreateDto, EditionDto, EditionUpdateDto, GetEditionPagedListInput }; diff --git a/apps/react-admin/types/saas/index.ts b/apps/react-admin/types/saas/index.ts new file mode 100644 index 000000000..ccf4cf89e --- /dev/null +++ b/apps/react-admin/types/saas/index.ts @@ -0,0 +1,3 @@ +export * from "./editions"; +export * from "./multiTenancys"; +export * from "./tenants"; diff --git a/apps/react-admin/types/saas/multiTenancys.ts b/apps/react-admin/types/saas/multiTenancys.ts new file mode 100644 index 000000000..99f9afac7 --- /dev/null +++ b/apps/react-admin/types/saas/multiTenancys.ts @@ -0,0 +1,9 @@ +interface FindTenantResultDto { + isActive: boolean; + name?: string; + normalizedName?: string; + success: boolean; + tenantId?: string; +} + +export type { FindTenantResultDto }; diff --git a/apps/react-admin/types/saas/tenants.ts b/apps/react-admin/types/saas/tenants.ts new file mode 100644 index 000000000..a2bc7e843 --- /dev/null +++ b/apps/react-admin/types/saas/tenants.ts @@ -0,0 +1,71 @@ +import type { + ExtensibleAuditedEntityDto, + ExtensibleObject, + IHasConcurrencyStamp, + NameValue, + PagedAndSortedResultRequestDto, +} from "#/abp-core"; + +type TenantConnectionStringDto = NameValue; + +type TenantConnectionStringSetInput = NameValue; + +interface TenantDto extends ExtensibleAuditedEntityDto, IHasConcurrencyStamp { + /** 禁用时间 */ + disableTime?: string; + /** 版本Id */ + editionId?: string; + /** 版本名称 */ + editionName?: string; + /** 启用时间 */ + enableTime?: string; + /** 是否可用 */ + isActive: boolean; + /** 名称 */ + name: string; + /** 名称 */ + normalizedName: string; +} + +interface GetTenantPagedListInput extends PagedAndSortedResultRequestDto { + filter?: string; +} + +interface TenantCreateOrUpdateBase extends ExtensibleObject { + /** 禁用时间 */ + disableTime?: string; + /** 版本Id */ + editionId?: string; + /** 启用时间 */ + enableTime?: string; + /** 是否可用 */ + isActive: boolean; + /** 名称 */ + name: string; +} + +interface TenantCreateDto extends TenantCreateOrUpdateBase { + adminEmailAddress: string; + adminPassword: string; + connectionStrings?: TenantConnectionStringSetInput[]; + defaultConnectionString?: string; + useSharedDatabase: boolean; +} + +interface TenantConnectionStringCheckInput { + connectionString: string; + name?: string; + provider: string; +} + +interface TenantUpdateDto extends IHasConcurrencyStamp, TenantCreateOrUpdateBase {} + +export type { + GetTenantPagedListInput, + TenantConnectionStringCheckInput, + TenantConnectionStringDto, + TenantConnectionStringSetInput, + TenantCreateDto, + TenantDto, + TenantUpdateDto, +}; diff --git a/apps/react-admin/types/tasks/index.ts b/apps/react-admin/types/tasks/index.ts new file mode 100644 index 000000000..390e5d771 --- /dev/null +++ b/apps/react-admin/types/tasks/index.ts @@ -0,0 +1,2 @@ +export * from "./job-infos"; +export * from "./job-logs"; diff --git a/apps/react-admin/types/tasks/job-infos.ts b/apps/react-admin/types/tasks/job-infos.ts new file mode 100644 index 000000000..792861ae3 --- /dev/null +++ b/apps/react-admin/types/tasks/job-infos.ts @@ -0,0 +1,130 @@ +import type { ExtensibleAuditedEntityDto, IHasConcurrencyStamp, PagedAndSortedResultRequestDto } from "#/abp-core"; + +enum JobStatus { + Completed = 0, + FailedRetry = 15, + None = -1, + Paused = 20, + Queuing = 5, + Running = 10, + Stopped = 30, +} + +enum JobType { + Once = 0, + Period = 1, + Persistent = 2, +} + +enum JobPriority { + AboveNormal = 20, + BelowNormal = 10, + High = 25, + Low = 5, + Normal = 15, +} + +enum JobSource { + None = -1, + System = 10, + User = 0, +} + +interface BackgroundJobInfoDto extends ExtensibleAuditedEntityDto, IHasConcurrencyStamp { + args: Record; + beginTime: string; + cron?: string; + description?: string; + endTime?: string; + group: string; + interval: number; + isAbandoned: boolean; + isEnabled: boolean; + jobType: JobType; + lastRunTime?: string; + lockTimeOut: number; + maxCount: number; + maxTryCount: number; + name: string; + nextRunTime?: string; + priority: JobPriority; + result?: string; + source: JobSource; + status: JobStatus; + triggerCount: number; + tryCount: number; + type: string; +} + +interface BackgroundJobInfoCreateOrUpdateDto { + args: Record; + cron?: string; + description?: string; + interval: number; + isEnabled: boolean; + jobType: JobType; + lockTimeOut: number; + maxCount: number; + maxTryCount: number; + priority: JobPriority; +} + +interface BackgroundJobInfoCreateDto extends BackgroundJobInfoCreateOrUpdateDto { + beginTime: string; + endTime?: string; + group: string; + name: string; + nodeName?: string; + source: JobSource; + type: string; +} + +interface BackgroundJobInfoUpdateDto extends BackgroundJobInfoCreateOrUpdateDto, IHasConcurrencyStamp {} + +interface BackgroundJobInfoGetListInput extends PagedAndSortedResultRequestDto { + beginCreationTime?: Date; + beginLastRunTime?: Date; + beginTime?: Date; + endCreationTime?: Date; + endLastRunTime?: Date; + endTime?: Date; + filter?: string; + group?: string; + isAbandoned?: boolean; + jobType?: JobType; + name?: string; + priority?: JobPriority; + source?: JobSource; + status?: JobStatus; + type?: string; +} + +interface BackgroundJobInfoBatchInput { + jobIds: string[]; +} + +interface BackgroundJobParamterDto { + description?: string; + displayName: string; + name: string; + required: boolean; +} + +interface BackgroundJobDefinitionDto { + description?: string; + displayName: string; + name: string; + paramters: BackgroundJobParamterDto[]; +} + +export { JobPriority, JobSource, JobStatus, JobType }; + +export type { + BackgroundJobDefinitionDto, + BackgroundJobInfoBatchInput, + BackgroundJobInfoCreateDto, + BackgroundJobInfoDto, + BackgroundJobInfoGetListInput, + BackgroundJobInfoUpdateDto, + BackgroundJobParamterDto, +}; diff --git a/apps/react-admin/types/tasks/job-logs.ts b/apps/react-admin/types/tasks/job-logs.ts new file mode 100644 index 000000000..d9c866ff0 --- /dev/null +++ b/apps/react-admin/types/tasks/job-logs.ts @@ -0,0 +1,23 @@ +import type { EntityDto, PagedAndSortedResultRequestDto } from "#/abp-core"; + +interface BackgroundJobLogDto extends EntityDto { + exception?: string; + jobGroup: string; + jobName: string; + jobType: string; + message: string; + runTime: string; +} + +interface BackgroundJobLogGetListInput extends PagedAndSortedResultRequestDto { + beginRunTime?: string; + endRunTime?: string; + filter?: string; + group?: string; + hasExceptions?: boolean; + jobId?: string; + name?: string; + type?: string; +} + +export type { BackgroundJobLogDto, BackgroundJobLogGetListInput }; diff --git a/apps/react-admin/types/text-templating/contents.ts b/apps/react-admin/types/text-templating/contents.ts new file mode 100644 index 000000000..95e465ffc --- /dev/null +++ b/apps/react-admin/types/text-templating/contents.ts @@ -0,0 +1,26 @@ +interface TextTemplateContentDto { + content?: string; + culture?: string; + name: string; +} + +interface TextTemplateContentGetInput { + culture?: string; + name: string; +} + +interface TextTemplateRestoreInput { + culture?: string; +} + +interface TextTemplateContentUpdateDto { + content: string; + culture?: string; +} + +export type { + TextTemplateContentDto, + TextTemplateContentGetInput, + TextTemplateContentUpdateDto, + TextTemplateRestoreInput, +}; diff --git a/apps/react-admin/types/text-templating/definitions.ts b/apps/react-admin/types/text-templating/definitions.ts new file mode 100644 index 000000000..190d38421 --- /dev/null +++ b/apps/react-admin/types/text-templating/definitions.ts @@ -0,0 +1,42 @@ +import type { ExtensibleObject, IHasConcurrencyStamp } from "#/abp-core"; + +interface TextTemplateDefinitionDto extends ExtensibleObject, IHasConcurrencyStamp { + defaultCultureName?: string; + displayName: string; + isInlineLocalized: boolean; + isLayout: boolean; + isStatic: boolean; + layout?: string; + localizationResourceName?: string; + name: string; + renderEngine?: string; +} + +interface TextTemplateDefinitionCreateOrUpdateDto { + defaultCultureName?: string; + displayName: string; + isInlineLocalized: boolean; + isLayout: boolean; + layout?: string; + localizationResourceName?: string; + renderEngine?: string; +} + +interface TextTemplateDefinitionCreateDto extends TextTemplateDefinitionCreateOrUpdateDto { + name: string; +} + +interface TextTemplateDefinitionUpdateDto extends IHasConcurrencyStamp, TextTemplateDefinitionCreateOrUpdateDto {} + +interface TextTemplateDefinitionGetListInput { + filter?: string; + isLayout?: boolean; + isStatic?: boolean; +} + +export type { + TextTemplateDefinitionCreateDto, + TextTemplateDefinitionDto, + TextTemplateDefinitionGetListInput, + TextTemplateDefinitionUpdateDto, +}; diff --git a/apps/react-admin/types/webhooks/definitions.ts b/apps/react-admin/types/webhooks/definitions.ts new file mode 100644 index 000000000..6b3faf7ce --- /dev/null +++ b/apps/react-admin/types/webhooks/definitions.ts @@ -0,0 +1,37 @@ +import type { IHasConcurrencyStamp, IHasExtraProperties } from "#/abp-core"; + +interface WebhookDefinitionDto extends IHasExtraProperties { + description?: string; + displayName: string; + groupName: string; + isEnabled: boolean; + isStatic: boolean; + name: string; + requiredFeatures?: string[]; +} + +interface WebhookDefinitionCreateOrUpdateDto extends IHasExtraProperties { + description?: string; + displayName: string; + isEnabled: boolean; + requiredFeatures?: string[]; // can not change the dot of dotnet +} + +interface WebhookDefinitionCreateDto extends WebhookDefinitionCreateOrUpdateDto { + groupName: string; + name: string; +} + +interface WebhookDefinitionUpdateDto extends IHasConcurrencyStamp, WebhookDefinitionCreateOrUpdateDto {} + +interface WebhookDefinitionGetListInput { + filter?: string; + groupName?: string; +} + +export type { + WebhookDefinitionCreateDto, + WebhookDefinitionDto, + WebhookDefinitionGetListInput, + WebhookDefinitionUpdateDto, +}; diff --git a/apps/react-admin/types/webhooks/groups.ts b/apps/react-admin/types/webhooks/groups.ts new file mode 100644 index 000000000..a8c995482 --- /dev/null +++ b/apps/react-admin/types/webhooks/groups.ts @@ -0,0 +1,28 @@ +import type { IHasConcurrencyStamp, IHasExtraProperties } from "#/abp-core"; + +interface WebhookGroupDefinitionDto extends IHasExtraProperties { + displayName: string; + isStatic: boolean; + name: string; +} + +interface WebhookGroupDefinitionCreateOrUpdateDto extends IHasExtraProperties { + displayName: string; +} + +interface WebhookGroupDefinitionCreateDto extends WebhookGroupDefinitionCreateOrUpdateDto { + name: string; +} + +interface WebhookGroupDefinitionUpdateDto extends IHasConcurrencyStamp, WebhookGroupDefinitionCreateOrUpdateDto {} + +interface WebhookGroupDefinitionGetListInput { + filter?: string; +} + +export type { + WebhookGroupDefinitionCreateDto, + WebhookGroupDefinitionDto, + WebhookGroupDefinitionGetListInput, + WebhookGroupDefinitionUpdateDto, +}; diff --git a/apps/react-admin/types/webhooks/index.ts b/apps/react-admin/types/webhooks/index.ts new file mode 100644 index 000000000..b7639ee41 --- /dev/null +++ b/apps/react-admin/types/webhooks/index.ts @@ -0,0 +1,4 @@ +export * from "./definitions"; +export * from "./groups"; +export * from "./send-attempts"; +export * from "./subscriptions"; diff --git a/apps/react-admin/types/webhooks/send-attempts.ts b/apps/react-admin/types/webhooks/send-attempts.ts new file mode 100644 index 000000000..627832580 --- /dev/null +++ b/apps/react-admin/types/webhooks/send-attempts.ts @@ -0,0 +1,50 @@ +import type { EntityDto, PagedAndSortedResultRequestDto } from "#/abp-core"; +import type { HttpStatusCode } from "@/constants/request/http-status"; + +interface WebhookEventRecordDto extends EntityDto { + creationTime: string; + data?: string; + tenantId?: string; + webhookName: string; +} + +interface WebhookSendRecordDto extends EntityDto { + creationTime: string; + lastModificationTime?: string; + requestHeaders?: Record; + response?: string; + responseHeaders?: Record; + responseStatusCode?: HttpStatusCode; + sendExactSameData: boolean; + tenantId?: string; + webhookEvent: WebhookEventRecordDto; + webhookEventId: string; + webhookSubscriptionId: string; +} + +interface WebhookSendRecordDeleteManyInput { + recordIds: string[]; +} + +interface WebhookSendRecordResendManyInput { + recordIds: string[]; +} + +interface WebhookSendRecordGetListInput extends PagedAndSortedResultRequestDto { + beginCreationTime?: Date; + endCreationTime?: Date; + filter?: string; + responseStatusCode?: HttpStatusCode; + state?: boolean; + subscriptionId?: string; + tenantId?: string; + webhookEventId?: string; +} + +export type { + WebhookEventRecordDto, + WebhookSendRecordDeleteManyInput, + WebhookSendRecordDto, + WebhookSendRecordGetListInput, + WebhookSendRecordResendManyInput, +}; diff --git a/apps/react-admin/types/webhooks/subscriptions.ts b/apps/react-admin/types/webhooks/subscriptions.ts new file mode 100644 index 000000000..9128826d9 --- /dev/null +++ b/apps/react-admin/types/webhooks/subscriptions.ts @@ -0,0 +1,64 @@ +import type { CreationAuditedEntityDto, IHasConcurrencyStamp, PagedAndSortedResultRequestDto } from "#/abp-core"; + +interface WebhookSubscriptionDto extends CreationAuditedEntityDto, IHasConcurrencyStamp { + description?: string; + headers?: Record; + isActive: boolean; + secret?: string; + tenantId?: string; + timeoutDuration?: number; + webhooks: string[]; + webhookUri: string; +} + +interface WebhookSubscriptionCreateOrUpdateDto { + description?: string; + headers?: Record; //TODO check + isActive: boolean; + secret?: string; + tenantId?: string; + timeoutDuration?: number; + webhooks: string[]; + webhookUri: string; +} + +type WebhookSubscriptionCreateDto = WebhookSubscriptionCreateOrUpdateDto; + +interface WebhookSubscriptionUpdateDto extends IHasConcurrencyStamp, WebhookSubscriptionCreateOrUpdateDto {} + +interface WebhookSubscriptionDeleteManyInput { + recordIds: string[]; +} + +interface WebhookSubscriptionGetListInput extends PagedAndSortedResultRequestDto { + beginCreationTime?: Date; + endCreationTime?: Date; + filter?: string; + isActive?: boolean; + secret?: string; + tenantId?: string; + webhooks?: string; + webhookUri?: string; +} + +interface WebhookAvailableDto { + description?: string; + displayName: string; + name: string; +} + +interface WebhookAvailableGroupDto { + displayName: string; + name: string; + webhooks: WebhookAvailableDto[]; +} + +export type { + WebhookAvailableDto, + WebhookAvailableGroupDto, + WebhookSubscriptionCreateDto, + WebhookSubscriptionDeleteManyInput, + WebhookSubscriptionDto, + WebhookSubscriptionGetListInput, + WebhookSubscriptionUpdateDto, +};