From 25ae18bab632c127c2e342715633a537ce7ed505 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Tue, 7 Jul 2026 18:24:53 +0200 Subject: [PATCH] (feature): release as unified npm package @maks-it.com/webui, deepDelta fixes --- .cursor/maksit-skills.json | 1 + .cursor/rules/maksit-skills.mdc | 13 +- .gitignore | 2 + .vscode/settings.json | 4 +- AGENTS.md | 8 +- CHANGELOG.md | 24 +++ README.md | 18 +- assets/badges/coverage-branches.svg | 14 +- assets/badges/coverage-lines.svg | 8 +- assets/badges/coverage-methods.svg | 10 +- assets/docs/NPM_CONSUMPTION.md | 45 ++++- assets/docs/NPM_PUBLISH.md | 36 ++-- src/.gitignore | 6 + src/.storybook/main.ts | 12 +- src/.storybook/tsconfig.json | 21 --- src/README.md | 54 ++++++ .../components/DataTable/DataTable.tsx | 2 +- .../components/DataTable/DataTableFilter.tsx | 0 .../components/DataTable/DataTableLabel.tsx | 2 +- .../components/DataTable/helpers.ts | 0 .../components/DataTable/index.ts | 0 .../components/FormLayout/FormContainer.tsx | 0 .../components/FormLayout/FormContent.tsx | 0 .../components/FormLayout/FormFooter.tsx | 0 .../components/FormLayout/FormHeader.tsx | 0 .../components/FormLayout/index.ts | 0 .../components/Layout/Container.tsx | 0 .../components/Layout/Content.tsx | 0 .../components/Layout/Footer.tsx | 0 .../components/Layout/Header.tsx | 0 .../components/Layout/MainContainer.tsx | 0 .../components/Layout/SideMenu/Container.tsx | 0 .../components/Layout/SideMenu/Content.tsx | 0 .../components/Layout/SideMenu/Footer.tsx | 0 .../components/Layout/SideMenu/Header.tsx | 0 .../components/Layout/SideMenu/index.tsx | 0 .../components/Layout/index.tsx | 0 .../components/LazyLoadTable.tsx | 0 .../components/Offcanvas.tsx | 0 .../components/Scopes/EntityScopesSummary.tsx | 2 +- .../components/Scopes/index.ts | 0 .../components/Toast/addToast.ts | 0 .../components/Toast/createToastId.ts | 0 .../components/Toast/index.tsx | 0 .../components/editors/ButtonComponent.tsx | 0 .../components/editors/CheckBoxComponent.tsx | 0 .../editors/DateTimePickerComponent.tsx | 2 +- .../editors/DualListboxComponent.tsx | 0 .../components/editors/FieldContainer.tsx | 0 .../editors/FileUploadComponent.tsx | 0 .../components/editors/ListBoxComponent.tsx | 0 .../editors/RadioGroupComponent.tsx | 0 .../editors/RemoteSelectBoxComponent.tsx | 6 +- .../components/editors/SecretComponent.tsx | 0 .../components/editors/SelectBoxComponent.tsx | 0 .../components/editors/TextBoxComponent.tsx | 0 .../components/editors/TreeViewComponent.tsx | 0 .../components/editors/editorStyles.ts | 0 .../components/editors/index.ts | 0 .../src => components}/functions/debounce.ts | 0 .../src => components}/functions/index.ts | 0 .../functions/tailwind/gridColSpan.ts | 0 .../functions/tailwind/index.ts | 0 .../components/src => components}/index.ts | 0 src/components/oauth/ExternalLoginButtons.tsx | 59 +++++++ .../oauth/IdpLoginCallbackHandler.tsx | 14 ++ .../oauth/MailboxOAuthReturnHandler.tsx | 11 ++ src/components/oauth/index.ts | 15 ++ .../src => contracts}/PagedRequest.ts | 5 +- .../src => contracts}/PagedResponse.ts | 0 .../src => contracts}/PatchOperation.ts | 0 .../PatchRequestModelBase.ts | 0 .../src => contracts}/ProblemDetails.ts | 0 .../src => contracts}/RequestModelBase.ts | 0 .../src => contracts}/ResponseModelBase.ts | 0 src/contracts/SearchResponseBase.ts | 11 ++ src/contracts/TrngResponse.ts | 6 + .../src => contracts}/identity/Claims.ts | 0 .../identity/login/LoginRequest.ts | 0 .../identity/login/LoginResponse.ts | 0 .../identity/login/RefreshTokenRequest.ts | 0 .../identity/logout/LogoutRequest.ts | 14 ++ .../identity/logout/LogoutResponse.ts | 0 .../contracts/src => contracts}/index.ts | 0 src/contracts/oauth/EmailProtocol.ts | 9 + src/contracts/oauth/IdpRedeemLoginResponse.ts | 6 + .../oauth/LoginExternalExchangeResponse.ts | 16 ++ src/contracts/oauth/LoginProviderExternal.ts | 23 +++ src/contracts/oauth/MailboxOAuthPending.ts | 8 + src/contracts/oauth/MsalViewModel.ts | 7 + .../oauth/RedeemLoginExternalRequest.ts | 18 ++ .../oauth/RefreshMailboxAccessTokenRequest.ts | 20 +++ .../RefreshMailboxAccessTokenResponse.ts | 9 + .../oauth/ServerAuthenticationMethod.ts | 9 + src/contracts/oauth/index.ts | 37 ++++ src/contracts/oauth/oauthConstants.ts | 19 ++ .../shared/search/SearchEntityScopeEntry.ts | 5 +- .../core/src => core}/functions/acl/index.ts | 0 .../functions/acl/parseAclEntry.ts | 0 .../functions/dataTable/dataTableFilters.ts | 0 .../functions/dataTable/dataTablePaged.ts | 2 +- .../src => core}/functions/dataTable/index.ts | 0 .../functions/date/dateTimeToUtcIsoSchema.ts | 0 .../functions/date/formatISODateString.ts | 0 .../core/src => core}/functions/date/index.ts | 0 .../functions/date/isValidDateString.ts | 0 .../src => core}/functions/deep/deepCopy.ts | 2 +- .../src => core}/functions/deep/deepDelta.ts | 55 ++++-- .../src => core}/functions/deep/deepEqual.ts | 0 .../src => core}/functions/deep/deepMerge.ts | 7 +- .../functions/deep/deepPatternMatch.ts | 20 ++- .../core/src => core}/functions/deep/index.ts | 1 - .../functions/deep/patchCollectionPolicies.ts | 18 +- .../src => core}/functions/enum/enumToArr.ts | 0 .../src => core}/functions/enum/enumToObj.ts | 0 .../functions/enum/enumToString.ts | 0 .../functions/enum/flagsToString.ts | 0 .../src => core}/functions/enum/hasAnyFlag.ts | 0 .../src => core}/functions/enum/hasFlag.ts | 0 .../core/src => core}/functions/enum/index.ts | 0 .../src => core}/functions/enum/toggleFlag.ts | 0 .../core/src => core}/functions/file/index.ts | 0 .../functions/file/saveBinaryToDisk.ts | 0 .../core/src => core}/functions/guid/index.ts | 0 .../src => core}/functions/guid/isGuid.ts | 0 .../headers/extractFilenameFromHeaders.ts | 0 .../src => core}/functions/headers/index.ts | 0 .../core/src => core}/functions/index.ts | 0 .../functions/zod/applyFormBulkChange.ts | 0 .../functions/zod/applyFormFieldChange.ts | 0 .../functions/zod/createFormBulkUpdater.ts | 0 .../functions/zod/createFormFieldUpdater.ts | 0 .../functions/zod/emptyFormErrors.ts | 0 .../zod/flattenFormValidationIssues.ts | 0 .../src => core}/functions/zod/formPath.ts | 0 .../core/src => core}/functions/zod/index.ts | 0 .../functions/zod/validateFormState.ts | 0 src/core/hooks/useFormState.ts | 45 +++++ .../core/src => core}/hooks/useFormState.tsx | 0 .../src => core}/http/authInterceptors.ts | 0 .../core/src => core}/http/config.ts | 0 .../http/createWebUiHttpClient.ts | 0 .../core/src => core}/http/errorHandler.ts | 2 +- .../core/src => core}/http/formData.ts | 0 src/{packages/core/src => core}/http/index.ts | 0 .../core/src => core}/http/problemDetails.ts | 2 +- src/{packages/core/src => core}/http/types.ts | 0 src/{packages/core/src => core}/index.ts | 0 .../src => core}/localStorage/identity.ts | 2 +- src/core/oauth/idpOAuth.ts | 46 +++++ src/core/oauth/index.ts | 33 ++++ src/core/oauth/mailboxOAuth.ts | 98 ++++++++++ src/core/oauth/urlUtils.ts | 18 ++ src/core/oauth/useIdpLoginCallback.ts | 65 +++++++ src/core/oauth/useMailboxOAuthReturn.ts | 167 ++++++++++++++++++ .../core/src => core}/signalr/index.ts | 0 .../core/src => core}/signalr/useWebUiHub.ts | 0 .../types/FormValidationSchema.ts | 0 src/core/types/ScopePermissions.ts | 11 ++ .../core/src => core}/types/index.ts | 2 +- src/coverage/coverage-summary.json | 35 ---- src/index.ts | 3 + src/jest.config.cjs | 15 +- src/package-lock.json | 86 ++++++--- src/package.json | 62 +++++-- src/packages/components/README.md | 68 ------- src/packages/components/package.json | 62 ------- src/packages/components/tsconfig.build.json | 6 - src/packages/components/tsconfig.json | 8 - src/packages/contracts/README.md | 49 ----- src/packages/contracts/package.json | 44 ----- .../contracts/src/SearchResponseBase.ts | 6 - src/packages/contracts/src/TrngResponse.ts | 5 - .../src/identity/logout/LogoutRequest.ts | 12 -- src/packages/contracts/tsconfig.build.json | 6 - src/packages/contracts/tsconfig.json | 8 - src/packages/core/README.md | 82 --------- src/packages/core/package.json | 55 ------ .../core/src/types/ScopePermissions.ts | 8 - src/packages/core/tsconfig.build.json | 6 - src/packages/core/tsconfig.json | 8 - src/stories/README.md | 10 +- src/stories/tsconfig.json | 4 - src/storybook.css | 2 +- .../test => test/contracts}/schemas.test.ts | 18 +- .../core}/functions/acl/parseAclEntry.test.ts | 4 +- .../dataTable/dataTableFilters.test.ts | 2 +- .../dataTable/dataTablePaged.test.ts | 4 +- .../functions/date/isValidDateString.test.ts | 2 +- .../core}/functions/deep/deepDelta.test.ts | 35 ++-- .../core}/functions/deep/deepEqual.test.ts | 2 +- .../core}/functions/enum/flags.test.ts | 6 +- .../core}/functions/guid/isGuid.test.ts | 2 +- .../extractFilenameFromHeaders.test.ts | 2 +- .../functions/zod/validateFormState.test.ts | 2 +- .../core}/http/problemDetails.test.ts | 2 +- .../core}/signalr/useWebUiHub.test.ts | 2 +- src/test/jest-env.d.ts | 1 + src/tsconfig.base.json | 20 --- src/tsconfig.jest.json | 13 -- src/tsconfig.json | 38 ++++ src/tsup.config.ts | 24 +++ src/vitest.config.ts | 8 +- tsconfig.json | 8 - utils/engines/release/scriptSettings.json | 13 +- utils/modules/ContainerEngineSupport.psm1 | 6 +- utils/modules/ContainerImagePushSupport.psm1 | 24 +-- utils/modules/Engine/ReleaseSupport.psm1 | 2 +- utils/plugins/Npm/NpmPack.psm1 | 20 ++- utils/plugins/Npm/NpmPackageSupport.psm1 | 62 +++++++ utils/plugins/Npm/NpmPublish.psm1 | 15 +- 211 files changed, 1433 insertions(+), 761 deletions(-) create mode 100644 src/.gitignore delete mode 100644 src/.storybook/tsconfig.json create mode 100644 src/README.md rename src/{packages/components/src => components}/components/DataTable/DataTable.tsx (99%) rename src/{packages/components/src => components}/components/DataTable/DataTableFilter.tsx (100%) rename src/{packages/components/src => components}/components/DataTable/DataTableLabel.tsx (95%) rename src/{packages/components/src => components}/components/DataTable/helpers.ts (100%) rename src/{packages/components/src => components}/components/DataTable/index.ts (100%) rename src/{packages/components/src => components}/components/FormLayout/FormContainer.tsx (100%) rename src/{packages/components/src => components}/components/FormLayout/FormContent.tsx (100%) rename src/{packages/components/src => components}/components/FormLayout/FormFooter.tsx (100%) rename src/{packages/components/src => components}/components/FormLayout/FormHeader.tsx (100%) rename src/{packages/components/src => components}/components/FormLayout/index.ts (100%) rename src/{packages/components/src => components}/components/Layout/Container.tsx (100%) rename src/{packages/components/src => components}/components/Layout/Content.tsx (100%) rename src/{packages/components/src => components}/components/Layout/Footer.tsx (100%) rename src/{packages/components/src => components}/components/Layout/Header.tsx (100%) rename src/{packages/components/src => components}/components/Layout/MainContainer.tsx (100%) rename src/{packages/components/src => components}/components/Layout/SideMenu/Container.tsx (100%) rename src/{packages/components/src => components}/components/Layout/SideMenu/Content.tsx (100%) rename src/{packages/components/src => components}/components/Layout/SideMenu/Footer.tsx (100%) rename src/{packages/components/src => components}/components/Layout/SideMenu/Header.tsx (100%) rename src/{packages/components/src => components}/components/Layout/SideMenu/index.tsx (100%) rename src/{packages/components/src => components}/components/Layout/index.tsx (100%) rename src/{packages/components/src => components}/components/LazyLoadTable.tsx (100%) rename src/{packages/components/src => components}/components/Offcanvas.tsx (100%) rename src/{packages/components/src => components}/components/Scopes/EntityScopesSummary.tsx (96%) rename src/{packages/components/src => components}/components/Scopes/index.ts (100%) rename src/{packages/components/src => components}/components/Toast/addToast.ts (100%) rename src/{packages/components/src => components}/components/Toast/createToastId.ts (100%) rename src/{packages/components/src => components}/components/Toast/index.tsx (100%) rename src/{packages/components/src => components}/components/editors/ButtonComponent.tsx (100%) rename src/{packages/components/src => components}/components/editors/CheckBoxComponent.tsx (100%) rename src/{packages/components/src => components}/components/editors/DateTimePickerComponent.tsx (99%) rename src/{packages/components/src => components}/components/editors/DualListboxComponent.tsx (100%) rename src/{packages/components/src => components}/components/editors/FieldContainer.tsx (100%) rename src/{packages/components/src => components}/components/editors/FileUploadComponent.tsx (100%) rename src/{packages/components/src => components}/components/editors/ListBoxComponent.tsx (100%) rename src/{packages/components/src => components}/components/editors/RadioGroupComponent.tsx (100%) rename src/{packages/components/src => components}/components/editors/RemoteSelectBoxComponent.tsx (93%) rename src/{packages/components/src => components}/components/editors/SecretComponent.tsx (100%) rename src/{packages/components/src => components}/components/editors/SelectBoxComponent.tsx (100%) rename src/{packages/components/src => components}/components/editors/TextBoxComponent.tsx (100%) rename src/{packages/components/src => components}/components/editors/TreeViewComponent.tsx (100%) rename src/{packages/components/src => components}/components/editors/editorStyles.ts (100%) rename src/{packages/components/src => components}/components/editors/index.ts (100%) rename src/{packages/components/src => components}/functions/debounce.ts (100%) rename src/{packages/components/src => components}/functions/index.ts (100%) rename src/{packages/components/src => components}/functions/tailwind/gridColSpan.ts (100%) rename src/{packages/components/src => components}/functions/tailwind/index.ts (100%) rename src/{packages/components/src => components}/index.ts (100%) create mode 100644 src/components/oauth/ExternalLoginButtons.tsx create mode 100644 src/components/oauth/IdpLoginCallbackHandler.tsx create mode 100644 src/components/oauth/MailboxOAuthReturnHandler.tsx create mode 100644 src/components/oauth/index.ts rename src/{packages/contracts/src => contracts}/PagedRequest.ts (74%) rename src/{packages/contracts/src => contracts}/PagedResponse.ts (100%) rename src/{packages/contracts/src => contracts}/PatchOperation.ts (100%) rename src/{packages/contracts/src => contracts}/PatchRequestModelBase.ts (100%) rename src/{packages/contracts/src => contracts}/ProblemDetails.ts (100%) rename src/{packages/contracts/src => contracts}/RequestModelBase.ts (100%) rename src/{packages/contracts/src => contracts}/ResponseModelBase.ts (100%) create mode 100644 src/contracts/SearchResponseBase.ts create mode 100644 src/contracts/TrngResponse.ts rename src/{packages/contracts/src => contracts}/identity/Claims.ts (100%) rename src/{packages/contracts/src => contracts}/identity/login/LoginRequest.ts (100%) rename src/{packages/contracts/src => contracts}/identity/login/LoginResponse.ts (100%) rename src/{packages/contracts/src => contracts}/identity/login/RefreshTokenRequest.ts (100%) create mode 100644 src/contracts/identity/logout/LogoutRequest.ts rename src/{packages/contracts/src => contracts}/identity/logout/LogoutResponse.ts (100%) rename src/{packages/contracts/src => contracts}/index.ts (100%) create mode 100644 src/contracts/oauth/EmailProtocol.ts create mode 100644 src/contracts/oauth/IdpRedeemLoginResponse.ts create mode 100644 src/contracts/oauth/LoginExternalExchangeResponse.ts create mode 100644 src/contracts/oauth/LoginProviderExternal.ts create mode 100644 src/contracts/oauth/MailboxOAuthPending.ts create mode 100644 src/contracts/oauth/MsalViewModel.ts create mode 100644 src/contracts/oauth/RedeemLoginExternalRequest.ts create mode 100644 src/contracts/oauth/RefreshMailboxAccessTokenRequest.ts create mode 100644 src/contracts/oauth/RefreshMailboxAccessTokenResponse.ts create mode 100644 src/contracts/oauth/ServerAuthenticationMethod.ts create mode 100644 src/contracts/oauth/index.ts create mode 100644 src/contracts/oauth/oauthConstants.ts rename src/{packages/contracts/src => contracts}/shared/search/SearchEntityScopeEntry.ts (55%) rename src/{packages/core/src => core}/functions/acl/index.ts (100%) rename src/{packages/core/src => core}/functions/acl/parseAclEntry.ts (100%) rename src/{packages/core/src => core}/functions/dataTable/dataTableFilters.ts (100%) rename src/{packages/core/src => core}/functions/dataTable/dataTablePaged.ts (95%) rename src/{packages/core/src => core}/functions/dataTable/index.ts (100%) rename src/{packages/core/src => core}/functions/date/dateTimeToUtcIsoSchema.ts (100%) rename src/{packages/core/src => core}/functions/date/formatISODateString.ts (100%) rename src/{packages/core/src => core}/functions/date/index.ts (100%) rename src/{packages/core/src => core}/functions/date/isValidDateString.ts (100%) rename src/{packages/core/src => core}/functions/deep/deepCopy.ts (98%) rename src/{packages/core/src => core}/functions/deep/deepDelta.ts (94%) rename src/{packages/core/src => core}/functions/deep/deepEqual.ts (100%) rename src/{packages/core/src => core}/functions/deep/deepMerge.ts (91%) rename src/{packages/core/src => core}/functions/deep/deepPatternMatch.ts (77%) rename src/{packages/core/src => core}/functions/deep/index.ts (99%) rename src/{packages/core/src => core}/functions/deep/patchCollectionPolicies.ts (81%) rename src/{packages/core/src => core}/functions/enum/enumToArr.ts (100%) rename src/{packages/core/src => core}/functions/enum/enumToObj.ts (100%) rename src/{packages/core/src => core}/functions/enum/enumToString.ts (100%) rename src/{packages/core/src => core}/functions/enum/flagsToString.ts (100%) rename src/{packages/core/src => core}/functions/enum/hasAnyFlag.ts (100%) rename src/{packages/core/src => core}/functions/enum/hasFlag.ts (100%) rename src/{packages/core/src => core}/functions/enum/index.ts (100%) rename src/{packages/core/src => core}/functions/enum/toggleFlag.ts (100%) rename src/{packages/core/src => core}/functions/file/index.ts (100%) rename src/{packages/core/src => core}/functions/file/saveBinaryToDisk.ts (100%) rename src/{packages/core/src => core}/functions/guid/index.ts (100%) rename src/{packages/core/src => core}/functions/guid/isGuid.ts (100%) rename src/{packages/core/src => core}/functions/headers/extractFilenameFromHeaders.ts (100%) rename src/{packages/core/src => core}/functions/headers/index.ts (100%) rename src/{packages/core/src => core}/functions/index.ts (100%) rename src/{packages/core/src => core}/functions/zod/applyFormBulkChange.ts (100%) rename src/{packages/core/src => core}/functions/zod/applyFormFieldChange.ts (100%) rename src/{packages/core/src => core}/functions/zod/createFormBulkUpdater.ts (100%) rename src/{packages/core/src => core}/functions/zod/createFormFieldUpdater.ts (100%) rename src/{packages/core/src => core}/functions/zod/emptyFormErrors.ts (100%) rename src/{packages/core/src => core}/functions/zod/flattenFormValidationIssues.ts (100%) rename src/{packages/core/src => core}/functions/zod/formPath.ts (100%) rename src/{packages/core/src => core}/functions/zod/index.ts (100%) rename src/{packages/core/src => core}/functions/zod/validateFormState.ts (100%) create mode 100644 src/core/hooks/useFormState.ts rename src/{packages/core/src => core}/hooks/useFormState.tsx (100%) rename src/{packages/core/src => core}/http/authInterceptors.ts (100%) rename src/{packages/core/src => core}/http/config.ts (100%) rename src/{packages/core/src => core}/http/createWebUiHttpClient.ts (100%) rename src/{packages/core/src => core}/http/errorHandler.ts (91%) rename src/{packages/core/src => core}/http/formData.ts (100%) rename src/{packages/core/src => core}/http/index.ts (100%) rename src/{packages/core/src => core}/http/problemDetails.ts (87%) rename src/{packages/core/src => core}/http/types.ts (100%) rename src/{packages/core/src => core}/index.ts (100%) rename src/{packages/core/src => core}/localStorage/identity.ts (85%) create mode 100644 src/core/oauth/idpOAuth.ts create mode 100644 src/core/oauth/index.ts create mode 100644 src/core/oauth/mailboxOAuth.ts create mode 100644 src/core/oauth/urlUtils.ts create mode 100644 src/core/oauth/useIdpLoginCallback.ts create mode 100644 src/core/oauth/useMailboxOAuthReturn.ts rename src/{packages/core/src => core}/signalr/index.ts (100%) rename src/{packages/core/src => core}/signalr/useWebUiHub.ts (100%) rename src/{packages/core/src => core}/types/FormValidationSchema.ts (100%) create mode 100644 src/core/types/ScopePermissions.ts rename src/{packages/core/src => core}/types/index.ts (72%) delete mode 100644 src/coverage/coverage-summary.json create mode 100644 src/index.ts delete mode 100644 src/packages/components/README.md delete mode 100644 src/packages/components/package.json delete mode 100644 src/packages/components/tsconfig.build.json delete mode 100644 src/packages/components/tsconfig.json delete mode 100644 src/packages/contracts/README.md delete mode 100644 src/packages/contracts/package.json delete mode 100644 src/packages/contracts/src/SearchResponseBase.ts delete mode 100644 src/packages/contracts/src/TrngResponse.ts delete mode 100644 src/packages/contracts/src/identity/logout/LogoutRequest.ts delete mode 100644 src/packages/contracts/tsconfig.build.json delete mode 100644 src/packages/contracts/tsconfig.json delete mode 100644 src/packages/core/README.md delete mode 100644 src/packages/core/package.json delete mode 100644 src/packages/core/src/types/ScopePermissions.ts delete mode 100644 src/packages/core/tsconfig.build.json delete mode 100644 src/packages/core/tsconfig.json delete mode 100644 src/stories/tsconfig.json rename src/{packages/contracts/test => test/contracts}/schemas.test.ts (69%) rename src/{packages/core/test => test/core}/functions/acl/parseAclEntry.test.ts (85%) rename src/{packages/core/test => test/core}/functions/dataTable/dataTableFilters.test.ts (91%) rename src/{packages/core/test => test/core}/functions/dataTable/dataTablePaged.test.ts (88%) rename src/{packages/core/test => test/core}/functions/date/isValidDateString.test.ts (83%) rename src/{packages/core/test => test/core}/functions/deep/deepDelta.test.ts (97%) rename src/{packages/core/test => test/core}/functions/deep/deepEqual.test.ts (93%) rename src/{packages/core/test => test/core}/functions/enum/flags.test.ts (79%) rename src/{packages/core/test => test/core}/functions/guid/isGuid.test.ts (88%) rename src/{packages/core/test => test/core}/functions/headers/extractFilenameFromHeaders.test.ts (88%) rename src/{packages/core/test => test/core}/functions/zod/validateFormState.test.ts (89%) rename src/{packages/core/test => test/core}/http/problemDetails.test.ts (91%) rename src/{packages/core/test => test/core}/signalr/useWebUiHub.test.ts (91%) create mode 100644 src/test/jest-env.d.ts delete mode 100644 src/tsconfig.base.json delete mode 100644 src/tsconfig.jest.json create mode 100644 src/tsconfig.json create mode 100644 src/tsup.config.ts delete mode 100644 tsconfig.json create mode 100644 utils/plugins/Npm/NpmPackageSupport.psm1 diff --git a/.cursor/maksit-skills.json b/.cursor/maksit-skills.json index de13349..739c525 100644 --- a/.cursor/maksit-skills.json +++ b/.cursor/maksit-skills.json @@ -2,6 +2,7 @@ "$schema": "homelab-maksit-skills-manifest-v1", "skillsRoot": "E:\\Users\\maksym\\source\\repos\\private\\homelab\\ai\\skills", "skills": [ + "common/maksit-webui", "common/react-typescript", "common/maksit-repo-maintenance", "local-ollama" diff --git a/.cursor/rules/maksit-skills.mdc b/.cursor/rules/maksit-skills.mdc index 3632394..386608c 100644 --- a/.cursor/rules/maksit-skills.mdc +++ b/.cursor/rules/maksit-skills.mdc @@ -6,8 +6,13 @@ alwaysApply: true # MaksIT skills (maksit-webui) -1. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\react-typescript\SKILL.md` -2. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-repo-maintenance\SKILL.md` -3. `E:\Users\maksym\source\repos\private\homelab\ai\skills\local-ollama\SKILL.md` — local Ollama offload (`@local-ollama`) +1. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-webui\SKILL.md` — `@maks-it.com/webui` ↔ **MaksIT.Core** + **maksit-identity-hub** + product backends (PATCH, OAuth, ACL) +2. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\react-typescript\SKILL.md` +3. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-repo-maintenance\SKILL.md` +4. `E:\Users\maksym\source\repos\private\homelab\ai\skills\local-ollama\SKILL.md` — `@local-ollama` -Manifest: `.cursor/maksit-skills.json`. \ No newline at end of file +Before changing `src/` library source: read **maksit-webui** skill; grep **maksit-core** and **maksit-identity-hub** as needed. Run `npm run build` in `src/` after source changes. + +Shortcuts: `c:\Users\maksym\Desktop\maksit-core.lnk`, `c:\Users\maksym\Desktop\maksit-identity-hub.lnk` + +Manifest: `.cursor/maksit-skills.json`. diff --git a/.gitignore b/.gitignore index 33591f0..00871ad 100644 --- a/.gitignore +++ b/.gitignore @@ -267,6 +267,8 @@ __pycache__/ node_modules/ dist/ coverage/ +src/coverage/ +src/dist-test-dts/ storybook-static/ *.tsbuildinfo .DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json index fb0ecaa..c014084 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,6 @@ { "typescript.tsdk": "src/node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true + "typescript.enablePromptUseWorkspaceTsdk": true, + "terminal.integrated.cwd": "${workspaceFolder}/src", + "npm.packageManager": "npm" } diff --git a/AGENTS.md b/AGENTS.md index 2ef2fa4..eacb159 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,12 +1,18 @@ # Agent instructions (maksit-webui) +Read **[maksit-webui](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-webui\SKILL.md)** before changing `src/` library source or shared npm packages. It covers alignment with **MaksIT.Core**, **maksit-identity-hub**, **maksit-results**, and product backends. + Complementary skills (no precedence): | Skill | Path | |-------|------| +| maksit-webui | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-webui\SKILL.md) | | react-typescript | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\react-typescript\SKILL.md) | | maksit-repo-maintenance | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-repo-maintenance\SKILL.md) | -(E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-repo-maintenance\SKILL.md) | | local-ollama | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\local-ollama\SKILL.md) | Manifest: `.cursor/maksit-skills.json`. + +**Shortcuts:** `c:\Users\maksym\Desktop\maksit-core.lnk`, `c:\Users\maksym\Desktop\maksit-identity-hub.lnk` + +**Build rule:** after editing `src/` library source (`contracts/`, `core/`, `components/`), run `npm run build` in `src/` before typechecking consumers. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cfd60b..3ea06fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.0] - 2026-07-07 + +### Changed + +- **Breaking:** merged `@maks-it.com/webui-contracts`, `@maks-it.com/webui-core`, and `@maks-it.com/webui-components` into a single published package **`@maks-it.com/webui`**. Host apps install one package and import everything from `@maks-it.com/webui` (see `assets/docs/NPM_CONSUMPTION.md` migration notes). +- **Breaking:** `src/` is now the npm package root — `src/package.json` publishes as `@maks-it.com/webui`; npm workspaces and `src/packages/*` nesting removed. +- Source layout: `contracts/`, `core/`, and `components/` live directly under `src/` (internal organization only; no subpath exports). +- Release pipeline (`utils/engines/release/scriptSettings.json`) publishes one package; `NpmReleaseVersion.syncWorkspaceVersions` disabled. +- Jest, Vitest, and Storybook configs updated for the flat `src/` layout (`@webui/*` aliases point at `src/{contracts,core,components}`). +- VS Code/Cursor: TypeScript SDK and integrated terminal default to `src/`. +- `src/coverage/` explicitly gitignored; generated coverage is no longer tracked (README badges remain under `assets/badges/`). + +### Removed + +- `@maks-it.com/webui-contracts`, `@maks-it.com/webui-core`, and `@maks-it.com/webui-components` as separate npm packages (superseded by `@maks-it.com/webui`). +- `src/packages/` workspace layout and per-package `package.json` files. +- Repo-root `tsconfig.json` and stray `node_modules/` (tooling and dependencies live under `src/` only). + +## [0.3.5] - 2026-07-06 + +### Fixed + +- `ENTITY_SCOPES_ARRAY_POLICY`: synthetic identity is now `{entityId}-{entityType}` (not including `scope`), so adding an application scope alongside an existing organization scope produces a correct `AddToCollection` delta, and permission changes on id-less rows update in place via `_deltaId`. + ## [0.3.4] - 2026-07-06 ### Added diff --git a/README.md b/README.md index 6760069..9ecbc39 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,13 @@ Shared React UI library for **maksit-certs-ui** and **maksit-vault** WebUI apps. -## Packages +## Package | npm package | Description | |-------------|-------------| -| `@maks-it.com/webui-contracts` | Shared TypeScript contracts (paging, gallery types, patch ops, scopes) | -| `@maks-it.com/webui-core` | Utilities (`deepDelta`, enum helpers, ACL parsers) and `useFormState` | -| `@maks-it.com/webui-components` | React components, layout, editors, DataTable, auth shell | +| `@maks-it.com/webui` | Contracts, utilities (`deepDelta`, hooks, HTTP, SignalR), and React components | -Source lives under `src/` (npm workspaces). Release automation lives under `utils/` (from [maksit-repoutils](https://github.com/MAKS-IT-COM/maksit-repoutils)). +Source lives under `src/` (`@maks-it.com/webui` — `contracts/`, `core/`, `components/`). Release automation lives under `utils/`. ## Local development @@ -24,7 +22,7 @@ npm test npm run storybook ``` -**Storybook** (`npm run storybook`) runs a local catalog of `@maks-it.com/webui-components` with Tailwind, React Router, autodocs, a11y checks, and **Vitest component tests** (testing widget + `npm run test-storybook`). Stories live under `src/stories/components/` (mirroring component folders); see `src/stories/README.md` for story conventions and testing. +**Storybook** (`npm run storybook`) runs a local component catalog with Tailwind, React Router, autodocs, a11y checks, and **Vitest component tests** (testing widget + `npm run test-storybook`). Stories live under `src/stories/components/`; see `src/stories/README.md`. Tests and coverage badges: **`utils\Invoke-TestEngine.bat`** (plugin config in `utils\engines\test\scriptSettings.json`; uses `NpmJestTest`). @@ -38,21 +36,21 @@ Configured plugins (see `utils\engines\release\scriptSettings.json`): | Plugin | Role | |--------|------| -| `NpmReleaseVersion` | Read semver from `src/package.json`; sync `packages/*/package.json` | +| `NpmReleaseVersion` | Read semver from `src/package.json` | | `NpmBuild` | `npm ci` + `npm run build` | | `ReleasePublishGuard` | Branch/tag checks before publish | | `GitHub` | GitHub release (optional; set `GitHub` env var) | -| `NpmPublish` | Publish workspace packages in dependency order | +| `NpmPublish` | Publish `@maks-it.com/webui` | Refresh shared utils from repoutils: **`utils\Update-RepoUtils.bat`**. ## Consume in product repos ```bash -npm install @maks-it.com/webui-contracts @maks-it.com/webui-core @maks-it.com/webui-components +npm install @maks-it.com/webui ``` -Wrap the app with `WebUiProvider` and pass axios/redux adapters — see [assets/docs/NPM_CONSUMPTION.md](assets/docs/NPM_CONSUMPTION.md). +See [assets/docs/NPM_CONSUMPTION.md](assets/docs/NPM_CONSUMPTION.md). ## License diff --git a/assets/badges/coverage-branches.svg b/assets/badges/coverage-branches.svg index 4071bcf..f5e7a1b 100644 --- a/assets/badges/coverage-branches.svg +++ b/assets/badges/coverage-branches.svg @@ -1,21 +1,21 @@ - - Branch Coverage: 78% + + Branch Coverage: 78.2% - + - - + + Branch Coverage - - 78% + + 78.2% diff --git a/assets/badges/coverage-lines.svg b/assets/badges/coverage-lines.svg index c27ff27..8a66996 100644 --- a/assets/badges/coverage-lines.svg +++ b/assets/badges/coverage-lines.svg @@ -1,5 +1,5 @@ - - Line Coverage: 82.2% + + Line Coverage: 82.5% @@ -15,7 +15,7 @@ Line Coverage - - 82.2% + + 82.5% diff --git a/assets/badges/coverage-methods.svg b/assets/badges/coverage-methods.svg index 974d7ed..2313428 100644 --- a/assets/badges/coverage-methods.svg +++ b/assets/badges/coverage-methods.svg @@ -1,5 +1,5 @@ - - Method Coverage: 62.4% + + Method Coverage: 58.2% @@ -9,13 +9,13 @@ - + Method Coverage - - 62.4% + + 58.2% diff --git a/assets/docs/NPM_CONSUMPTION.md b/assets/docs/NPM_CONSUMPTION.md index 0cf3e96..3a78bee 100644 --- a/assets/docs/NPM_CONSUMPTION.md +++ b/assets/docs/NPM_CONSUMPTION.md @@ -1,16 +1,29 @@ -# Consuming @maks-it.com/webui-* in Certs UI / Vault +# Consuming `@maks-it.com/webui` in Certs UI / Vault Install: ```bash -npm install @maks-it.com/webui-contracts @maks-it.com/webui-core @maks-it.com/webui-components +npm install @maks-it.com/webui +npm install react react-dom react-router-dom lucide-react @tanstack/react-table react-virtualized axios @microsoft/signalr zod +``` + +Example imports: + +```tsx +import { + WebUiProvider, + Loader, + Authorization, + DataTable, + createColumns, + deepDelta, + PatchOperation, +} from '@maks-it.com/webui' ``` Wrap the app: ```tsx -import { WebUiProvider, Loader, Authorization } from '@maks-it.com/webui-components' - getData(url), @@ -39,9 +52,27 @@ import { WebUiProvider, Loader, Authorization } from '@maks-it.com/webui-compone ``` -## API differences vs copied local folders +## Migration from `@maks-it.com/webui-*` + +Replace three package installs with one: + +```diff +- npm install @maks-it.com/webui-contracts @maks-it.com/webui-core @maks-it.com/webui-components ++ npm install @maks-it.com/webui +``` + +Update imports: + +```diff +- import { PatchOperation } from '@maks-it.com/webui-contracts' +- import { deepDelta } from '@maks-it.com/webui-core' +- import { DataTable } from '@maks-it.com/webui-components' ++ import { PatchOperation, deepDelta, DataTable } from '@maks-it.com/webui' +``` + +## API notes - `RemoteSelectBoxComponent`: use `searchRoute` (absolute API path string) instead of `apiRoute: ApiRoutes`. - `SecretComponent`: pass `generateSecretRoute` when `enableGenerate` is true. -- ACL: generic `parseAclEntry` / `parseAclEntries` from `@maks-it.com/webui-core`; per-app entity maps and `parse*AclEntries` live in each WebUI project (`models/acl.ts`). -- Identity request types and Zod schemas (`LoginRequest` + `LoginRequestSchema`, `LogoutRequest` + `LogoutRequestSchema`, `RefreshTokenRequest` + `RefreshTokenRequestSchema`) live in `@maks-it.com/webui-contracts`. +- ACL: generic `parseAclEntry` / `parseAclEntries` from `@maks-it.com/webui`; per-app entity maps and `parse*AclEntries` live in each WebUI project (`models/acl.ts`). +- Identity request types and Zod schemas (`LoginRequest` + `LoginRequestSchema`, `LogoutRequest` + `LogoutRequestSchema`, `RefreshTokenRequest` + `RefreshTokenRequestSchema`) are exported from `@maks-it.com/webui`. diff --git a/assets/docs/NPM_PUBLISH.md b/assets/docs/NPM_PUBLISH.md index 1a4d904..b8eb286 100644 --- a/assets/docs/NPM_PUBLISH.md +++ b/assets/docs/NPM_PUBLISH.md @@ -1,19 +1,19 @@ -# Publishing `@maks-it.com/webui-*` to npm +# Publishing `@maks-it.com/webui` to npm -Packages are published under the **`@maks-it.com` scope** to [registry.npmjs.org](https://registry.npmjs.org), managed from the [maks-it.com npm org](https://www.npmjs.com/settings/maks-it.com/packages). +The library is published under the **`@maks-it.com` scope** to [registry.npmjs.org](https://registry.npmjs.org), managed from the [maks-it.com npm org](https://www.npmjs.com/settings/maks-it.com/packages). -Published packages: +Published package: | Package | npm | |---------|-----| -| `@maks-it.com/webui-contracts` | https://www.npmjs.com/package/@maks-it.com/webui-contracts | -| `@maks-it.com/webui-core` | https://www.npmjs.com/package/@maks-it.com/webui-core | -| `@maks-it.com/webui-components` | https://www.npmjs.com/package/@maks-it.com/webui-components | +| `@maks-it.com/webui` | https://www.npmjs.com/package/@maks-it.com/webui | + +> **Note:** `@maks-it.com/webui-contracts`, `@maks-it.com/webui-core`, and `@maks-it.com/webui-components` were merged into `@maks-it.com/webui` starting at **0.4.0**. Migrate host apps to the single package. ## One-time npm setup 1. Sign in at https://www.npmjs.com/ with the **maks-it.com** org account. -2. Confirm the **`@maks-it.com` scope** exists under [Packages](https://www.npmjs.com/settings/maks-it.com/packages). The org name `maks-it.com` is the npm scope for scoped packages. +2. Confirm the **`@maks-it.com` scope** exists under [Packages](https://www.npmjs.com/settings/maks-it.com/packages). 3. Create an **Automation** token (recommended) or Granular Access token with **Publish** on `@maks-it.com/*`: - https://www.npmjs.com/settings/maks-it.com/tokens 4. Store the token for release tooling: @@ -23,9 +23,9 @@ Published packages: //registry.npmjs.org/:_authToken=YOUR_TOKEN ``` -Scoped packages must use **`--access public`** (already configured in each package `publishConfig`). +Scoped packages must use **`--access public`** (already configured in package `publishConfig`). -## Manual first publish (0.1.0) +## Manual publish From the repo root: @@ -33,19 +33,13 @@ From the repo root: cd src npm ci npm run build -npm publish -w @maks-it.com/webui-contracts --access public -npm publish -w @maks-it.com/webui-core --access public -npm publish -w @maks-it.com/webui-components --access public +npm publish -w @maks-it.com/webui --access public ``` -Order matters: **contracts → core → components**. - Verify: ```powershell -npm view @maks-it.com/webui-contracts version -npm view @maks-it.com/webui-core version -npm view @maks-it.com/webui-components version +npm view @maks-it.com/webui version ``` ## Release pipeline (recommended) @@ -53,19 +47,17 @@ npm view @maks-it.com/webui-components version Use **`utils\Invoke-ReleasePackage-Single.bat`** (or `pwsh utils\engines\release\Invoke-ReleasePackage.ps1`): 1. Bump version in `src/package.json` (or tag drives `NpmReleaseVersion`). -2. Tag `HEAD` with exact semver, e.g. `git tag v0.2.0 && git push origin v0.2.0`. +2. Tag `HEAD` with exact semver, e.g. `git tag v0.4.0 && git push origin v0.4.0`. 3. Set `$env:Npm` and run the release engine. -`utils\engines\release\scriptSettings.json` runs `NpmReleaseVersion`, `NpmBuild`, `ReleasePublishGuard`, optional `GitHub`, then `NpmPublish` in dependency order. +`utils\engines\release\scriptSettings.json` runs `NpmReleaseVersion`, `NpmBuild`, `ReleasePublishGuard`, optional `GitHub`, then `NpmPublish`. ## After publish — Certs UI / Vault In each WebUI app (`MaksIT.WebUI/package.json`): ```json -"@maks-it.com/webui-contracts": "^0.1.0", -"@maks-it.com/webui-core": "^0.1.0", -"@maks-it.com/webui-components": "^0.1.0" +"@maks-it.com/webui": "^0.4.0" ``` Then refresh the lockfile: diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..a3dc382 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,6 @@ +coverage/ +dist/ +dist-test-dts/ +storybook-static/ +*.tsbuildinfo +*.tgz diff --git a/src/.storybook/main.ts b/src/.storybook/main.ts index 2314a0b..d0d697b 100644 --- a/src/.storybook/main.ts +++ b/src/.storybook/main.ts @@ -9,6 +9,12 @@ const srcDir = join(storybookDir, '..') const config: StorybookConfig = { stories: ['../stories/**/*.stories.@(ts|tsx)'], staticDirs: ['../public'], + typescript: { + reactDocgen: 'react-docgen-typescript', + reactDocgenTypescriptOptions: { + tsconfigPath: join(srcDir, 'tsconfig.json'), + }, + }, addons: [ getAbsolutePath("@storybook/addon-docs"), getAbsolutePath("@storybook/addon-a11y"), @@ -29,9 +35,9 @@ const config: StorybookConfig = { ...config.resolve, alias: { ...(config.resolve?.alias as Record | undefined), - '@webui/components': join(srcDir, 'packages/components/src'), - '@webui/contracts': join(srcDir, 'packages/contracts/src'), - '@webui/core': join(srcDir, 'packages/core/src'), + '@webui/components': join(srcDir, 'components'), + '@webui/contracts': join(srcDir, 'contracts'), + '@webui/core': join(srcDir, 'core'), }, } return config diff --git a/src/.storybook/tsconfig.json b/src/.storybook/tsconfig.json deleted file mode 100644 index 7948ea5..0000000 --- a/src/.storybook/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "compilerOptions": { - "noEmit": true, - "declaration": false, - "declarationMap": false, - "rootDir": "..", - "paths": { - "@webui/components": ["../packages/components/src"], - "@webui/contracts": ["../packages/contracts/src"], - "@webui/core": ["../packages/core/src"], - "@webui/components/*": ["../packages/components/src/*"], - "@webui/contracts/*": ["../packages/contracts/src/*"], - "@webui/core/*": ["../packages/core/src/*"] - } - }, - "include": [ - "./**/*", - "../stories/**/*" - ] -} diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..585c49e --- /dev/null +++ b/src/README.md @@ -0,0 +1,54 @@ +# @maks-it.com/webui + +Shared TypeScript contracts, utilities, hooks, and React components for MaksIT WebUI apps (Certs UI, Vault WebUI, CICD, and related products). + +## Install + +```bash +npm install @maks-it.com/webui +npm install react react-dom react-router-dom lucide-react @tanstack/react-table react-virtualized axios @microsoft/signalr zod +``` + +Peer dependencies must be installed in the host app. + +## Contents + +| Area | Examples | +|------|----------| +| Contracts | `PagedRequest`, `PatchOperation`, `LoginRequestSchema`, OAuth DTOs | +| Core | `deepDelta`, `useFormState`, `createWebUiHttpClient`, `useWebUiHub`, ACL parsers | +| Components | `DataTable`, `Layout`, editors, `ExternalLoginButtons`, `IdpLoginCallbackHandler` | + +## Example — imports + +```tsx +import { + PatchOperation, + deepDelta, + deltaHasOperations, + useFormState, + DataTable, + createColumns, +} from '@maks-it.com/webui' +``` + +## Example — PATCH delta + +```ts +import { deepDelta, deltaHasOperations, ENTITY_SCOPES_ARRAY_POLICY } from '@maks-it.com/webui' + +const delta = deepDelta(formState, backupState, { + arrays: { entityScopes: ENTITY_SCOPES_ARRAY_POLICY }, +}) +if (deltaHasOperations(delta)) { + await api.patch('/resource', delta) +} +``` + +## Repository + +[github.com/MAKS-IT-COM/maksit-webui](https://github.com/MAKS-IT-COM/maksit-webui) — `src/` + +## License + +MIT diff --git a/src/packages/components/src/components/DataTable/DataTable.tsx b/src/components/components/DataTable/DataTable.tsx similarity index 99% rename from src/packages/components/src/components/DataTable/DataTable.tsx rename to src/components/components/DataTable/DataTable.tsx index a5e120b..8dd8514 100644 --- a/src/packages/components/src/components/DataTable/DataTable.tsx +++ b/src/components/components/DataTable/DataTable.tsx @@ -1,7 +1,7 @@ import React, { useState, useMemo, useRef, useEffect } from 'react' import { AutoSizer, MultiGrid, GridCellProps } from 'react-virtualized' -import { mapPagedToDataTable, type DataTablePageView, type PagedResponse } from '@maks-it.com/webui-core' +import { mapPagedToDataTable, type DataTablePageView, type PagedResponse } from '@webui/core' import { Plus, Trash2, Edit } from 'lucide-react' import { debounce, colSpanClass, type GridColSpan } from '../../functions' diff --git a/src/packages/components/src/components/DataTable/DataTableFilter.tsx b/src/components/components/DataTable/DataTableFilter.tsx similarity index 100% rename from src/packages/components/src/components/DataTable/DataTableFilter.tsx rename to src/components/components/DataTable/DataTableFilter.tsx diff --git a/src/packages/components/src/components/DataTable/DataTableLabel.tsx b/src/components/components/DataTable/DataTableLabel.tsx similarity index 95% rename from src/packages/components/src/components/DataTable/DataTableLabel.tsx rename to src/components/components/DataTable/DataTableLabel.tsx index c85687f..ad2ad76 100644 --- a/src/packages/components/src/components/DataTable/DataTableLabel.tsx +++ b/src/components/components/DataTable/DataTableLabel.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react' -import { formatISODateString } from '@maks-it.com/webui-core' +import { formatISODateString } from '@webui/core' interface NormalLabelProps { type: 'normal' diff --git a/src/packages/components/src/components/DataTable/helpers.ts b/src/components/components/DataTable/helpers.ts similarity index 100% rename from src/packages/components/src/components/DataTable/helpers.ts rename to src/components/components/DataTable/helpers.ts diff --git a/src/packages/components/src/components/DataTable/index.ts b/src/components/components/DataTable/index.ts similarity index 100% rename from src/packages/components/src/components/DataTable/index.ts rename to src/components/components/DataTable/index.ts diff --git a/src/packages/components/src/components/FormLayout/FormContainer.tsx b/src/components/components/FormLayout/FormContainer.tsx similarity index 100% rename from src/packages/components/src/components/FormLayout/FormContainer.tsx rename to src/components/components/FormLayout/FormContainer.tsx diff --git a/src/packages/components/src/components/FormLayout/FormContent.tsx b/src/components/components/FormLayout/FormContent.tsx similarity index 100% rename from src/packages/components/src/components/FormLayout/FormContent.tsx rename to src/components/components/FormLayout/FormContent.tsx diff --git a/src/packages/components/src/components/FormLayout/FormFooter.tsx b/src/components/components/FormLayout/FormFooter.tsx similarity index 100% rename from src/packages/components/src/components/FormLayout/FormFooter.tsx rename to src/components/components/FormLayout/FormFooter.tsx diff --git a/src/packages/components/src/components/FormLayout/FormHeader.tsx b/src/components/components/FormLayout/FormHeader.tsx similarity index 100% rename from src/packages/components/src/components/FormLayout/FormHeader.tsx rename to src/components/components/FormLayout/FormHeader.tsx diff --git a/src/packages/components/src/components/FormLayout/index.ts b/src/components/components/FormLayout/index.ts similarity index 100% rename from src/packages/components/src/components/FormLayout/index.ts rename to src/components/components/FormLayout/index.ts diff --git a/src/packages/components/src/components/Layout/Container.tsx b/src/components/components/Layout/Container.tsx similarity index 100% rename from src/packages/components/src/components/Layout/Container.tsx rename to src/components/components/Layout/Container.tsx diff --git a/src/packages/components/src/components/Layout/Content.tsx b/src/components/components/Layout/Content.tsx similarity index 100% rename from src/packages/components/src/components/Layout/Content.tsx rename to src/components/components/Layout/Content.tsx diff --git a/src/packages/components/src/components/Layout/Footer.tsx b/src/components/components/Layout/Footer.tsx similarity index 100% rename from src/packages/components/src/components/Layout/Footer.tsx rename to src/components/components/Layout/Footer.tsx diff --git a/src/packages/components/src/components/Layout/Header.tsx b/src/components/components/Layout/Header.tsx similarity index 100% rename from src/packages/components/src/components/Layout/Header.tsx rename to src/components/components/Layout/Header.tsx diff --git a/src/packages/components/src/components/Layout/MainContainer.tsx b/src/components/components/Layout/MainContainer.tsx similarity index 100% rename from src/packages/components/src/components/Layout/MainContainer.tsx rename to src/components/components/Layout/MainContainer.tsx diff --git a/src/packages/components/src/components/Layout/SideMenu/Container.tsx b/src/components/components/Layout/SideMenu/Container.tsx similarity index 100% rename from src/packages/components/src/components/Layout/SideMenu/Container.tsx rename to src/components/components/Layout/SideMenu/Container.tsx diff --git a/src/packages/components/src/components/Layout/SideMenu/Content.tsx b/src/components/components/Layout/SideMenu/Content.tsx similarity index 100% rename from src/packages/components/src/components/Layout/SideMenu/Content.tsx rename to src/components/components/Layout/SideMenu/Content.tsx diff --git a/src/packages/components/src/components/Layout/SideMenu/Footer.tsx b/src/components/components/Layout/SideMenu/Footer.tsx similarity index 100% rename from src/packages/components/src/components/Layout/SideMenu/Footer.tsx rename to src/components/components/Layout/SideMenu/Footer.tsx diff --git a/src/packages/components/src/components/Layout/SideMenu/Header.tsx b/src/components/components/Layout/SideMenu/Header.tsx similarity index 100% rename from src/packages/components/src/components/Layout/SideMenu/Header.tsx rename to src/components/components/Layout/SideMenu/Header.tsx diff --git a/src/packages/components/src/components/Layout/SideMenu/index.tsx b/src/components/components/Layout/SideMenu/index.tsx similarity index 100% rename from src/packages/components/src/components/Layout/SideMenu/index.tsx rename to src/components/components/Layout/SideMenu/index.tsx diff --git a/src/packages/components/src/components/Layout/index.tsx b/src/components/components/Layout/index.tsx similarity index 100% rename from src/packages/components/src/components/Layout/index.tsx rename to src/components/components/Layout/index.tsx diff --git a/src/packages/components/src/components/LazyLoadTable.tsx b/src/components/components/LazyLoadTable.tsx similarity index 100% rename from src/packages/components/src/components/LazyLoadTable.tsx rename to src/components/components/LazyLoadTable.tsx diff --git a/src/packages/components/src/components/Offcanvas.tsx b/src/components/components/Offcanvas.tsx similarity index 100% rename from src/packages/components/src/components/Offcanvas.tsx rename to src/components/components/Offcanvas.tsx diff --git a/src/packages/components/src/components/Scopes/EntityScopesSummary.tsx b/src/components/components/Scopes/EntityScopesSummary.tsx similarity index 96% rename from src/packages/components/src/components/Scopes/EntityScopesSummary.tsx rename to src/components/components/Scopes/EntityScopesSummary.tsx index 34abae1..5a1751f 100644 --- a/src/packages/components/src/components/Scopes/EntityScopesSummary.tsx +++ b/src/components/components/Scopes/EntityScopesSummary.tsx @@ -1,4 +1,4 @@ -import type { SearchEntityScopeEntry } from '@maks-it.com/webui-contracts' +import type { SearchEntityScopeEntry } from '@webui/contracts' export interface EntityScopesSummaryProps { entries: SearchEntityScopeEntry[] diff --git a/src/packages/components/src/components/Scopes/index.ts b/src/components/components/Scopes/index.ts similarity index 100% rename from src/packages/components/src/components/Scopes/index.ts rename to src/components/components/Scopes/index.ts diff --git a/src/packages/components/src/components/Toast/addToast.ts b/src/components/components/Toast/addToast.ts similarity index 100% rename from src/packages/components/src/components/Toast/addToast.ts rename to src/components/components/Toast/addToast.ts diff --git a/src/packages/components/src/components/Toast/createToastId.ts b/src/components/components/Toast/createToastId.ts similarity index 100% rename from src/packages/components/src/components/Toast/createToastId.ts rename to src/components/components/Toast/createToastId.ts diff --git a/src/packages/components/src/components/Toast/index.tsx b/src/components/components/Toast/index.tsx similarity index 100% rename from src/packages/components/src/components/Toast/index.tsx rename to src/components/components/Toast/index.tsx diff --git a/src/packages/components/src/components/editors/ButtonComponent.tsx b/src/components/components/editors/ButtonComponent.tsx similarity index 100% rename from src/packages/components/src/components/editors/ButtonComponent.tsx rename to src/components/components/editors/ButtonComponent.tsx diff --git a/src/packages/components/src/components/editors/CheckBoxComponent.tsx b/src/components/components/editors/CheckBoxComponent.tsx similarity index 100% rename from src/packages/components/src/components/editors/CheckBoxComponent.tsx rename to src/components/components/editors/CheckBoxComponent.tsx diff --git a/src/packages/components/src/components/editors/DateTimePickerComponent.tsx b/src/components/components/editors/DateTimePickerComponent.tsx similarity index 99% rename from src/packages/components/src/components/editors/DateTimePickerComponent.tsx rename to src/components/components/editors/DateTimePickerComponent.tsx index 5e6d1b9..f9db17f 100644 --- a/src/packages/components/src/components/editors/DateTimePickerComponent.tsx +++ b/src/components/components/editors/DateTimePickerComponent.tsx @@ -6,7 +6,7 @@ import { getDaysInMonth, addMonths, subMonths, -} from '@maks-it.com/webui-core' +} from '@webui/core' import { CircleX } from 'lucide-react' import { ButtonComponent } from './ButtonComponent' import { TextBoxComponent } from './TextBoxComponent' diff --git a/src/packages/components/src/components/editors/DualListboxComponent.tsx b/src/components/components/editors/DualListboxComponent.tsx similarity index 100% rename from src/packages/components/src/components/editors/DualListboxComponent.tsx rename to src/components/components/editors/DualListboxComponent.tsx diff --git a/src/packages/components/src/components/editors/FieldContainer.tsx b/src/components/components/editors/FieldContainer.tsx similarity index 100% rename from src/packages/components/src/components/editors/FieldContainer.tsx rename to src/components/components/editors/FieldContainer.tsx diff --git a/src/packages/components/src/components/editors/FileUploadComponent.tsx b/src/components/components/editors/FileUploadComponent.tsx similarity index 100% rename from src/packages/components/src/components/editors/FileUploadComponent.tsx rename to src/components/components/editors/FileUploadComponent.tsx diff --git a/src/packages/components/src/components/editors/ListBoxComponent.tsx b/src/components/components/editors/ListBoxComponent.tsx similarity index 100% rename from src/packages/components/src/components/editors/ListBoxComponent.tsx rename to src/components/components/editors/ListBoxComponent.tsx diff --git a/src/packages/components/src/components/editors/RadioGroupComponent.tsx b/src/components/components/editors/RadioGroupComponent.tsx similarity index 100% rename from src/packages/components/src/components/editors/RadioGroupComponent.tsx rename to src/components/components/editors/RadioGroupComponent.tsx diff --git a/src/packages/components/src/components/editors/RemoteSelectBoxComponent.tsx b/src/components/components/editors/RemoteSelectBoxComponent.tsx similarity index 93% rename from src/packages/components/src/components/editors/RemoteSelectBoxComponent.tsx rename to src/components/components/editors/RemoteSelectBoxComponent.tsx index aadc9e3..10bdeef 100644 --- a/src/packages/components/src/components/editors/RemoteSelectBoxComponent.tsx +++ b/src/components/components/editors/RemoteSelectBoxComponent.tsx @@ -1,7 +1,7 @@ import { useState, useCallback, ChangeEvent, useEffect, useRef } from 'react' -import type { PagedRequest } from '@maks-it.com/webui-contracts' -import type { SearchResponseBase } from '@maks-it.com/webui-contracts' -import { deepEqual } from '@maks-it.com/webui-core' +import type { PagedRequest } from '@webui/contracts' +import type { SearchResponseBase } from '@webui/contracts' +import { deepEqual } from '@webui/core' import type { GridColSpan } from '../../functions' import { SelectBoxComponent } from './SelectBoxComponent' diff --git a/src/packages/components/src/components/editors/SecretComponent.tsx b/src/components/components/editors/SecretComponent.tsx similarity index 100% rename from src/packages/components/src/components/editors/SecretComponent.tsx rename to src/components/components/editors/SecretComponent.tsx diff --git a/src/packages/components/src/components/editors/SelectBoxComponent.tsx b/src/components/components/editors/SelectBoxComponent.tsx similarity index 100% rename from src/packages/components/src/components/editors/SelectBoxComponent.tsx rename to src/components/components/editors/SelectBoxComponent.tsx diff --git a/src/packages/components/src/components/editors/TextBoxComponent.tsx b/src/components/components/editors/TextBoxComponent.tsx similarity index 100% rename from src/packages/components/src/components/editors/TextBoxComponent.tsx rename to src/components/components/editors/TextBoxComponent.tsx diff --git a/src/packages/components/src/components/editors/TreeViewComponent.tsx b/src/components/components/editors/TreeViewComponent.tsx similarity index 100% rename from src/packages/components/src/components/editors/TreeViewComponent.tsx rename to src/components/components/editors/TreeViewComponent.tsx diff --git a/src/packages/components/src/components/editors/editorStyles.ts b/src/components/components/editors/editorStyles.ts similarity index 100% rename from src/packages/components/src/components/editors/editorStyles.ts rename to src/components/components/editors/editorStyles.ts diff --git a/src/packages/components/src/components/editors/index.ts b/src/components/components/editors/index.ts similarity index 100% rename from src/packages/components/src/components/editors/index.ts rename to src/components/components/editors/index.ts diff --git a/src/packages/components/src/functions/debounce.ts b/src/components/functions/debounce.ts similarity index 100% rename from src/packages/components/src/functions/debounce.ts rename to src/components/functions/debounce.ts diff --git a/src/packages/components/src/functions/index.ts b/src/components/functions/index.ts similarity index 100% rename from src/packages/components/src/functions/index.ts rename to src/components/functions/index.ts diff --git a/src/packages/components/src/functions/tailwind/gridColSpan.ts b/src/components/functions/tailwind/gridColSpan.ts similarity index 100% rename from src/packages/components/src/functions/tailwind/gridColSpan.ts rename to src/components/functions/tailwind/gridColSpan.ts diff --git a/src/packages/components/src/functions/tailwind/index.ts b/src/components/functions/tailwind/index.ts similarity index 100% rename from src/packages/components/src/functions/tailwind/index.ts rename to src/components/functions/tailwind/index.ts diff --git a/src/packages/components/src/index.ts b/src/components/index.ts similarity index 100% rename from src/packages/components/src/index.ts rename to src/components/index.ts diff --git a/src/components/oauth/ExternalLoginButtons.tsx b/src/components/oauth/ExternalLoginButtons.tsx new file mode 100644 index 0000000..13740d7 --- /dev/null +++ b/src/components/oauth/ExternalLoginButtons.tsx @@ -0,0 +1,59 @@ +import type { FC, ReactNode } from 'react' +import { LoginProviderExternal } from '@webui/contracts' + +export interface ExternalLoginProviderConfig { + provider: LoginProviderExternal + label: string + iconSrc?: string +} + +export interface ExternalLoginButtonsProps { + providers: ExternalLoginProviderConfig[] + onSelect: (provider: LoginProviderExternal) => void + className?: string + renderButton?: (props: { + provider: LoginProviderExternal + label: string + iconSrc?: string + onClick: () => void + }) => ReactNode +} + +const defaultRenderButton = ({ + label, + iconSrc, + onClick, +}: { + label: string + iconSrc?: string + onClick: () => void +}) => ( + } + {label} + +) + +const ExternalLoginButtons: FC = ({ + providers, + onSelect, + className, + renderButton = defaultRenderButton, +}) => ( +
+ {providers.map(({ provider, label, iconSrc }) => + renderButton({ + provider, + label, + iconSrc, + onClick: () => onSelect(provider), + }), + )} +
+) + +export { ExternalLoginButtons } diff --git a/src/components/oauth/IdpLoginCallbackHandler.tsx b/src/components/oauth/IdpLoginCallbackHandler.tsx new file mode 100644 index 0000000..4a55c3c --- /dev/null +++ b/src/components/oauth/IdpLoginCallbackHandler.tsx @@ -0,0 +1,14 @@ +import type { FC } from 'react' +import { useIdpLoginCallback, type UseIdpLoginCallbackOptions } from '@webui/core' + +export type IdpLoginCallbackHandlerProps = UseIdpLoginCallbackOptions + +const IdpLoginCallbackHandler: FC = (props) => { + useIdpLoginCallback(props) + return null +} + +export { IdpLoginCallbackHandler } +export type { LoginExternalExchangeResponse } from '@webui/contracts' +/** @deprecated Use `LoginExternalExchangeResponse`. */ +export type { IdpRedeemLoginResponse } from '@webui/contracts' diff --git a/src/components/oauth/MailboxOAuthReturnHandler.tsx b/src/components/oauth/MailboxOAuthReturnHandler.tsx new file mode 100644 index 0000000..cb062a3 --- /dev/null +++ b/src/components/oauth/MailboxOAuthReturnHandler.tsx @@ -0,0 +1,11 @@ +import type { FC } from 'react' +import { useMailboxOAuthReturn, type UseMailboxOAuthReturnOptions } from '@webui/core' + +export type MailboxOAuthReturnHandlerProps = UseMailboxOAuthReturnOptions + +const MailboxOAuthReturnHandler: FC = (props) => { + useMailboxOAuthReturn(props) + return null +} + +export { MailboxOAuthReturnHandler } diff --git a/src/components/oauth/index.ts b/src/components/oauth/index.ts new file mode 100644 index 0000000..f2e7478 --- /dev/null +++ b/src/components/oauth/index.ts @@ -0,0 +1,15 @@ +export { + ExternalLoginButtons, + type ExternalLoginButtonsProps, + type ExternalLoginProviderConfig, +} from './ExternalLoginButtons' + +export { + IdpLoginCallbackHandler, + type IdpLoginCallbackHandlerProps, +} from './IdpLoginCallbackHandler' + +export { + MailboxOAuthReturnHandler, + type MailboxOAuthReturnHandlerProps, +} from './MailboxOAuthReturnHandler' diff --git a/src/packages/contracts/src/PagedRequest.ts b/src/contracts/PagedRequest.ts similarity index 74% rename from src/packages/contracts/src/PagedRequest.ts rename to src/contracts/PagedRequest.ts index 62f4a5d..0de0b4e 100644 --- a/src/packages/contracts/src/PagedRequest.ts +++ b/src/contracts/PagedRequest.ts @@ -1,12 +1,12 @@ -import { boolean, intersection, number, object, record, string, type ZodType } from 'zod' +import { boolean, intersection, number, object, string, type ZodType } from 'zod' import type { RequestModelBase } from './RequestModelBase' import { RequestModelBaseSchema } from './RequestModelBase' +/** Matches server `MaksIT.Core.Webapi.Models.PagedRequest` (camelCase JSON). */ export interface PagedRequest extends RequestModelBase { pageSize?: number pageNumber?: number filters?: string - collectionFilters?: Record sortBy?: string isAscending?: boolean } @@ -17,7 +17,6 @@ export const PagedRequestSchema: ZodType = intersection( pageSize: number().optional(), pageNumber: number().optional(), filters: string().optional(), - collectionFilters: record(string(), string()).optional(), sortBy: string().optional(), isAscending: boolean().optional(), }) diff --git a/src/packages/contracts/src/PagedResponse.ts b/src/contracts/PagedResponse.ts similarity index 100% rename from src/packages/contracts/src/PagedResponse.ts rename to src/contracts/PagedResponse.ts diff --git a/src/packages/contracts/src/PatchOperation.ts b/src/contracts/PatchOperation.ts similarity index 100% rename from src/packages/contracts/src/PatchOperation.ts rename to src/contracts/PatchOperation.ts diff --git a/src/packages/contracts/src/PatchRequestModelBase.ts b/src/contracts/PatchRequestModelBase.ts similarity index 100% rename from src/packages/contracts/src/PatchRequestModelBase.ts rename to src/contracts/PatchRequestModelBase.ts diff --git a/src/packages/contracts/src/ProblemDetails.ts b/src/contracts/ProblemDetails.ts similarity index 100% rename from src/packages/contracts/src/ProblemDetails.ts rename to src/contracts/ProblemDetails.ts diff --git a/src/packages/contracts/src/RequestModelBase.ts b/src/contracts/RequestModelBase.ts similarity index 100% rename from src/packages/contracts/src/RequestModelBase.ts rename to src/contracts/RequestModelBase.ts diff --git a/src/packages/contracts/src/ResponseModelBase.ts b/src/contracts/ResponseModelBase.ts similarity index 100% rename from src/packages/contracts/src/ResponseModelBase.ts rename to src/contracts/ResponseModelBase.ts diff --git a/src/contracts/SearchResponseBase.ts b/src/contracts/SearchResponseBase.ts new file mode 100644 index 0000000..1ff2b16 --- /dev/null +++ b/src/contracts/SearchResponseBase.ts @@ -0,0 +1,11 @@ +import type { ResponseModelBase } from './ResponseModelBase' + +/** + * Minimal id + display label for remote select / paged search helpers. + * Not a MaksIT.Core type — product `Search*Response` models add domain fields + * (e.g. `username` on users, `description` on API keys). Map to `name` in the UI layer. + */ +export interface SearchResponseBase extends ResponseModelBase { + id: string + name: string +} \ No newline at end of file diff --git a/src/contracts/TrngResponse.ts b/src/contracts/TrngResponse.ts new file mode 100644 index 0000000..81af44c --- /dev/null +++ b/src/contracts/TrngResponse.ts @@ -0,0 +1,6 @@ +import type { ResponseModelBase } from './ResponseModelBase' + +/** Matches `GET api/secret/generatesecret` anonymous response `{ secret }` (e.g. MaksIT.Vault). */ +export interface TrngResponse extends ResponseModelBase { + secret: string +} \ No newline at end of file diff --git a/src/packages/contracts/src/identity/Claims.ts b/src/contracts/identity/Claims.ts similarity index 100% rename from src/packages/contracts/src/identity/Claims.ts rename to src/contracts/identity/Claims.ts diff --git a/src/packages/contracts/src/identity/login/LoginRequest.ts b/src/contracts/identity/login/LoginRequest.ts similarity index 100% rename from src/packages/contracts/src/identity/login/LoginRequest.ts rename to src/contracts/identity/login/LoginRequest.ts diff --git a/src/packages/contracts/src/identity/login/LoginResponse.ts b/src/contracts/identity/login/LoginResponse.ts similarity index 100% rename from src/packages/contracts/src/identity/login/LoginResponse.ts rename to src/contracts/identity/login/LoginResponse.ts diff --git a/src/packages/contracts/src/identity/login/RefreshTokenRequest.ts b/src/contracts/identity/login/RefreshTokenRequest.ts similarity index 100% rename from src/packages/contracts/src/identity/login/RefreshTokenRequest.ts rename to src/contracts/identity/login/RefreshTokenRequest.ts diff --git a/src/contracts/identity/logout/LogoutRequest.ts b/src/contracts/identity/logout/LogoutRequest.ts new file mode 100644 index 0000000..43af810 --- /dev/null +++ b/src/contracts/identity/logout/LogoutRequest.ts @@ -0,0 +1,14 @@ +import { boolean, object, type ZodType } from 'zod' +import type { RequestModelBase } from '../../RequestModelBase' +import { RequestModelBaseSchema } from '../../RequestModelBase' + +/** Matches product `Models/Identity/Logout/LogoutRequest` (e.g. MaksIT.CertsUI). */ +export interface LogoutRequest extends RequestModelBase { + logoutFromAllDevices?: boolean +} + +export const LogoutRequestSchema: ZodType = RequestModelBaseSchema.and( + object({ + logoutFromAllDevices: boolean().optional(), + }) +) diff --git a/src/packages/contracts/src/identity/logout/LogoutResponse.ts b/src/contracts/identity/logout/LogoutResponse.ts similarity index 100% rename from src/packages/contracts/src/identity/logout/LogoutResponse.ts rename to src/contracts/identity/logout/LogoutResponse.ts diff --git a/src/packages/contracts/src/index.ts b/src/contracts/index.ts similarity index 100% rename from src/packages/contracts/src/index.ts rename to src/contracts/index.ts diff --git a/src/contracts/oauth/EmailProtocol.ts b/src/contracts/oauth/EmailProtocol.ts new file mode 100644 index 0000000..3c33b25 --- /dev/null +++ b/src/contracts/oauth/EmailProtocol.ts @@ -0,0 +1,9 @@ +/** + * Mailbox email protocol flags for multi-step OAuth UI. + * FiPlan/legacy FE shape — not in MaksIT.IdentityHub.Contracts. + */ +export enum EmailProtocol { + Smtp = 0, + Pop3 = 1, + Imap = 2, +} diff --git a/src/contracts/oauth/IdpRedeemLoginResponse.ts b/src/contracts/oauth/IdpRedeemLoginResponse.ts new file mode 100644 index 0000000..3a1c0f8 --- /dev/null +++ b/src/contracts/oauth/IdpRedeemLoginResponse.ts @@ -0,0 +1,6 @@ +import type { LoginExternalExchangeResponse } from './LoginExternalExchangeResponse' + +/** + * @deprecated Use {@link LoginExternalExchangeResponse}. Legacy FiPlan.Auth snake_case shape removed. + */ +export type IdpRedeemLoginResponse = LoginExternalExchangeResponse diff --git a/src/contracts/oauth/LoginExternalExchangeResponse.ts b/src/contracts/oauth/LoginExternalExchangeResponse.ts new file mode 100644 index 0000000..4ca3fb5 --- /dev/null +++ b/src/contracts/oauth/LoginExternalExchangeResponse.ts @@ -0,0 +1,16 @@ +import type { ResponseModelBase } from '../ResponseModelBase' + +/** + * Response from Identity Hub `POST login-external/redeem`. + * Align with `MaksIT.IdentityHub.Contracts.LoginExternalExchangeResponse`. + */ +export interface LoginExternalExchangeResponse extends ResponseModelBase { + provider: string + subject: string + email: string + idToken: string + accessToken: string + refreshToken?: string + expiresAt?: string + postLogin?: string +} diff --git a/src/contracts/oauth/LoginProviderExternal.ts b/src/contracts/oauth/LoginProviderExternal.ts new file mode 100644 index 0000000..702b6f2 --- /dev/null +++ b/src/contracts/oauth/LoginProviderExternal.ts @@ -0,0 +1,23 @@ +/** + * External identity provider for Identity Hub login (`login-external/challenge`). + * Values match `MaksIT.IdentityHub.Engine.LoginProviderExternal` names (`Microsoft`, `Google`). + */ +export enum LoginProviderExternal { + Microsoft = 'Microsoft', + Google = 'Google', +} + +export const toAuthProviderName = (provider: LoginProviderExternal): string => provider + +export const tryParseLoginProviderExternal = ( + value: string | null | undefined, +): LoginProviderExternal | undefined => { + if (!value) + return undefined + + const normalized = value.trim() + if (normalized === LoginProviderExternal.Google || normalized === LoginProviderExternal.Microsoft) + return normalized + + return undefined +} diff --git a/src/contracts/oauth/MailboxOAuthPending.ts b/src/contracts/oauth/MailboxOAuthPending.ts new file mode 100644 index 0000000..908c0c7 --- /dev/null +++ b/src/contracts/oauth/MailboxOAuthPending.ts @@ -0,0 +1,8 @@ +import type { EmailProtocol } from './EmailProtocol' + +/** sessionStorage payload during multi-protocol mailbox OAuth (FE-only). */ +export interface MailboxOAuthPending { + mailboxId: number + protocols: EmailProtocol[] + returnPath: string +} diff --git a/src/contracts/oauth/MsalViewModel.ts b/src/contracts/oauth/MsalViewModel.ts new file mode 100644 index 0000000..e546864 --- /dev/null +++ b/src/contracts/oauth/MsalViewModel.ts @@ -0,0 +1,7 @@ +import type { EmailProtocol } from './EmailProtocol' + +/** FiPlan/legacy mailbox redeem payload — not Identity Hub `LoginExternalExchangeResponse`. */ +export interface MsalViewModel { + code: string + emailProtocol: EmailProtocol +} diff --git a/src/contracts/oauth/RedeemLoginExternalRequest.ts b/src/contracts/oauth/RedeemLoginExternalRequest.ts new file mode 100644 index 0000000..caedc2d --- /dev/null +++ b/src/contracts/oauth/RedeemLoginExternalRequest.ts @@ -0,0 +1,18 @@ +import { object, string, type ZodType } from 'zod' +import type { RequestModelBase } from '../RequestModelBase' +import { RequestModelBaseSchema } from '../RequestModelBase' + +/** + * Body for Identity Hub `POST login-external/redeem`. + * Align with `MaksIT.IdentityHub.Contracts.RedeemLoginExternalRequest`. + */ +export interface RedeemLoginExternalRequest extends RequestModelBase { + code: string +} + +export const RedeemLoginExternalRequestSchema: ZodType = + RequestModelBaseSchema.and( + object({ + code: string().min(1), + }), + ) diff --git a/src/contracts/oauth/RefreshMailboxAccessTokenRequest.ts b/src/contracts/oauth/RefreshMailboxAccessTokenRequest.ts new file mode 100644 index 0000000..a7f2bac --- /dev/null +++ b/src/contracts/oauth/RefreshMailboxAccessTokenRequest.ts @@ -0,0 +1,20 @@ +import { object, string, type ZodType } from 'zod' +import type { RequestModelBase } from '../RequestModelBase' +import { RequestModelBaseSchema } from '../RequestModelBase' + +/** + * Body for Identity Hub `POST login-external/refresh-mailbox`. + * Align with `MaksIT.IdentityHub.Contracts.RefreshMailboxAccessTokenRequest`. + */ +export interface RefreshMailboxAccessTokenRequest extends RequestModelBase { + provider: string + refreshToken: string +} + +export const RefreshMailboxAccessTokenRequestSchema: ZodType = + RequestModelBaseSchema.and( + object({ + provider: string().min(1), + refreshToken: string().min(1), + }), + ) diff --git a/src/contracts/oauth/RefreshMailboxAccessTokenResponse.ts b/src/contracts/oauth/RefreshMailboxAccessTokenResponse.ts new file mode 100644 index 0000000..28c533a --- /dev/null +++ b/src/contracts/oauth/RefreshMailboxAccessTokenResponse.ts @@ -0,0 +1,9 @@ +import type { ResponseModelBase } from '../ResponseModelBase' + +/** + * Response from Identity Hub `POST login-external/refresh-mailbox`. + * Align with `MaksIT.IdentityHub.Contracts.RefreshMailboxAccessTokenResponse`. + */ +export interface RefreshMailboxAccessTokenResponse extends ResponseModelBase { + accessToken: string +} diff --git a/src/contracts/oauth/ServerAuthenticationMethod.ts b/src/contracts/oauth/ServerAuthenticationMethod.ts new file mode 100644 index 0000000..e1b65d2 --- /dev/null +++ b/src/contracts/oauth/ServerAuthenticationMethod.ts @@ -0,0 +1,9 @@ +/** + * Mailbox server authentication method (OAuth2 vs login). + * FiPlan/legacy FE shape — not in MaksIT.IdentityHub.Contracts. + */ +export enum ServerAuthenticationMethod { + Default = 0, + Login = 1, + OAuth2 = 6, +} diff --git a/src/contracts/oauth/index.ts b/src/contracts/oauth/index.ts new file mode 100644 index 0000000..2b707dc --- /dev/null +++ b/src/contracts/oauth/index.ts @@ -0,0 +1,37 @@ +export { + LoginProviderExternal, + toAuthProviderName, + tryParseLoginProviderExternal, +} from './LoginProviderExternal' + +export { EmailProtocol } from './EmailProtocol' + +export { ServerAuthenticationMethod } from './ServerAuthenticationMethod' + +export type { MsalViewModel } from './MsalViewModel' + +export type { LoginExternalExchangeResponse } from './LoginExternalExchangeResponse' + +export type { RedeemLoginExternalRequest } from './RedeemLoginExternalRequest' +export { RedeemLoginExternalRequestSchema } from './RedeemLoginExternalRequest' + +export type { RefreshMailboxAccessTokenRequest } from './RefreshMailboxAccessTokenRequest' +export { RefreshMailboxAccessTokenRequestSchema } from './RefreshMailboxAccessTokenRequest' + +export type { RefreshMailboxAccessTokenResponse } from './RefreshMailboxAccessTokenResponse' + +/** @deprecated Use `LoginExternalExchangeResponse`. */ +export type { IdpRedeemLoginResponse } from './IdpRedeemLoginResponse' + +export type { MailboxOAuthPending } from './MailboxOAuthPending' + +export { + MAILBOX_OAUTH_STORAGE_KEY, + MAILBOX_OAUTH_QUERY_FLAG, + MAILBOX_OAUTH_PROCESSING_KEY_PREFIX, + IDP_PROVIDER_STORAGE_KEY, + IDENTITY_HUB_ROUTE_CHALLENGE, + IDENTITY_HUB_ROUTE_REDEEM, + IDENTITY_HUB_ROUTE_REFRESH_MAILBOX, + IDENTITY_HUB_MAILBOX_PURPOSE, +} from './oauthConstants' diff --git a/src/contracts/oauth/oauthConstants.ts b/src/contracts/oauth/oauthConstants.ts new file mode 100644 index 0000000..25af13f --- /dev/null +++ b/src/contracts/oauth/oauthConstants.ts @@ -0,0 +1,19 @@ +/** sessionStorage key while a multi-protocol mailbox OAuth flow is in progress. */ +export const MAILBOX_OAUTH_STORAGE_KEY = 'mailboxOAuth_pending' + +/** Query flag on the OAuth redirect landing URL (`?mailboxOAuth=1`). */ +export const MAILBOX_OAUTH_QUERY_FLAG = 'mailboxOAuth' + +/** sessionStorage prefix to dedupe concurrent code redemption for the same authorization code. */ +export const MAILBOX_OAUTH_PROCESSING_KEY_PREFIX = 'mailboxOAuth_processing_' + +/** sessionStorage key for the last external login provider (optional diagnostics). */ +export const IDP_PROVIDER_STORAGE_KEY = 'ext_provider' + +/** Identity Hub route segments (append to hub base URL). */ +export const IDENTITY_HUB_ROUTE_CHALLENGE = 'login-external/challenge' +export const IDENTITY_HUB_ROUTE_REDEEM = 'login-external/redeem' +export const IDENTITY_HUB_ROUTE_REFRESH_MAILBOX = 'login-external/refresh-mailbox' + +/** Query value for mailbox OAuth challenge (`?purpose=mailbox`). Matches `HttpExternalLoginSessionContext`. */ +export const IDENTITY_HUB_MAILBOX_PURPOSE = 'mailbox' diff --git a/src/packages/contracts/src/shared/search/SearchEntityScopeEntry.ts b/src/contracts/shared/search/SearchEntityScopeEntry.ts similarity index 55% rename from src/packages/contracts/src/shared/search/SearchEntityScopeEntry.ts rename to src/contracts/shared/search/SearchEntityScopeEntry.ts index 2a37f6e..eaadcdb 100644 --- a/src/packages/contracts/src/shared/search/SearchEntityScopeEntry.ts +++ b/src/contracts/shared/search/SearchEntityScopeEntry.ts @@ -1,4 +1,7 @@ -/** One entity scope line in search results (entity + CRUD permission flags). */ +/** + * One entity scope line in search results (entity + CRUD permission flags). + * Align with product `Models/Shared/Search/SearchEntityScopeEntry.cs` (certs-ui, vault). + */ export interface SearchEntityScopeEntry { scopeEntityType: TScopeEntityType entityId: string diff --git a/src/packages/core/src/functions/acl/index.ts b/src/core/functions/acl/index.ts similarity index 100% rename from src/packages/core/src/functions/acl/index.ts rename to src/core/functions/acl/index.ts diff --git a/src/packages/core/src/functions/acl/parseAclEntry.ts b/src/core/functions/acl/parseAclEntry.ts similarity index 100% rename from src/packages/core/src/functions/acl/parseAclEntry.ts rename to src/core/functions/acl/parseAclEntry.ts diff --git a/src/packages/core/src/functions/dataTable/dataTableFilters.ts b/src/core/functions/dataTable/dataTableFilters.ts similarity index 100% rename from src/packages/core/src/functions/dataTable/dataTableFilters.ts rename to src/core/functions/dataTable/dataTableFilters.ts diff --git a/src/packages/core/src/functions/dataTable/dataTablePaged.ts b/src/core/functions/dataTable/dataTablePaged.ts similarity index 95% rename from src/packages/core/src/functions/dataTable/dataTablePaged.ts rename to src/core/functions/dataTable/dataTablePaged.ts index 11282f1..8ac2997 100644 --- a/src/packages/core/src/functions/dataTable/dataTablePaged.ts +++ b/src/core/functions/dataTable/dataTablePaged.ts @@ -1,4 +1,4 @@ -import type { PagedResponse } from '@maks-it.com/webui-contracts' +import type { PagedResponse } from '@webui/contracts' /** * Virtualized DataTable view model used by client paging and search helpers. diff --git a/src/packages/core/src/functions/dataTable/index.ts b/src/core/functions/dataTable/index.ts similarity index 100% rename from src/packages/core/src/functions/dataTable/index.ts rename to src/core/functions/dataTable/index.ts diff --git a/src/packages/core/src/functions/date/dateTimeToUtcIsoSchema.ts b/src/core/functions/date/dateTimeToUtcIsoSchema.ts similarity index 100% rename from src/packages/core/src/functions/date/dateTimeToUtcIsoSchema.ts rename to src/core/functions/date/dateTimeToUtcIsoSchema.ts diff --git a/src/packages/core/src/functions/date/formatISODateString.ts b/src/core/functions/date/formatISODateString.ts similarity index 100% rename from src/packages/core/src/functions/date/formatISODateString.ts rename to src/core/functions/date/formatISODateString.ts diff --git a/src/packages/core/src/functions/date/index.ts b/src/core/functions/date/index.ts similarity index 100% rename from src/packages/core/src/functions/date/index.ts rename to src/core/functions/date/index.ts diff --git a/src/packages/core/src/functions/date/isValidDateString.ts b/src/core/functions/date/isValidDateString.ts similarity index 100% rename from src/packages/core/src/functions/date/isValidDateString.ts rename to src/core/functions/date/isValidDateString.ts diff --git a/src/packages/core/src/functions/deep/deepCopy.ts b/src/core/functions/deep/deepCopy.ts similarity index 98% rename from src/packages/core/src/functions/deep/deepCopy.ts rename to src/core/functions/deep/deepCopy.ts index ee43100..076a1fd 100644 --- a/src/packages/core/src/functions/deep/deepCopy.ts +++ b/src/core/functions/deep/deepCopy.ts @@ -8,7 +8,7 @@ * @param seen - Internal map of already-cloned objects (used for cycle detection). * @returns A deep copy of `obj`. */ -const deepCopy = (obj: T, seen = new WeakMap()): T =>{ +const deepCopy = (obj: T, seen = new WeakMap()): T => { if ( obj === null || typeof obj !== 'object' || diff --git a/src/packages/core/src/functions/deep/deepDelta.ts b/src/core/functions/deep/deepDelta.ts similarity index 94% rename from src/packages/core/src/functions/deep/deepDelta.ts rename to src/core/functions/deep/deepDelta.ts index 65e2782..4daacf1 100644 --- a/src/packages/core/src/functions/deep/deepDelta.ts +++ b/src/core/functions/deep/deepDelta.ts @@ -1,4 +1,4 @@ -import { COLLECTION_ITEM_OPERATION, PatchOperation } from '@maks-it.com/webui-contracts' +import { COLLECTION_ITEM_OPERATION, PatchOperation } from '@webui/contracts' import { deepCopy } from './deepCopy' import { deepEqual } from './deepEqual' @@ -29,7 +29,7 @@ export type ArrayPolicy = { rootKey?: string /** Child array field names to process on re-parenting (e.g. ['applicationRoles']) */ - childArrayKeys?: string[] + childArrayKeys?: readonly string[] /** If true, children are cleared on root change (default TRUE) */ dropChildrenOnRootChange?: boolean @@ -276,14 +276,23 @@ export const deepDelta = >( * 3. Else: return null → item will be treated as “new”. */ const resolveId = (item?: U): IdLike => { - if (!item) return null + if (!item) + return null + const directId = (item as Identifiable).id - if (directId !== null && directId !== undefined) return directId - if (!policy?.identityKey) return null + if (directId !== null && directId !== undefined) + return directId + + if (!policy?.identityKey) + return null if (typeof policy.identityKey === 'function') { - try { return policy.identityKey(item as unknown as Record) } - catch { return null } + try { + return policy.identityKey(item as unknown as Record) + } + catch { + return null + } } const k = policy.identityKey as string @@ -323,7 +332,9 @@ export const deepDelta = >( * When no `rootKey` is configured, all items are treated as having the same root. */ const sameRoot = (f: U, b: U): boolean => { - if (!rootKey) return true + if (!rootKey) + return true + return (f as PlainObject)[rootKey] === (b as PlainObject)[rootKey] } @@ -332,11 +343,14 @@ export const deepDelta = >( const backupMap = new Map() for (const item of formArray) { const id = resolveId(item) - if (id !== null && id !== undefined) formMap.set(id as string | number, item) + if (id !== null && id !== undefined) + formMap.set(id as string | number, item) } + for (const item of backupArray) { const id = resolveId(item) - if (id !== null && id !== undefined) backupMap.set(id as string | number, item) + if (id !== null && id !== undefined) + backupMap.set(id as string | number, item) } // 1) Items present in the form array @@ -410,7 +424,8 @@ export const deepDelta = >( ;(addItem as PlainObject)[ck] = [] } } - } else { + } + else { for (const ck of childrenKeys) { const v = (addItem as PlainObject)[ck] if (Array.isArray(v)) { @@ -466,7 +481,9 @@ export const deepDelta = >( // 2) Items removed for (const backupItem of backupArray) { const bid = resolveId(backupItem) - if (bid === null || bid === undefined) continue + if (bid === null || bid === undefined) + continue + if (!formMap.has(bid as string | number)) { const removeItem = {} as DeltaArrayItem ;(removeItem as PlainObject)[getIdFieldForItem(backupItem)] = bid as IdLike @@ -499,19 +516,25 @@ export const deepDelta = >( * @returns True if any operations are present, otherwise false. */ export const deltaHasOperations = >(delta: Delta): boolean => { - if (!isPlainObject(delta)) return false - if ('operations' in delta && isPlainObject(delta.operations)) return true + if (!isPlainObject(delta)) + return false + + if ('operations' in delta && isPlainObject(delta.operations)) + return true for (const key in delta) { const v = (delta as PlainObject)[key] - if (isPlainObject(v) && deltaHasOperations(v as Delta<{}>)) return true + if (isPlainObject(v) && deltaHasOperations(v as Delta<{}>)) + return true if (Array.isArray(v)) { for (const item of v) { - if (isPlainObject(item) && deltaHasOperations(item as Delta<{}>)) return true + if (isPlainObject(item) && deltaHasOperations(item as Delta<{}>)) + return true } } } + return false } diff --git a/src/packages/core/src/functions/deep/deepEqual.ts b/src/core/functions/deep/deepEqual.ts similarity index 100% rename from src/packages/core/src/functions/deep/deepEqual.ts rename to src/core/functions/deep/deepEqual.ts diff --git a/src/packages/core/src/functions/deep/deepMerge.ts b/src/core/functions/deep/deepMerge.ts similarity index 91% rename from src/packages/core/src/functions/deep/deepMerge.ts rename to src/core/functions/deep/deepMerge.ts index a1092b5..ce3a481 100644 --- a/src/packages/core/src/functions/deep/deepMerge.ts +++ b/src/core/functions/deep/deepMerge.ts @@ -11,8 +11,11 @@ * @returns A new merged structure (does not mutate `target` or `source`). */ const deepMerge = (target: T, source: T, seen = new WeakMap()): T => { - if (target === null || typeof target !== 'object') return source - if (source === null || typeof source !== 'object') return target + if (target === null || typeof target !== 'object') + return source + + if (source === null || typeof source !== 'object') + return target if (seen.has(target as object)) { return seen.get(target as object) as T diff --git a/src/packages/core/src/functions/deep/deepPatternMatch.ts b/src/core/functions/deep/deepPatternMatch.ts similarity index 77% rename from src/packages/core/src/functions/deep/deepPatternMatch.ts rename to src/core/functions/deep/deepPatternMatch.ts index 952159d..4f67d6f 100644 --- a/src/packages/core/src/functions/deep/deepPatternMatch.ts +++ b/src/core/functions/deep/deepPatternMatch.ts @@ -10,18 +10,24 @@ * @returns `true` when `obj` matches the pattern shape. */ const deepPatternMatch = (pattern: T, obj: unknown): boolean => { - if (typeof obj !== 'object' || obj === null) return false + if (typeof obj !== 'object' || obj === null) + return false + const objKeys = Object.keys(obj as object) const patternKeys = Object.keys(pattern) + // obj must not have more keys than pattern - if (objKeys.length > patternKeys.length) return false + if (objKeys.length > patternKeys.length) + return false + for (const key of objKeys) { - if (!(key in pattern)) return false - if (typeof (obj as T)[key as keyof T] !== typeof pattern[key as keyof T]) return false + if (!(key in pattern)) + return false + + if (typeof (obj as T)[key as keyof T] !== typeof pattern[key as keyof T]) + return false } return true } -export { - deepPatternMatch -} +export { deepPatternMatch } diff --git a/src/packages/core/src/functions/deep/index.ts b/src/core/functions/deep/index.ts similarity index 99% rename from src/packages/core/src/functions/deep/index.ts rename to src/core/functions/deep/index.ts index d35381a..d1bcb9c 100644 --- a/src/packages/core/src/functions/deep/index.ts +++ b/src/core/functions/deep/index.ts @@ -12,7 +12,6 @@ import { deepEqualArrays, deepEqual } from './deepEqual' import { deepMerge } from './deepMerge' import { deepPatternMatch } from './deepPatternMatch' - export { deepCopy, deepDelta, diff --git a/src/packages/core/src/functions/deep/patchCollectionPolicies.ts b/src/core/functions/deep/patchCollectionPolicies.ts similarity index 81% rename from src/packages/core/src/functions/deep/patchCollectionPolicies.ts rename to src/core/functions/deep/patchCollectionPolicies.ts index 4231389..7c3ecbc 100644 --- a/src/packages/core/src/functions/deep/patchCollectionPolicies.ts +++ b/src/core/functions/deep/patchCollectionPolicies.ts @@ -9,10 +9,10 @@ import type { ArrayPolicy } from './deepDelta' * @param fallback - Synthetic identity when `id` is absent. * @returns String identity used for map lookups. */ -function preferIdElse( +const preferIdElse = ( item: Record, - fallback: (item: Record) => string -): string { + fallback: (item: Record) => string, +): string => { const id = item.id if (id !== null && id !== undefined && String(id).length > 0) @@ -24,15 +24,14 @@ function preferIdElse( /** * Builds a synthetic identity for entity-scope rows without a server `id`. * - * Combines `entityId`, `entityType`, and `scope` so duplicate scopes on the same - * entity remain distinguishable during {@link deepDelta} array diffing. + * Combines `entityId` and `entityType` so one scope row exists per entity+type + * during {@link deepDelta} array diffing (scope is updated in place). */ -function entityScopeFallback(item: Record): string { +const entityScopeFallback = (item: Record): string => { const entityId = item.entityId ?? '' const entityType = item.entityType ?? '' - const scope = item.scope ?? '' - return `${entityId}-${entityType}-${scope}` + return `${entityId}-${entityType}` } /** @@ -47,7 +46,6 @@ export const HOSTNAMES_ARRAY_POLICY: ArrayPolicy = { } /** - * * Uses `_deltaId` in the delta payload when items lack a server `id`, avoiding * invalid Guid values in the `id` field expected by the API. */ @@ -61,7 +59,7 @@ export const ENTITY_SCOPES_ARRAY_POLICY: ArrayPolicy = { * * Falls back to the `version` field, or an empty string when neither is present. */ -function versionFallback(item: Record): string { +const versionFallback = (item: Record): string => { const version = item.version if (version !== null && version !== undefined && String(version).length > 0) diff --git a/src/packages/core/src/functions/enum/enumToArr.ts b/src/core/functions/enum/enumToArr.ts similarity index 100% rename from src/packages/core/src/functions/enum/enumToArr.ts rename to src/core/functions/enum/enumToArr.ts diff --git a/src/packages/core/src/functions/enum/enumToObj.ts b/src/core/functions/enum/enumToObj.ts similarity index 100% rename from src/packages/core/src/functions/enum/enumToObj.ts rename to src/core/functions/enum/enumToObj.ts diff --git a/src/packages/core/src/functions/enum/enumToString.ts b/src/core/functions/enum/enumToString.ts similarity index 100% rename from src/packages/core/src/functions/enum/enumToString.ts rename to src/core/functions/enum/enumToString.ts diff --git a/src/packages/core/src/functions/enum/flagsToString.ts b/src/core/functions/enum/flagsToString.ts similarity index 100% rename from src/packages/core/src/functions/enum/flagsToString.ts rename to src/core/functions/enum/flagsToString.ts diff --git a/src/packages/core/src/functions/enum/hasAnyFlag.ts b/src/core/functions/enum/hasAnyFlag.ts similarity index 100% rename from src/packages/core/src/functions/enum/hasAnyFlag.ts rename to src/core/functions/enum/hasAnyFlag.ts diff --git a/src/packages/core/src/functions/enum/hasFlag.ts b/src/core/functions/enum/hasFlag.ts similarity index 100% rename from src/packages/core/src/functions/enum/hasFlag.ts rename to src/core/functions/enum/hasFlag.ts diff --git a/src/packages/core/src/functions/enum/index.ts b/src/core/functions/enum/index.ts similarity index 100% rename from src/packages/core/src/functions/enum/index.ts rename to src/core/functions/enum/index.ts diff --git a/src/packages/core/src/functions/enum/toggleFlag.ts b/src/core/functions/enum/toggleFlag.ts similarity index 100% rename from src/packages/core/src/functions/enum/toggleFlag.ts rename to src/core/functions/enum/toggleFlag.ts diff --git a/src/packages/core/src/functions/file/index.ts b/src/core/functions/file/index.ts similarity index 100% rename from src/packages/core/src/functions/file/index.ts rename to src/core/functions/file/index.ts diff --git a/src/packages/core/src/functions/file/saveBinaryToDisk.ts b/src/core/functions/file/saveBinaryToDisk.ts similarity index 100% rename from src/packages/core/src/functions/file/saveBinaryToDisk.ts rename to src/core/functions/file/saveBinaryToDisk.ts diff --git a/src/packages/core/src/functions/guid/index.ts b/src/core/functions/guid/index.ts similarity index 100% rename from src/packages/core/src/functions/guid/index.ts rename to src/core/functions/guid/index.ts diff --git a/src/packages/core/src/functions/guid/isGuid.ts b/src/core/functions/guid/isGuid.ts similarity index 100% rename from src/packages/core/src/functions/guid/isGuid.ts rename to src/core/functions/guid/isGuid.ts diff --git a/src/packages/core/src/functions/headers/extractFilenameFromHeaders.ts b/src/core/functions/headers/extractFilenameFromHeaders.ts similarity index 100% rename from src/packages/core/src/functions/headers/extractFilenameFromHeaders.ts rename to src/core/functions/headers/extractFilenameFromHeaders.ts diff --git a/src/packages/core/src/functions/headers/index.ts b/src/core/functions/headers/index.ts similarity index 100% rename from src/packages/core/src/functions/headers/index.ts rename to src/core/functions/headers/index.ts diff --git a/src/packages/core/src/functions/index.ts b/src/core/functions/index.ts similarity index 100% rename from src/packages/core/src/functions/index.ts rename to src/core/functions/index.ts diff --git a/src/packages/core/src/functions/zod/applyFormBulkChange.ts b/src/core/functions/zod/applyFormBulkChange.ts similarity index 100% rename from src/packages/core/src/functions/zod/applyFormBulkChange.ts rename to src/core/functions/zod/applyFormBulkChange.ts diff --git a/src/packages/core/src/functions/zod/applyFormFieldChange.ts b/src/core/functions/zod/applyFormFieldChange.ts similarity index 100% rename from src/packages/core/src/functions/zod/applyFormFieldChange.ts rename to src/core/functions/zod/applyFormFieldChange.ts diff --git a/src/packages/core/src/functions/zod/createFormBulkUpdater.ts b/src/core/functions/zod/createFormBulkUpdater.ts similarity index 100% rename from src/packages/core/src/functions/zod/createFormBulkUpdater.ts rename to src/core/functions/zod/createFormBulkUpdater.ts diff --git a/src/packages/core/src/functions/zod/createFormFieldUpdater.ts b/src/core/functions/zod/createFormFieldUpdater.ts similarity index 100% rename from src/packages/core/src/functions/zod/createFormFieldUpdater.ts rename to src/core/functions/zod/createFormFieldUpdater.ts diff --git a/src/packages/core/src/functions/zod/emptyFormErrors.ts b/src/core/functions/zod/emptyFormErrors.ts similarity index 100% rename from src/packages/core/src/functions/zod/emptyFormErrors.ts rename to src/core/functions/zod/emptyFormErrors.ts diff --git a/src/packages/core/src/functions/zod/flattenFormValidationIssues.ts b/src/core/functions/zod/flattenFormValidationIssues.ts similarity index 100% rename from src/packages/core/src/functions/zod/flattenFormValidationIssues.ts rename to src/core/functions/zod/flattenFormValidationIssues.ts diff --git a/src/packages/core/src/functions/zod/formPath.ts b/src/core/functions/zod/formPath.ts similarity index 100% rename from src/packages/core/src/functions/zod/formPath.ts rename to src/core/functions/zod/formPath.ts diff --git a/src/packages/core/src/functions/zod/index.ts b/src/core/functions/zod/index.ts similarity index 100% rename from src/packages/core/src/functions/zod/index.ts rename to src/core/functions/zod/index.ts diff --git a/src/packages/core/src/functions/zod/validateFormState.ts b/src/core/functions/zod/validateFormState.ts similarity index 100% rename from src/packages/core/src/functions/zod/validateFormState.ts rename to src/core/functions/zod/validateFormState.ts diff --git a/src/core/hooks/useFormState.ts b/src/core/hooks/useFormState.ts new file mode 100644 index 0000000..b749e1a --- /dev/null +++ b/src/core/hooks/useFormState.ts @@ -0,0 +1,45 @@ +import { useCallback, useMemo, useState } from 'react' +import { applyFormBulkChange, applyFormFieldChange, validateFormState } from '../functions/zod' +import type { FormPath } from '../functions/zod/formPath' +import type { FormValidationSchema } from '../types/FormValidationSchema' + +export interface UseFormStateProps> { + initialState: FormState + validationSchema: FormValidationSchema +} + +const useFormState = >(props: UseFormStateProps) => { + const { initialState, validationSchema } = props + + const [formState, setFormState] = useState(initialState) + + const memoizedValidationSchema = useMemo(() => validationSchema, [validationSchema]) + + const { errors, formIsValid } = useMemo( + () => validateFormState(formState, memoizedValidationSchema), + [formState, memoizedValidationSchema], + ) + + const handleInputChange = useCallback((key: FormPath, value: unknown) => { + setFormState((prev) => applyFormFieldChange(prev, key, value)) + }, []) + + const setInitialState = useCallback((newInitialState: T) => { + setFormState(newInitialState) + }, []) + + const setBulkState = useCallback((updater: (prev: T) => Partial) => { + setFormState((prev) => applyFormBulkChange(prev, updater)) + }, []) + + return { + formState, + errors, + formIsValid, + handleInputChange, + setInitialState, + setBulkState, + } +} + +export { useFormState } diff --git a/src/packages/core/src/hooks/useFormState.tsx b/src/core/hooks/useFormState.tsx similarity index 100% rename from src/packages/core/src/hooks/useFormState.tsx rename to src/core/hooks/useFormState.tsx diff --git a/src/packages/core/src/http/authInterceptors.ts b/src/core/http/authInterceptors.ts similarity index 100% rename from src/packages/core/src/http/authInterceptors.ts rename to src/core/http/authInterceptors.ts diff --git a/src/packages/core/src/http/config.ts b/src/core/http/config.ts similarity index 100% rename from src/packages/core/src/http/config.ts rename to src/core/http/config.ts diff --git a/src/packages/core/src/http/createWebUiHttpClient.ts b/src/core/http/createWebUiHttpClient.ts similarity index 100% rename from src/packages/core/src/http/createWebUiHttpClient.ts rename to src/core/http/createWebUiHttpClient.ts diff --git a/src/packages/core/src/http/errorHandler.ts b/src/core/http/errorHandler.ts similarity index 91% rename from src/packages/core/src/http/errorHandler.ts rename to src/core/http/errorHandler.ts index 1cb3535..a66c7ae 100644 --- a/src/packages/core/src/http/errorHandler.ts +++ b/src/core/http/errorHandler.ts @@ -1,5 +1,5 @@ import type { AxiosError } from 'axios' -import type { ProblemDetails } from '@maks-it.com/webui-contracts' +import type { ProblemDetails } from '@webui/contracts' import { formatProblemDetailsMessage } from './problemDetails' /** Shows toast(s) for problem+json and 401 responses. */ diff --git a/src/packages/core/src/http/formData.ts b/src/core/http/formData.ts similarity index 100% rename from src/packages/core/src/http/formData.ts rename to src/core/http/formData.ts diff --git a/src/packages/core/src/http/index.ts b/src/core/http/index.ts similarity index 100% rename from src/packages/core/src/http/index.ts rename to src/core/http/index.ts diff --git a/src/packages/core/src/http/problemDetails.ts b/src/core/http/problemDetails.ts similarity index 87% rename from src/packages/core/src/http/problemDetails.ts rename to src/core/http/problemDetails.ts index 64ca814..fb114b0 100644 --- a/src/packages/core/src/http/problemDetails.ts +++ b/src/core/http/problemDetails.ts @@ -1,4 +1,4 @@ -import type { ProblemDetails } from '@maks-it.com/webui-contracts' +import type { ProblemDetails } from '@webui/contracts' /** Builds a user-facing message from RFC 7807 problem details. */ export function formatProblemDetailsMessage(problem: ProblemDetails): string { diff --git a/src/packages/core/src/http/types.ts b/src/core/http/types.ts similarity index 100% rename from src/packages/core/src/http/types.ts rename to src/core/http/types.ts diff --git a/src/packages/core/src/index.ts b/src/core/index.ts similarity index 100% rename from src/packages/core/src/index.ts rename to src/core/index.ts diff --git a/src/packages/core/src/localStorage/identity.ts b/src/core/localStorage/identity.ts similarity index 85% rename from src/packages/core/src/localStorage/identity.ts rename to src/core/localStorage/identity.ts index 23dd882..09d3959 100644 --- a/src/packages/core/src/localStorage/identity.ts +++ b/src/core/localStorage/identity.ts @@ -1,4 +1,4 @@ -import type { LoginResponse } from '@maks-it.com/webui-contracts' +import type { LoginResponse } from '@webui/contracts' const readIdentity = () => { const json = localStorage.getItem('identity') diff --git a/src/core/oauth/idpOAuth.ts b/src/core/oauth/idpOAuth.ts new file mode 100644 index 0000000..5321f6e --- /dev/null +++ b/src/core/oauth/idpOAuth.ts @@ -0,0 +1,46 @@ +import { + IDP_PROVIDER_STORAGE_KEY, + LoginProviderExternal, + MAILBOX_OAUTH_QUERY_FLAG, + toAuthProviderName, +} from '@webui/contracts' + +export interface StartIdpRedirectOptions { + /** Absolute redirect route, e.g. `https://api.example.com/Account/Idp/Redirect`. */ + redirectRoute: string + /** Post-login landing URL; defaults to `${window.location.origin}/`. */ + postLoginUrl?: string +} + +const startIdpRedirect = (provider: LoginProviderExternal, options: StartIdpRedirectOptions) => { + const providerName = toAuthProviderName(provider) + + try { + sessionStorage.setItem(IDP_PROVIDER_STORAGE_KEY, providerName) + } + catch { + /* ignore */ + } + + const url = new URL(options.redirectRoute) + url.searchParams.set('provider', providerName) + url.searchParams.set('postLogin', options.postLoginUrl ?? `${window.location.origin}/`) + + window.location.href = url.toString() +} + +const isMailboxOAuthReturn = (params: URLSearchParams = new URLSearchParams(window.location.search)) => + params.get(MAILBOX_OAUTH_QUERY_FLAG) === '1' + +/** + * Returns true when a `?code=` on the current URL must not be consumed as app login OAuth + * (mailbox OAuth popup or embedded opener). + */ +const shouldSkipIdpLoginCallback = (params: URLSearchParams = new URLSearchParams(window.location.search)) => + Boolean(window.opener) || isMailboxOAuthReturn(params) + +export { + startIdpRedirect, + isMailboxOAuthReturn, + shouldSkipIdpLoginCallback, +} diff --git a/src/core/oauth/index.ts b/src/core/oauth/index.ts new file mode 100644 index 0000000..e804cfd --- /dev/null +++ b/src/core/oauth/index.ts @@ -0,0 +1,33 @@ +export { + removeParamFromUrl, + removeParamsFromUrl, +} from './urlUtils' + +export { + startIdpRedirect, + isMailboxOAuthReturn, + shouldSkipIdpLoginCallback, + type StartIdpRedirectOptions, +} from './idpOAuth' + +export { + mailboxOAuthRedirectHost, + collectMailboxOAuthProtocols, + saveMailboxOAuthPending, + readMailboxOAuthPending, + clearMailboxOAuthPending, + startMailboxOAuthRedirect, + type MailboxOAuthEntity, + type StartMailboxOAuthRedirectOptions, +} from './mailboxOAuth' + +export { + useIdpLoginCallback, + type UseIdpLoginCallbackOptions, +} from './useIdpLoginCallback' + +export { + useMailboxOAuthReturn, + type MailboxOAuthApi, + type UseMailboxOAuthReturnOptions, +} from './useMailboxOAuthReturn' diff --git a/src/core/oauth/mailboxOAuth.ts b/src/core/oauth/mailboxOAuth.ts new file mode 100644 index 0000000..fdf4416 --- /dev/null +++ b/src/core/oauth/mailboxOAuth.ts @@ -0,0 +1,98 @@ +import { + EmailProtocol, + MAILBOX_OAUTH_QUERY_FLAG, + MAILBOX_OAUTH_STORAGE_KEY, + ServerAuthenticationMethod, + type MailboxOAuthPending, +} from '@webui/contracts' + +export interface MailboxOAuthEntity { + popAuthenticationMethod?: ServerAuthenticationMethod | null + smtpAuthenticationMethod?: ServerAuthenticationMethod | null + imapAuthenticationMethod?: ServerAuthenticationMethod | null +} + +const mailboxOAuthRedirectHost = () => + `${window.location.origin}${window.location.pathname}?${MAILBOX_OAUTH_QUERY_FLAG}=1` + +const collectMailboxOAuthProtocols = (data: MailboxOAuthEntity): EmailProtocol[] => { + const protocols: EmailProtocol[] = [] + + if (data.popAuthenticationMethod === ServerAuthenticationMethod.OAuth2) + protocols.push(EmailProtocol.Pop3) + + if (data.smtpAuthenticationMethod === ServerAuthenticationMethod.OAuth2) + protocols.push(EmailProtocol.Smtp) + + if (data.imapAuthenticationMethod === ServerAuthenticationMethod.OAuth2) + protocols.push(EmailProtocol.Imap) + + return protocols +} + +const saveMailboxOAuthPending = (pending: MailboxOAuthPending) => { + try { + sessionStorage.setItem(MAILBOX_OAUTH_STORAGE_KEY, JSON.stringify(pending)) + } + catch { + /* ignore */ + } +} + +const readMailboxOAuthPending = (): MailboxOAuthPending | undefined => { + const raw = sessionStorage.getItem(MAILBOX_OAUTH_STORAGE_KEY) + if (!raw) + return undefined + + try { + return JSON.parse(raw) as MailboxOAuthPending + } + catch { + return undefined + } +} + +const clearMailboxOAuthPending = () => { + try { + sessionStorage.removeItem(MAILBOX_OAUTH_STORAGE_KEY) + } + catch { + /* ignore */ + } +} + +const getHashReturnPath = () => window.location.hash.replace(/^#/, '') || '/' + +export interface StartMailboxOAuthRedirectOptions { + mailboxId: number + protocols: EmailProtocol[] + /** Resolves the provider login URL for the given mailbox and redirect host. */ + getOAuth2LoginUrl: (mailboxId: number, redirectHost: string) => Promise + returnPath?: string +} + +const startMailboxOAuthRedirect = async (options: StartMailboxOAuthRedirectOptions) => { + const pending: MailboxOAuthPending = { + mailboxId: options.mailboxId, + protocols: options.protocols, + returnPath: options.returnPath ?? getHashReturnPath(), + } + + saveMailboxOAuthPending(pending) + + const loginUrl = await options.getOAuth2LoginUrl( + options.mailboxId, + mailboxOAuthRedirectHost(), + ) + + window.location.href = loginUrl +} + +export { + mailboxOAuthRedirectHost, + collectMailboxOAuthProtocols, + saveMailboxOAuthPending, + readMailboxOAuthPending, + clearMailboxOAuthPending, + startMailboxOAuthRedirect, +} diff --git a/src/core/oauth/urlUtils.ts b/src/core/oauth/urlUtils.ts new file mode 100644 index 0000000..2fc47e0 --- /dev/null +++ b/src/core/oauth/urlUtils.ts @@ -0,0 +1,18 @@ +const removeParamFromUrl = (param: string) => { + const url = new URL(window.location.href) + url.searchParams.delete(param) + window.history.replaceState({}, document.title, url.toString()) +} + +const removeParamsFromUrl = (params: string[]) => { + const url = new URL(window.location.href) + for (const param of params) + url.searchParams.delete(param) + + window.history.replaceState({}, document.title, url.toString()) +} + +export { + removeParamFromUrl, + removeParamsFromUrl, +} diff --git a/src/core/oauth/useIdpLoginCallback.ts b/src/core/oauth/useIdpLoginCallback.ts new file mode 100644 index 0000000..1a28b05 --- /dev/null +++ b/src/core/oauth/useIdpLoginCallback.ts @@ -0,0 +1,65 @@ +import { useEffect } from 'react' +import type { LoginExternalExchangeResponse } from '@webui/contracts' +import { removeParamFromUrl } from './urlUtils' +import { shouldSkipIdpLoginCallback } from './idpOAuth' + +export interface UseIdpLoginCallbackOptions { + /** + * Resolves context needed before redeem (e.g. tenant id for a product BFF). + * Identity Hub `POST login-external/redeem` only requires `{ code }` — hosts may ignore extra args. + */ + getTenantId: () => string | undefined + redeemLogin: (code: string, tenantId: string) => Promise + onSuccess: (result: LoginExternalExchangeResponse) => void | Promise + onError: (error: unknown) => void + onTenantMissing?: () => void + maxWaitMs?: number + pollIntervalMs?: number +} + +const useIdpLoginCallback = ({ + getTenantId, + redeemLogin, + onSuccess, + onError, + onTenantMissing, + maxWaitMs = 5000, + pollIntervalMs = 250, +}: UseIdpLoginCallbackOptions) => { + useEffect(() => { + const params = new URLSearchParams(window.location.search) + const code = params.get('code') + if (!code || shouldSkipIdpLoginCallback(params)) + return + + let waitedMs = 0 + const intervalId = setInterval(() => { + const tenantId = getTenantId() + + if (tenantId) { + clearInterval(intervalId) + + redeemLogin(code, tenantId) + .then(async (result) => { + removeParamFromUrl('code') + await onSuccess(result) + }) + .catch((err) => { + removeParamFromUrl('code') + onError(err) + }) + } + else { + waitedMs += pollIntervalMs + if (waitedMs >= maxWaitMs) { + clearInterval(intervalId) + onTenantMissing?.() + } + } + }, pollIntervalMs) + + return () => clearInterval(intervalId) + }, [getTenantId, redeemLogin, onSuccess, onError, onTenantMissing, maxWaitMs, pollIntervalMs]) +} + +export { useIdpLoginCallback } diff --git a/src/core/oauth/useMailboxOAuthReturn.ts b/src/core/oauth/useMailboxOAuthReturn.ts new file mode 100644 index 0000000..f8c60f1 --- /dev/null +++ b/src/core/oauth/useMailboxOAuthReturn.ts @@ -0,0 +1,167 @@ +import { useEffect, useRef } from 'react' +import { + EmailProtocol, + MAILBOX_OAUTH_PROCESSING_KEY_PREFIX, + MAILBOX_OAUTH_QUERY_FLAG, + MAILBOX_OAUTH_STORAGE_KEY, + type MailboxOAuthPending, +} from '@webui/contracts' +import { + clearMailboxOAuthPending, + mailboxOAuthRedirectHost, + saveMailboxOAuthPending, +} from './mailboxOAuth' +import { removeParamsFromUrl } from './urlUtils' + +const MAILBOX_OAUTH_CLEAN_PARAMS = ['code', MAILBOX_OAUTH_QUERY_FLAG, 'error', 'error_description'] as const + +export interface MailboxOAuthApi { + getOAuth2LoginUrl: (mailboxId: number, redirectHost: string) => Promise + redeemMsal: (mailboxId: number, code: string, emailProtocol: EmailProtocol) => Promise +} + +export interface UseMailboxOAuthReturnOptions { + api: MailboxOAuthApi + isAuthenticated: () => boolean + onSuccess?: (message?: string) => void + onError?: (message: string) => void + successMessage?: string + errorMessage?: string +} + +const useMailboxOAuthReturn = ({ + api, + isAuthenticated, + onSuccess, + onError, + successMessage = 'Mailbox OAuth2 authentication completed.', + errorMessage = 'Mailbox OAuth2 authentication failed.', +}: UseMailboxOAuthReturnOptions) => { + const handled = useRef(false) + + useEffect(() => { + const params = new URLSearchParams(window.location.search) + if (params.get(MAILBOX_OAUTH_QUERY_FLAG) !== '1' || handled.current) + return + + handled.current = true + + const cleanParams = () => removeParamsFromUrl([...MAILBOX_OAUTH_CLEAN_PARAMS]) + + const goBack = (returnPath: string) => { + clearMailboxOAuthPending() + cleanParams() + + const path = returnPath.startsWith('/') + ? returnPath + : `/${returnPath}` + + window.location.replace(`${window.location.origin}${window.location.pathname}#${path}`) + } + + const oauthError = params.get('error') + if (oauthError) { + const pendingRaw = sessionStorage.getItem(MAILBOX_OAUTH_STORAGE_KEY) + + clearMailboxOAuthPending() + cleanParams() + + onError?.(decodeURIComponent((params.get('error_description') ?? oauthError).replace(/\+/g, ' '))) + + if (pendingRaw) { + try { + goBack((JSON.parse(pendingRaw) as MailboxOAuthPending).returnPath ?? '/') + } + catch { + goBack('/') + } + } + + return + } + + const code = params.get('code') + if (!code || !isAuthenticated()) + return + + const processingKey = `${MAILBOX_OAUTH_PROCESSING_KEY_PREFIX}${code}` + + if (sessionStorage.getItem(processingKey)) + return + + try { + sessionStorage.setItem(processingKey, '1') + } + catch { + /* ignore */ + } + + const raw = sessionStorage.getItem(MAILBOX_OAUTH_STORAGE_KEY) + if (!raw) { + try { + sessionStorage.removeItem(processingKey) + } + catch { + /* ignore */ + } + + cleanParams() + return + } + + let pending: MailboxOAuthPending + try { + pending = JSON.parse(raw) as MailboxOAuthPending + } + catch { + try { + sessionStorage.removeItem(processingKey) + sessionStorage.removeItem(MAILBOX_OAUTH_STORAGE_KEY) + } + catch { + /* ignore */ + } + + cleanParams() + return + } + + const protocol = pending.protocols[0] + const remaining = pending.protocols.slice(1) + + api.redeemMsal(pending.mailboxId, code, protocol) + .then(() => { + cleanParams() + + if (remaining.length > 0) { + const nextPending: MailboxOAuthPending = { ...pending, protocols: remaining } + saveMailboxOAuthPending(nextPending) + + return api + .getOAuth2LoginUrl(pending.mailboxId, mailboxOAuthRedirectHost()) + .then((loginUrl) => { + window.location.href = loginUrl + }) + } + + clearMailboxOAuthPending() + onSuccess?.(successMessage) + goBack(pending.returnPath) + }) + .catch(() => { + try { + sessionStorage.removeItem(processingKey) + sessionStorage.removeItem(MAILBOX_OAUTH_STORAGE_KEY) + } + catch { + /* ignore */ + } + + cleanParams() + onError?.(errorMessage) + goBack(pending.returnPath) + }) + }, [api, isAuthenticated, onSuccess, onError, successMessage, errorMessage]) +} + +export { useMailboxOAuthReturn } diff --git a/src/packages/core/src/signalr/index.ts b/src/core/signalr/index.ts similarity index 100% rename from src/packages/core/src/signalr/index.ts rename to src/core/signalr/index.ts diff --git a/src/packages/core/src/signalr/useWebUiHub.ts b/src/core/signalr/useWebUiHub.ts similarity index 100% rename from src/packages/core/src/signalr/useWebUiHub.ts rename to src/core/signalr/useWebUiHub.ts diff --git a/src/packages/core/src/types/FormValidationSchema.ts b/src/core/types/FormValidationSchema.ts similarity index 100% rename from src/packages/core/src/types/FormValidationSchema.ts rename to src/core/types/FormValidationSchema.ts diff --git a/src/core/types/ScopePermissions.ts b/src/core/types/ScopePermissions.ts new file mode 100644 index 0000000..15be419 --- /dev/null +++ b/src/core/types/ScopePermissions.ts @@ -0,0 +1,11 @@ +/** + * Scope permission bit flags (numeric JSON). + * Must match product `*.Engine.ScopePermission` (e.g. MaksIT.CertsUI.Engine) — not MaksIT.Core. + */ +export enum ScopePermission { + None = 0, + Read = 1 << 0, + Write = 1 << 1, + Delete = 1 << 2, + Create = 1 << 3, +} diff --git a/src/packages/core/src/types/index.ts b/src/core/types/index.ts similarity index 72% rename from src/packages/core/src/types/index.ts rename to src/core/types/index.ts index c298191..18270d9 100644 --- a/src/packages/core/src/types/index.ts +++ b/src/core/types/index.ts @@ -3,5 +3,5 @@ export type { FormValidationResult, FormValidationSchema, } from './FormValidationSchema' -export type { PagedResponse } from '@maks-it.com/webui-contracts' +export type { PagedResponse } from '@webui/contracts' export { ScopePermission } from './ScopePermissions' diff --git a/src/coverage/coverage-summary.json b/src/coverage/coverage-summary.json deleted file mode 100644 index 8bf01c3..0000000 --- a/src/coverage/coverage-summary.json +++ /dev/null @@ -1,35 +0,0 @@ -{"total": {"lines":{"total":535,"covered":440,"skipped":0,"pct":82.24},"statements":{"total":572,"covered":471,"skipped":0,"pct":82.34},"functions":{"total":85,"covered":53,"skipped":0,"pct":62.35},"branches":{"total":309,"covered":241,"skipped":0,"pct":77.99},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\PagedRequest.ts": {"lines":{"total":3,"covered":3,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":3,"covered":3,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\PatchOperation.ts": {"lines":{"total":6,"covered":6,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":6,"covered":6,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\PatchRequestModelBase.ts": {"lines":{"total":4,"covered":4,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":4,"covered":4,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\RequestModelBase.ts": {"lines":{"total":2,"covered":2,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":2,"covered":2,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\index.ts": {"lines":{"total":11,"covered":11,"skipped":0,"pct":100},"functions":{"total":9,"covered":2,"skipped":0,"pct":22.22},"statements":{"total":18,"covered":18,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\identity\\Claims.ts": {"lines":{"total":56,"covered":56,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":56,"covered":56,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\identity\\login\\LoginRequest.ts": {"lines":{"total":9,"covered":9,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":9,"covered":9,"skipped":0,"pct":100},"branches":{"total":8,"covered":8,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\identity\\login\\RefreshTokenRequest.ts": {"lines":{"total":2,"covered":2,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":2,"covered":2,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\identity\\logout\\LogoutRequest.ts": {"lines":{"total":2,"covered":2,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":2,"covered":2,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\oauth\\EmailProtocol.ts": {"lines":{"total":4,"covered":4,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":4,"covered":4,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\oauth\\LoginProviderExternal.ts": {"lines":{"total":11,"covered":5,"skipped":0,"pct":45.45},"functions":{"total":3,"covered":1,"skipped":0,"pct":33.33},"statements":{"total":14,"covered":7,"skipped":0,"pct":50},"branches":{"total":8,"covered":2,"skipped":0,"pct":25}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\oauth\\ServerAuthenticationMethod.ts": {"lines":{"total":4,"covered":4,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":4,"covered":4,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\oauth\\index.ts": {"lines":{"total":11,"covered":11,"skipped":0,"pct":100},"functions":{"total":9,"covered":0,"skipped":0,"pct":0},"statements":{"total":13,"covered":13,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\oauth\\oauthConstants.ts": {"lines":{"total":4,"covered":4,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":4,"covered":4,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\acl\\parseAclEntry.ts": {"lines":{"total":17,"covered":17,"skipped":0,"pct":100},"functions":{"total":4,"covered":4,"skipped":0,"pct":100},"statements":{"total":18,"covered":18,"skipped":0,"pct":100},"branches":{"total":6,"covered":6,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\dataTable\\dataTableFilters.ts": {"lines":{"total":5,"covered":5,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":6,"covered":6,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\dataTable\\dataTablePaged.ts": {"lines":{"total":4,"covered":4,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":4,"covered":4,"skipped":0,"pct":100},"branches":{"total":16,"covered":16,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\date\\isValidDateString.ts": {"lines":{"total":6,"covered":6,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":7,"covered":7,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\deepCopy.ts": {"lines":{"total":17,"covered":16,"skipped":0,"pct":94.11},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":17,"covered":16,"skipped":0,"pct":94.11},"branches":{"total":12,"covered":11,"skipped":0,"pct":91.66}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\deepDelta.ts": {"lines":{"total":185,"covered":153,"skipped":0,"pct":82.7},"functions":{"total":18,"covered":15,"skipped":0,"pct":83.33},"statements":{"total":200,"covered":164,"skipped":0,"pct":82},"branches":{"total":139,"covered":116,"skipped":0,"pct":83.45}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\deepEqual.ts": {"lines":{"total":36,"covered":35,"skipped":0,"pct":97.22},"functions":{"total":3,"covered":3,"skipped":0,"pct":100},"statements":{"total":38,"covered":37,"skipped":0,"pct":97.36},"branches":{"total":26,"covered":25,"skipped":0,"pct":96.15}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\patchCollectionPolicies.ts": {"lines":{"total":17,"covered":15,"skipped":0,"pct":88.23},"functions":{"total":5,"covered":5,"skipped":0,"pct":100},"statements":{"total":17,"covered":15,"skipped":0,"pct":88.23},"branches":{"total":16,"covered":10,"skipped":0,"pct":62.5}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\enum\\hasAnyFlag.ts": {"lines":{"total":3,"covered":3,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":3,"covered":3,"skipped":0,"pct":100},"branches":{"total":1,"covered":0,"skipped":0,"pct":0}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\enum\\hasFlag.ts": {"lines":{"total":3,"covered":3,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":3,"covered":3,"skipped":0,"pct":100},"branches":{"total":1,"covered":0,"skipped":0,"pct":0}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\enum\\toggleFlag.ts": {"lines":{"total":3,"covered":3,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":3,"covered":3,"skipped":0,"pct":100},"branches":{"total":3,"covered":2,"skipped":0,"pct":66.66}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\guid\\isGuid.ts": {"lines":{"total":4,"covered":4,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":4,"covered":4,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\headers\\extractFilenameFromHeaders.ts": {"lines":{"total":17,"covered":15,"skipped":0,"pct":88.23},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":17,"covered":15,"skipped":0,"pct":88.23},"branches":{"total":15,"covered":14,"skipped":0,"pct":93.33}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\zod\\emptyFormErrors.ts": {"lines":{"total":3,"covered":3,"skipped":0,"pct":100},"functions":{"total":2,"covered":2,"skipped":0,"pct":100},"statements":{"total":4,"covered":4,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\zod\\flattenFormValidationIssues.ts": {"lines":{"total":7,"covered":7,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":7,"covered":7,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\zod\\validateFormState.ts": {"lines":{"total":7,"covered":7,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":7,"covered":7,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\http\\problemDetails.ts": {"lines":{"total":5,"covered":5,"skipped":0,"pct":100},"functions":{"total":3,"covered":3,"skipped":0,"pct":100},"statements":{"total":6,"covered":6,"skipped":0,"pct":100},"branches":{"total":9,"covered":8,"skipped":0,"pct":88.88}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\signalr\\useWebUiHub.ts": {"lines":{"total":61,"covered":10,"skipped":0,"pct":16.39},"functions":{"total":12,"covered":1,"skipped":0,"pct":8.33},"statements":{"total":64,"covered":12,"skipped":0,"pct":18.75},"branches":{"total":33,"covered":7,"skipped":0,"pct":21.21}} -,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\types\\ScopePermissions.ts": {"lines":{"total":6,"covered":6,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":6,"covered":6,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}} -} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..0fec00b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export * from './contracts' +export * from './core' +export * from './components' diff --git a/src/jest.config.cjs b/src/jest.config.cjs index 67bab30..5f4824e 100644 --- a/src/jest.config.cjs +++ b/src/jest.config.cjs @@ -2,23 +2,28 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - roots: ['/packages/core/test', '/packages/contracts/test'], + roots: ['/test'], testMatch: ['**/*.test.ts'], collectCoverageFrom: [ - 'packages/core/src/**/*.ts', - 'packages/contracts/src/**/*.ts', + 'contracts/**/*.ts', + 'core/**/*.ts', ], coverageDirectory: 'coverage', coverageReporters: ['json-summary', 'text'], moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1', - '^@maks-it.com/webui-contracts$': '/packages/contracts/src/index.ts', + '^@webui/contracts$': '/contracts/index.ts', + '^@webui/contracts/(.*)$': '/contracts/$1', + '^@webui/core$': '/core/index.ts', + '^@webui/core/(.*)$': '/core/$1', + '^@webui/components$': '/components/index.ts', + '^@webui/components/(.*)$': '/components/$1', }, transform: { '^.+\\.tsx?$': [ 'ts-jest', { - tsconfig: '/tsconfig.jest.json', + tsconfig: '/tsconfig.json', }, ], }, diff --git a/src/package-lock.json b/src/package-lock.json index 91f08f1..a5f2664 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,29 +1,33 @@ { - "name": "maksit-webui", - "version": "0.3.4", + "name": "@maks-it.com/webui", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "maksit-webui", - "version": "0.3.4", + "name": "@maks-it.com/webui", + "version": "0.4.0", "license": "MIT", - "workspaces": [ - "packages/*" - ], + "dependencies": { + "date-fns": "^4.3.0" + }, "devDependencies": { + "@microsoft/signalr": "^8.0.7", "@storybook/addon-a11y": "^10.4.1", "@storybook/addon-docs": "^10.4.1", "@storybook/addon-vitest": "^10.4.1", "@storybook/react-vite": "^10.4.1", "@tailwindcss/vite": "^4.3.0", + "@tanstack/react-table": "^8.21.3", "@types/jest": "^30.0.0", "@types/mockdate": "^2.0.0", "@types/node": "^22.19.19", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", + "@types/react-virtualized": "^9.22.3", "@vitest/browser-playwright": "^4.1.7", "@vitest/coverage-v8": "^4.1.7", + "axios": "^1.16.1", "jest": "^30.4.2", "lucide-react": "^1.16.0", "mockdate": "^3.0.5", @@ -33,15 +37,29 @@ "react": "^19.2.6", "react-dom": "^19.2.6", "react-router-dom": "^7.15.1", + "react-virtualized": "^9.22.6", "storybook": "^10.4.1", "tailwindcss": "^4.3.0", "ts-jest": "^29.4.11", "tsup": "^8.5.1", + "typescript": "^6.0.3", "vite": "^6.4.2", - "vitest": "^4.1.7" + "vitest": "^4.1.7", + "zod": "^4.4.3" }, "engines": { "node": ">=20.19" + }, + "peerDependencies": { + "@microsoft/signalr": "^8.0.0", + "@tanstack/react-table": "^8.0.0", + "axios": "^1.16.0", + "lucide-react": "^1.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "react-router-dom": "^7.0.0", + "react-virtualized": "^9.22.0", + "zod": "^4.4.0" } }, "node_modules/@adobe/css-tools": { @@ -1573,18 +1591,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@maks-it.com/webui-components": { - "resolved": "packages/components", - "link": true - }, - "node_modules/@maks-it.com/webui-contracts": { - "resolved": "packages/contracts", - "link": true - }, - "node_modules/@maks-it.com/webui-core": { - "resolved": "packages/core", - "link": true - }, "node_modules/@mdx-js/react": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", @@ -3660,6 +3666,7 @@ "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3670,6 +3677,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -10545,6 +10553,7 @@ "packages/components": { "name": "@maks-it.com/webui-components", "version": "0.3.4", + "extraneous": true, "dependencies": { "@maks-it.com/webui-contracts": "^0.3.4", "@maks-it.com/webui-core": "^0.3.4" @@ -10576,6 +10585,7 @@ "packages/contracts": { "name": "@maks-it.com/webui-contracts", "version": "0.3.4", + "extraneous": true, "devDependencies": { "tsup": "^8.5.1", "typescript": "^6.0.3", @@ -10588,6 +10598,7 @@ "packages/core": { "name": "@maks-it.com/webui-core", "version": "0.3.4", + "extraneous": true, "dependencies": { "@maks-it.com/webui-contracts": "^0.3.4", "date-fns": "^4.3.0" @@ -10607,6 +10618,41 @@ "react": "^18.0.0 || ^19.0.0", "zod": "^4.4.0" } + }, + "packages/webui": { + "name": "@maks-it.com/webui", + "version": "0.4.0", + "extraneous": true, + "dependencies": { + "date-fns": "^4.3.0" + }, + "devDependencies": { + "@microsoft/signalr": "^8.0.7", + "@tanstack/react-table": "^8.21.3", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "@types/react-virtualized": "^9.22.3", + "axios": "^1.16.1", + "lucide-react": "^1.16.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-router-dom": "^7.15.1", + "react-virtualized": "^9.22.6", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@microsoft/signalr": "^8.0.0", + "@tanstack/react-table": "^8.0.0", + "axios": "^1.16.0", + "lucide-react": "^1.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "react-router-dom": "^7.0.0", + "react-virtualized": "^9.22.0", + "zod": "^4.4.0" + } } } } diff --git a/src/package.json b/src/package.json index eb5e98c..c2b4a4c 100644 --- a/src/package.json +++ b/src/package.json @@ -1,10 +1,21 @@ { - "name": "maksit-webui", - "private": true, - "version": "0.3.4", - "description": "Shared React UI library for MaksIT Certs UI and Vault WebUI", - "workspaces": [ - "packages/*" + "name": "@maks-it.com/webui", + "version": "0.4.0", + "description": "Shared contracts, utilities, and React components for MaksIT WebUI apps", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist", + "README.md" ], "engines": { "node": ">=20.19" @@ -15,30 +26,58 @@ "test-exclude": "^8.0.0" }, "scripts": { - "build": "npm run build -w @maks-it.com/webui-contracts && npm run build -w @maks-it.com/webui-core && npm run build -w @maks-it.com/webui-components", + "build": "npx tsup", "test": "jest --config jest.config.cjs", "test:coverage": "jest --config jest.config.cjs --coverage", - "typecheck": "npm run typecheck --workspaces --if-present", - "clean": "npm run clean --workspaces --if-present", + "typecheck": "tsc -p tsconfig.json --noEmit", + "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"", + "prepublishOnly": "npm run build", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build -o storybook-static", "test-storybook": "vitest --project storybook run", "test-storybook:watch": "vitest --project storybook", "test-storybook:coverage": "vitest --project storybook run --coverage" }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/MAKS-IT-COM/maksit-webui.git", + "directory": "src" + }, + "dependencies": { + "date-fns": "^4.3.0" + }, + "peerDependencies": { + "@microsoft/signalr": "^8.0.0", + "@tanstack/react-table": "^8.0.0", + "axios": "^1.16.0", + "lucide-react": "^1.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "react-router-dom": "^7.0.0", + "react-virtualized": "^9.22.0", + "zod": "^4.4.0" + }, "devDependencies": { + "@microsoft/signalr": "^8.0.7", "@storybook/addon-a11y": "^10.4.1", "@storybook/addon-docs": "^10.4.1", "@storybook/addon-vitest": "^10.4.1", "@storybook/react-vite": "^10.4.1", "@tailwindcss/vite": "^4.3.0", + "@tanstack/react-table": "^8.21.3", "@types/jest": "^30.0.0", "@types/mockdate": "^2.0.0", "@types/node": "^22.19.19", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", + "@types/react-virtualized": "^9.22.3", "@vitest/browser-playwright": "^4.1.7", "@vitest/coverage-v8": "^4.1.7", + "axios": "^1.16.1", "jest": "^30.4.2", "lucide-react": "^1.16.0", "mockdate": "^3.0.5", @@ -48,12 +87,15 @@ "react": "^19.2.6", "react-dom": "^19.2.6", "react-router-dom": "^7.15.1", + "react-virtualized": "^9.22.6", "storybook": "^10.4.1", "tailwindcss": "^4.3.0", "ts-jest": "^29.4.11", "tsup": "^8.5.1", + "typescript": "^6.0.3", "vite": "^6.4.2", - "vitest": "^4.1.7" + "vitest": "^4.1.7", + "zod": "^4.4.3" }, "author": "MaksIT", "license": "MIT", diff --git a/src/packages/components/README.md b/src/packages/components/README.md deleted file mode 100644 index 32427a0..0000000 --- a/src/packages/components/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# @maks-it.com/webui-components - -Shared React UI components for MaksIT WebUI apps: forms, DataTable, layout, editors, and list views. - -Depends on `@maks-it.com/webui-core` and `@maks-it.com/webui-contracts`. Peer dependencies must be installed in the host app. - -## Install - -```bash -npm install @maks-it.com/webui-components @maks-it.com/webui-core @maks-it.com/webui-contracts -npm install react react-dom react-router-dom lucide-react @tanstack/react-table react-virtualized zod -``` - -## Components - -| Export | Purpose | -|--------|---------| -| `DataTable`, `DataTableFilter`, `DataTableLabel` | Virtualized server-paged tables | -| `RemoteSelectBoxComponent` | Async search select (pass `searchRoute` API path) | -| `SecretComponent` | Secret field with optional generate action | -| `FormContainer`, `FormHeader`, `FormContent`, `FormFooter` | Form layout shell | -| `Layout` | App chrome / navigation wrapper | -| `Offcanvas` | Slide-over panel | -| `LazyLoadTable` | Incrementally loaded table | -| `EntityScopesSummary` | Entity scope permissions summary | -| `Toast`, `addToast` | Toast notifications | -| `FieldContainer` | Label + validation wrapper for fields | -| `ExternalLoginButtons` | Google/Microsoft (or custom) external login buttons | -| `IdpLoginCallbackHandler` | Headless handler for `?code=` app login OAuth return | -| `MailboxOAuthReturnHandler` | Headless handler for mailbox OAuth multi-protocol return | - -## Example — DataTable - -```tsx -import { DataTable, createColumns } from '@maks-it.com/webui-components' - -const columns = createColumns([ - { key: 'name', header: 'Name' }, - { key: 'createdAt', header: 'Created', label: 'date' }, -]) - - -``` - -## Example — remote select - -```tsx -import { RemoteSelectBoxComponent } from '@maks-it.com/webui-components' - - -``` - -## Repository - -[github.com/MAKS-IT-COM/maksit-webui](https://github.com/MAKS-IT-COM/maksit-webui) — `src/packages/components` - -## License - -MIT diff --git a/src/packages/components/package.json b/src/packages/components/package.json deleted file mode 100644 index e2fac76..0000000 --- a/src/packages/components/package.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "name": "@maks-it.com/webui-components", - "version": "0.3.4", - "description": "Shared React components for MaksIT WebUI apps", - "type": "module", - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" - } - }, - "files": [ - "dist", - "README.md" - ], - "scripts": { - "build": "npx tsup src/index.ts --format esm,cjs --dts --clean --tsconfig tsconfig.build.json --external react --external react-dom --external react-router-dom --external lucide-react --external @tanstack/react-table --external react-virtualized", - "typecheck": "tsc -p tsconfig.json --noEmit", - "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"", - "prepublishOnly": "npm run build" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/MAKS-IT-COM/maksit-webui.git", - "directory": "src/packages/components" - }, - "dependencies": { - "@maks-it.com/webui-contracts": "^0.3.4", - "@maks-it.com/webui-core": "^0.3.4" - }, - "peerDependencies": { - "@tanstack/react-table": "^8.0.0", - "lucide-react": "^1.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0", - "react-router-dom": "^7.0.0", - "react-virtualized": "^9.22.0", - "zod": "^4.4.0" - }, - "devDependencies": { - "@tanstack/react-table": "^8.21.3", - "@types/react": "^19.2.15", - "@types/react-dom": "^19.2.3", - "@types/react-virtualized": "^9.22.3", - "lucide-react": "^1.16.0", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "react-router-dom": "^7.15.1", - "react-virtualized": "^9.22.6", - "tsup": "^8.5.1", - "typescript": "^6.0.3", - "zod": "^4.4.3" - } -} diff --git a/src/packages/components/tsconfig.build.json b/src/packages/components/tsconfig.build.json deleted file mode 100644 index 35bcdb2..0000000 --- a/src/packages/components/tsconfig.build.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "ignoreDeprecations": "6.0" - } -} diff --git a/src/packages/components/tsconfig.json b/src/packages/components/tsconfig.json deleted file mode 100644 index 5285d28..0000000 --- a/src/packages/components/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist" - }, - "include": ["src"] -} diff --git a/src/packages/contracts/README.md b/src/packages/contracts/README.md deleted file mode 100644 index f1a089f..0000000 --- a/src/packages/contracts/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# @maks-it.com/webui-contracts - -Shared TypeScript contracts and Zod schemas for MaksIT WebUI apps (Certs UI, Vault WebUI, and related products). - -## Install - -```bash -npm install @maks-it.com/webui-contracts zod -``` - -`zod` is a peer dependency (schemas use Zod v4). - -## Contents - -| Area | Exports | -|------|---------| -| Paging | `PagedRequest`, `PagedRequestSchema`, `PagedResponse` | -| PATCH | `PatchOperation`, `PatchRequestModelBase`, `PatchRequestModelBaseSchema` | -| API errors | `ProblemDetails` | -| Search | `SearchResponseBase`, `SearchEntityScopeEntry` | -| Identity | `LoginRequest` / `LoginRequestSchema`, `LoginResponse`, `RefreshTokenRequest`, `LogoutRequest`, `Claims` | -| OAuth / IdP | `LoginProviderExternal`, `EmailProtocol`, `ServerAuthenticationMethod`, `IdpRedeemLoginResponse`, `MailboxOAuthPending`, mailbox OAuth constants | -| Misc | `TrngResponse`, `RequestModelBase`, `ResponseModelBase` | - -## Example - -```ts -import { - LoginRequestSchema, - type PagedRequest, - PatchOperation, -} from '@maks-it.com/webui-contracts' - -const login = LoginRequestSchema.parse({ username: 'admin', password: '***' }) - -const page: PagedRequest = { - pageNumber: 1, - pageSize: 25, - filters: 'Name.Contains("cert")', -} -``` - -## Repository - -[github.com/MAKS-IT-COM/maksit-webui](https://github.com/MAKS-IT-COM/maksit-webui) — `src/packages/contracts` - -## License - -MIT diff --git a/src/packages/contracts/package.json b/src/packages/contracts/package.json deleted file mode 100644 index bd5f01b..0000000 --- a/src/packages/contracts/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "@maks-it.com/webui-contracts", - "version": "0.3.4", - "description": "Shared TypeScript contracts for MaksIT WebUI apps", - "type": "module", - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" - } - }, - "files": [ - "dist", - "README.md" - ], - "scripts": { - "build": "npx tsup src/index.ts --format esm,cjs --dts --clean --tsconfig tsconfig.build.json", - "test": "jest --config ../../jest.config.cjs --testPathPatterns packages/contracts", - "typecheck": "tsc -p tsconfig.json --noEmit", - "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"", - "prepublishOnly": "npm run build" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/MAKS-IT-COM/maksit-webui.git", - "directory": "src/packages/contracts" - }, - "peerDependencies": { - "zod": "^4.4.0" - }, - "devDependencies": { - "tsup": "^8.5.1", - "typescript": "^6.0.3", - "zod": "^4.4.3" - } -} diff --git a/src/packages/contracts/src/SearchResponseBase.ts b/src/packages/contracts/src/SearchResponseBase.ts deleted file mode 100644 index c1825d8..0000000 --- a/src/packages/contracts/src/SearchResponseBase.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ResponseModelBase } from './ResponseModelBase' - -export interface SearchResponseBase extends ResponseModelBase { - id: string; - name: string; -} \ No newline at end of file diff --git a/src/packages/contracts/src/TrngResponse.ts b/src/packages/contracts/src/TrngResponse.ts deleted file mode 100644 index d8424f4..0000000 --- a/src/packages/contracts/src/TrngResponse.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ResponseModelBase } from './ResponseModelBase' - -export interface TrngResponse extends ResponseModelBase { - secret: string; -} \ No newline at end of file diff --git a/src/packages/contracts/src/identity/logout/LogoutRequest.ts b/src/packages/contracts/src/identity/logout/LogoutRequest.ts deleted file mode 100644 index 0d560b1..0000000 --- a/src/packages/contracts/src/identity/logout/LogoutRequest.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { boolean, object, string, type ZodType } from 'zod' - -export interface LogoutRequest { - logoutFromAllDevices?: boolean - /** Optional; some clients send the access token in the body for reference. */ - token?: string -} - -export const LogoutRequestSchema: ZodType = object({ - logoutFromAllDevices: boolean().optional(), - token: string().optional(), -}) diff --git a/src/packages/contracts/tsconfig.build.json b/src/packages/contracts/tsconfig.build.json deleted file mode 100644 index 35bcdb2..0000000 --- a/src/packages/contracts/tsconfig.build.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "ignoreDeprecations": "6.0" - } -} diff --git a/src/packages/contracts/tsconfig.json b/src/packages/contracts/tsconfig.json deleted file mode 100644 index 5285d28..0000000 --- a/src/packages/contracts/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist" - }, - "include": ["src"] -} diff --git a/src/packages/core/README.md b/src/packages/core/README.md deleted file mode 100644 index 56aef11..0000000 --- a/src/packages/core/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# @maks-it.com/webui-core - -Shared utilities, HTTP helpers, and React hooks for MaksIT WebUI apps. - -Depends on `@maks-it.com/webui-contracts`. Install peer dependencies in the host app: `react`, `axios`, `zod`. - -## Install - -```bash -npm install @maks-it.com/webui-core @maks-it.com/webui-contracts @microsoft/signalr axios react zod -``` - -## Highlights - -| Module | Exports | -|--------|---------| -| Deep diff | `deepDelta`, `deltaHasOperations`, collection policies | -| Deep utils | `deepCopy`, `deepEqual`, `deepMerge`, `deepPatternMatch` | -| Forms | `useFormState`, `validateFormState`, `applyFormFieldChange`, `createFormFieldUpdater` | -| DataTable | `mapPagedToDataTable`, `extractPropFilter`, `DataTablePageView` | -| ACL | `parseAclEntry`, `parseAclEntries` | -| HTTP | `createWebUiHttpClient`, auth interceptors, Problem Details helpers | -| SignalR | `useWebUiHub` | -| Enum / flags | `enumToArr`, `flagsToString`, `hasFlag`, `toggleFlag` | -| PATCH collections | `ENTITY_SCOPES_ARRAY_POLICY`, `HOSTNAMES_ARRAY_POLICY`, `VERSIONS_ARRAY_POLICY` | -| Identity storage | `readIdentity`, `writeIdentity`, `removeIdentity` | -| OAuth / IdP | `startIdpRedirect`, `useIdpLoginCallback`, `collectMailboxOAuthProtocols`, `startMailboxOAuthRedirect`, `useMailboxOAuthReturn` | - -## Example — form state - -```tsx -import { z } from 'zod' -import { useFormState } from '@maks-it.com/webui-core' - -const schema = z.object({ name: z.string().min(1) }) - -function MyForm() { - const { formState, errors, formIsValid, handleInputChange } = useFormState({ - initialState: { name: '' }, - validationSchema: schema, - }) - // ... -} -``` - -## Example — SignalR hub - -```tsx -import { readIdentity, useWebUiHub } from '@maks-it.com/webui-core' - -function JobProgressPanel({ canSubscribe }: { canSubscribe: boolean }) { - const [progress, setProgress] = useState() - - const { connectionState } = useWebUiHub({ - hubUrl: '/hubs/jobs', - enabled: canSubscribe, - accessToken: () => readIdentity()?.token, - events: { progressUpdated: setProgress }, - }) - - // connectionState: idle | connecting | connected | reconnecting | disconnected -} -``` - -## Example — PATCH delta - -```ts -import { deepDelta, deltaHasOperations } from '@maks-it.com/webui-core' - -const delta = deepDelta(formState, backupState, { arrays: { items: { identityKey: 'id' } } }) -if (deltaHasOperations(delta)) { - await api.patch('/resource', delta) -} -``` - -## Repository - -[github.com/MAKS-IT-COM/maksit-webui](https://github.com/MAKS-IT-COM/maksit-webui) — `src/packages/core` - -## License - -MIT diff --git a/src/packages/core/package.json b/src/packages/core/package.json deleted file mode 100644 index 22bdbb4..0000000 --- a/src/packages/core/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "@maks-it.com/webui-core", - "version": "0.3.4", - "description": "Shared utilities and hooks for MaksIT WebUI apps", - "type": "module", - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" - } - }, - "files": [ - "dist", - "README.md" - ], - "scripts": { - "build": "npx tsup src/index.ts --format esm,cjs --dts --clean --tsconfig tsconfig.build.json", - "test": "jest --config ../../jest.config.cjs", - "typecheck": "tsc -p tsconfig.json --noEmit", - "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"", - "prepublishOnly": "npm run build" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/MAKS-IT-COM/maksit-webui.git", - "directory": "src/packages/core" - }, - "dependencies": { - "@maks-it.com/webui-contracts": "^0.3.4", - "date-fns": "^4.3.0" - }, - "peerDependencies": { - "@microsoft/signalr": "^8.0.0", - "axios": "^1.16.0", - "react": "^18.0.0 || ^19.0.0", - "zod": "^4.4.0" - }, - "devDependencies": { - "@microsoft/signalr": "^8.0.7", - "@types/react": "^19.2.15", - "axios": "^1.16.1", - "react": "^19.2.6", - "tsup": "^8.5.1", - "typescript": "^6.0.3", - "zod": "^4.4.3" - } -} diff --git a/src/packages/core/src/types/ScopePermissions.ts b/src/packages/core/src/types/ScopePermissions.ts deleted file mode 100644 index 767eb7c..0000000 --- a/src/packages/core/src/types/ScopePermissions.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** Shared scope permission flags (numeric JSON). */ -export enum ScopePermission { - None = 0, - Read = 1 << 0, - Write = 1 << 1, - Delete = 1 << 2, - Create = 1 << 3, -} diff --git a/src/packages/core/tsconfig.build.json b/src/packages/core/tsconfig.build.json deleted file mode 100644 index 35bcdb2..0000000 --- a/src/packages/core/tsconfig.build.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "ignoreDeprecations": "6.0" - } -} diff --git a/src/packages/core/tsconfig.json b/src/packages/core/tsconfig.json deleted file mode 100644 index 5285d28..0000000 --- a/src/packages/core/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist" - }, - "include": ["src"] -} diff --git a/src/stories/README.md b/src/stories/README.md index 42eadc7..4670555 100644 --- a/src/stories/README.md +++ b/src/stories/README.md @@ -1,13 +1,13 @@ # Storybook stories -Stories are **not** published with `@maks-it.com/webui-components`. They live here so the library package stays free of Storybook-only code. +Stories are **not** published with `@maks-it.com/webui`. They live here so the library package stays free of Storybook-only code. ## Layout (mirrors package source) -`stories/components/` maps one-to-one to `packages/components/src/components/`: +`stories/components/` maps one-to-one to `components/components/`: ``` -packages/components/src/components/ stories/components/ +components/components/ stories/components/ ├── editors/ ├── editors/ │ └── *.tsx │ └── *.stories.tsx ├── Toast/ ├── Toast/ @@ -42,7 +42,7 @@ From `stories/components//`, helpers are always `../../helpers/`. | Topic | Practice | |--------|----------| -| **Folder** | Same name as under `packages/components/src/components/` | +| **Folder** | Same name as under `components/components/` | | **File name** | Same as component: `ButtonComponent.stories.tsx` | | **Title** | `components//` — mirrors the folder path | | **Autodocs** | `tags: ['autodocs']` on meta | @@ -63,7 +63,7 @@ Storybook **component tests** run stories as Vitest tests in a real browser (Chr 1. Start Storybook: `npm run storybook` 2. Use the **testing widget** at the bottom of the sidebar to run all tests, or use a story’s context menu to run tests for one story/component. 3. Enable **Accessibility** in the widget to include a11y checks (requires `@storybook/addon-a11y`, already configured). -4. Enable **Coverage** in the widget to generate a coverage report for component source under `packages/components/src/`. +4. Enable **Coverage** in the widget to generate a coverage report for component source under `components/`. 5. Stories with **`play` functions** show results in the **Interactions** panel. ### From the CLI diff --git a/src/stories/tsconfig.json b/src/stories/tsconfig.json deleted file mode 100644 index fdbc086..0000000 --- a/src/stories/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../.storybook/tsconfig.json", - "include": ["./**/*"] -} diff --git a/src/storybook.css b/src/storybook.css index 10fc240..6d67bcb 100644 --- a/src/storybook.css +++ b/src/storybook.css @@ -1,5 +1,5 @@ @import 'tailwindcss'; -@source '../packages/components/src/**/*.tsx'; +@source './components/**/*.tsx'; @source './.storybook/**/*.tsx'; @source './stories/**/*.tsx'; diff --git a/src/packages/contracts/test/schemas.test.ts b/src/test/contracts/schemas.test.ts similarity index 69% rename from src/packages/contracts/test/schemas.test.ts rename to src/test/contracts/schemas.test.ts index ab55bd8..60405d2 100644 --- a/src/packages/contracts/test/schemas.test.ts +++ b/src/test/contracts/schemas.test.ts @@ -1,8 +1,9 @@ -import { PatchOperation } from '../src/PatchOperation' -import { LoginRequestSchema } from '../src/identity/login/LoginRequest' -import { PagedRequestSchema } from '../src/PagedRequest' -import { PatchRequestModelBaseSchema } from '../src/PatchRequestModelBase' -import { RefreshTokenRequestSchema } from '../src/identity/login/RefreshTokenRequest' +import { PatchOperation } from '@webui/contracts/PatchOperation' +import { LoginRequestSchema } from '@webui/contracts/identity/login/LoginRequest' +import { PagedRequestSchema } from '@webui/contracts/PagedRequest' +import { PatchRequestModelBaseSchema } from '@webui/contracts/PatchRequestModelBase' +import { RefreshTokenRequestSchema } from '@webui/contracts/identity/login/RefreshTokenRequest' +import { RedeemLoginExternalRequestSchema } from '@webui/contracts/oauth/RedeemLoginExternalRequest' describe('LoginRequestSchema', () => { it('accepts valid credentials', () => { @@ -71,3 +72,10 @@ describe('RefreshTokenRequestSchema', () => { expect(RefreshTokenRequestSchema.safeParse({ refreshToken: '' }).success).toBe(false) }) }) + +describe('RedeemLoginExternalRequestSchema', () => { + it('requires a non-empty code', () => { + expect(RedeemLoginExternalRequestSchema.safeParse({ code: 'otc-abc' }).success).toBe(true) + expect(RedeemLoginExternalRequestSchema.safeParse({ code: '' }).success).toBe(false) + }) +}) diff --git a/src/packages/core/test/functions/acl/parseAclEntry.test.ts b/src/test/core/functions/acl/parseAclEntry.test.ts similarity index 85% rename from src/packages/core/test/functions/acl/parseAclEntry.test.ts rename to src/test/core/functions/acl/parseAclEntry.test.ts index 2a2bffd..659af68 100644 --- a/src/packages/core/test/functions/acl/parseAclEntry.test.ts +++ b/src/test/core/functions/acl/parseAclEntry.test.ts @@ -1,5 +1,5 @@ -import { ScopePermission } from '../../../src/types/ScopePermissions' -import { parseAclEntry, parseAclEntries } from '../../../src/functions/acl/parseAclEntry' +import { ScopePermission } from '@webui/core/types/ScopePermissions' +import { parseAclEntry, parseAclEntries } from '@webui/core/functions/acl/parseAclEntry' const entityTypeMap = { O: 1, V: 2 } as const diff --git a/src/packages/core/test/functions/dataTable/dataTableFilters.test.ts b/src/test/core/functions/dataTable/dataTableFilters.test.ts similarity index 91% rename from src/packages/core/test/functions/dataTable/dataTableFilters.test.ts rename to src/test/core/functions/dataTable/dataTableFilters.test.ts index 10d3406..bf6dcfb 100644 --- a/src/packages/core/test/functions/dataTable/dataTableFilters.test.ts +++ b/src/test/core/functions/dataTable/dataTableFilters.test.ts @@ -1,4 +1,4 @@ -import { extractPropFilter } from '../../../src/functions/dataTable/dataTableFilters' +import { extractPropFilter } from '@webui/core/functions/dataTable/dataTableFilters' describe('extractPropFilter', () => { it('extracts Contains filter values', () => { diff --git a/src/packages/core/test/functions/dataTable/dataTablePaged.test.ts b/src/test/core/functions/dataTable/dataTablePaged.test.ts similarity index 88% rename from src/packages/core/test/functions/dataTable/dataTablePaged.test.ts rename to src/test/core/functions/dataTable/dataTablePaged.test.ts index 07bcef1..53b64cf 100644 --- a/src/packages/core/test/functions/dataTable/dataTablePaged.test.ts +++ b/src/test/core/functions/dataTable/dataTablePaged.test.ts @@ -1,5 +1,5 @@ -import type { PagedResponse } from '@maks-it.com/webui-contracts' -import { mapPagedToDataTable } from '../../../src/functions/dataTable/dataTablePaged' +import type { PagedResponse } from '@webui/contracts' +import { mapPagedToDataTable } from '@webui/core/functions/dataTable/dataTablePaged' describe('mapPagedToDataTable', () => { it('returns an empty page for missing responses', () => { diff --git a/src/packages/core/test/functions/date/isValidDateString.test.ts b/src/test/core/functions/date/isValidDateString.test.ts similarity index 83% rename from src/packages/core/test/functions/date/isValidDateString.test.ts rename to src/test/core/functions/date/isValidDateString.test.ts index 85a0d18..711c7a7 100644 --- a/src/packages/core/test/functions/date/isValidDateString.test.ts +++ b/src/test/core/functions/date/isValidDateString.test.ts @@ -1,4 +1,4 @@ -import { isValidISODateString } from '../../../src/functions/date/isValidDateString' +import { isValidISODateString } from '@webui/core/functions/date/isValidDateString' describe('isValidISODateString', () => { it('accepts valid ISO date strings', () => { diff --git a/src/packages/core/test/functions/deep/deepDelta.test.ts b/src/test/core/functions/deep/deepDelta.test.ts similarity index 97% rename from src/packages/core/test/functions/deep/deepDelta.test.ts rename to src/test/core/functions/deep/deepDelta.test.ts index 62dd505..5d82f08 100644 --- a/src/packages/core/test/functions/deep/deepDelta.test.ts +++ b/src/test/core/functions/deep/deepDelta.test.ts @@ -1,10 +1,10 @@ -import { COLLECTION_ITEM_OPERATION, PatchOperation } from '@maks-it.com/webui-contracts' -import { deepDelta, deltaHasOperations } from '../../../src/functions/deep/deepDelta' +import { COLLECTION_ITEM_OPERATION, PatchOperation } from '@webui/contracts' +import { deepDelta, deltaHasOperations } from '@webui/core/functions/deep/deepDelta' import { ENTITY_SCOPES_ARRAY_POLICY, HOSTNAMES_ARRAY_POLICY, VERSIONS_ARRAY_POLICY, -} from '../../../src/functions/deep/patchCollectionPolicies' +} from '@webui/core/functions/deep/patchCollectionPolicies' type EntityScopeRow = { id?: string @@ -57,7 +57,7 @@ describe('deepDelta', () => { const backup = { name: 'value', optional: 'present' } const form = { name: 'value', optional: null } - const delta = deepDelta(form, backup) + const delta = deepDelta<{ name: string; optional: string | null }>(form, backup) expect(delta.optional).toBeUndefined() expect(delta.operations?.optional).toBe(PatchOperation.RemoveField) @@ -1008,35 +1008,28 @@ describe('deepDelta', () => { const policyOptions = { arrays: { entityScopes: ENTITY_SCOPES_ARRAY_POLICY } } /** - * Synthetic identity includes `scope`, so changing scope on a row without server `id` - * is modeled as remove + add (not an in-place update). + * Synthetic identity is entityId + entityType (scope is a field), so changing scope + * on a row without server `id` is an in-place update (not remove + add). * * backup: * { entityScopes: [{ entityId: 'org-1', entityType: 0, scope: 1 }] } * form: * { entityScopes: [{ entityId: 'org-1', entityType: 0, scope: 3 }] } */ - it('treats scope bitmask change on id-less rows as remove and add', () => { + it('updates scope bitmask on id-less rows in place by _deltaId', () => { const delta = deepDelta( { entityScopes: [orgScope({ entityId: 'org-1', entityType: 0, scope: 3 })] }, { entityScopes: [orgScope({ entityId: 'org-1', entityType: 0, scope: 1 })] }, policyOptions ) - expect(delta.entityScopes).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - entityId: 'org-1', - entityType: 0, - scope: 3, - operations: { [COLLECTION_ITEM_OPERATION]: PatchOperation.AddToCollection }, - }), - expect.objectContaining({ - _deltaId: 'org-1-0-1', - operations: { [COLLECTION_ITEM_OPERATION]: PatchOperation.RemoveFromCollection }, - }), - ]) - ) + expect(delta.entityScopes).toEqual([ + expect.objectContaining({ + _deltaId: 'org-1-0', + scope: 3, + operations: expect.objectContaining({ scope: PatchOperation.SetField }), + }), + ]) }) /** diff --git a/src/packages/core/test/functions/deep/deepEqual.test.ts b/src/test/core/functions/deep/deepEqual.test.ts similarity index 93% rename from src/packages/core/test/functions/deep/deepEqual.test.ts rename to src/test/core/functions/deep/deepEqual.test.ts index e911ab5..63e3d93 100644 --- a/src/packages/core/test/functions/deep/deepEqual.test.ts +++ b/src/test/core/functions/deep/deepEqual.test.ts @@ -1,4 +1,4 @@ -import { deepEqual, deepEqualArrays } from '../../../src/functions/deep/deepEqual' +import { deepEqual, deepEqualArrays } from '@webui/core/functions/deep/deepEqual' describe('deepEqual', () => { it('returns true for identical primitives', () => { diff --git a/src/packages/core/test/functions/enum/flags.test.ts b/src/test/core/functions/enum/flags.test.ts similarity index 79% rename from src/packages/core/test/functions/enum/flags.test.ts rename to src/test/core/functions/enum/flags.test.ts index fb69479..78e4e1d 100644 --- a/src/packages/core/test/functions/enum/flags.test.ts +++ b/src/test/core/functions/enum/flags.test.ts @@ -1,6 +1,6 @@ -import { hasAnyFlag } from '../../../src/functions/enum/hasAnyFlag' -import { hasFlag } from '../../../src/functions/enum/hasFlag' -import { toggleFlag } from '../../../src/functions/enum/toggleFlag' +import { hasAnyFlag } from '@webui/core/functions/enum/hasAnyFlag' +import { hasFlag } from '@webui/core/functions/enum/hasFlag' +import { toggleFlag } from '@webui/core/functions/enum/toggleFlag' describe('hasFlag', () => { it('returns true when all flag bits are set', () => { diff --git a/src/packages/core/test/functions/guid/isGuid.test.ts b/src/test/core/functions/guid/isGuid.test.ts similarity index 88% rename from src/packages/core/test/functions/guid/isGuid.test.ts rename to src/test/core/functions/guid/isGuid.test.ts index 89be5e4..bcf4d8f 100644 --- a/src/packages/core/test/functions/guid/isGuid.test.ts +++ b/src/test/core/functions/guid/isGuid.test.ts @@ -1,4 +1,4 @@ -import { isGuid } from '../../../src/functions/guid/isGuid' +import { isGuid } from '@webui/core/functions/guid/isGuid' describe('isGuid', () => { it('accepts valid GUIDs', () => { diff --git a/src/packages/core/test/functions/headers/extractFilenameFromHeaders.test.ts b/src/test/core/functions/headers/extractFilenameFromHeaders.test.ts similarity index 88% rename from src/packages/core/test/functions/headers/extractFilenameFromHeaders.test.ts rename to src/test/core/functions/headers/extractFilenameFromHeaders.test.ts index 16f6ea3..4f33cb0 100644 --- a/src/packages/core/test/functions/headers/extractFilenameFromHeaders.test.ts +++ b/src/test/core/functions/headers/extractFilenameFromHeaders.test.ts @@ -1,4 +1,4 @@ -import { extractFilenameFromHeaders } from '../../../src/functions/headers/extractFilenameFromHeaders' +import { extractFilenameFromHeaders } from '@webui/core/functions/headers/extractFilenameFromHeaders' describe('extractFilenameFromHeaders', () => { it('returns fallback when header is missing', () => { diff --git a/src/packages/core/test/functions/zod/validateFormState.test.ts b/src/test/core/functions/zod/validateFormState.test.ts similarity index 89% rename from src/packages/core/test/functions/zod/validateFormState.test.ts rename to src/test/core/functions/zod/validateFormState.test.ts index b9b6246..b37ddfb 100644 --- a/src/packages/core/test/functions/zod/validateFormState.test.ts +++ b/src/test/core/functions/zod/validateFormState.test.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { validateFormState } from '../../../src/functions/zod/validateFormState' +import { validateFormState } from '@webui/core/functions/zod/validateFormState' describe('validateFormState', () => { const schema = z.object({ diff --git a/src/packages/core/test/http/problemDetails.test.ts b/src/test/core/http/problemDetails.test.ts similarity index 91% rename from src/packages/core/test/http/problemDetails.test.ts rename to src/test/core/http/problemDetails.test.ts index f4ce91b..00b9147 100644 --- a/src/packages/core/test/http/problemDetails.test.ts +++ b/src/test/core/http/problemDetails.test.ts @@ -1,4 +1,4 @@ -import { formatProblemDetailsMessage } from '../../src/http/problemDetails' +import { formatProblemDetailsMessage } from '@webui/core/http/problemDetails' describe('formatProblemDetailsMessage', () => { it('combines detail and field errors', () => { diff --git a/src/packages/core/test/signalr/useWebUiHub.test.ts b/src/test/core/signalr/useWebUiHub.test.ts similarity index 91% rename from src/packages/core/test/signalr/useWebUiHub.test.ts rename to src/test/core/signalr/useWebUiHub.test.ts index d0ea444..b68c4d3 100644 --- a/src/packages/core/test/signalr/useWebUiHub.test.ts +++ b/src/test/core/signalr/useWebUiHub.test.ts @@ -1,4 +1,4 @@ -import { resolveHubUrl, useWebUiHub } from '../../src/signalr/useWebUiHub' +import { resolveHubUrl, useWebUiHub } from '@webui/core/signalr/useWebUiHub' describe('resolveHubUrl', () => { it('returns absolute URLs unchanged', () => { diff --git a/src/test/jest-env.d.ts b/src/test/jest-env.d.ts new file mode 100644 index 0000000..ce16983 --- /dev/null +++ b/src/test/jest-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json deleted file mode 100644 index 1b32333..0000000 --- a/src/tsconfig.base.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "strict": true, - "skipLibCheck": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "isolatedModules": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true - } -} diff --git a/src/tsconfig.jest.json b/src/tsconfig.jest.json deleted file mode 100644 index cf5da01..0000000 --- a/src/tsconfig.jest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "noEmit": true, - "types": ["jest", "node"] - }, - "include": [ - "packages/core/src/**/*.ts", - "packages/core/test/**/*.ts", - "packages/contracts/src/**/*.ts", - "packages/contracts/test/**/*.ts" - ] -} diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 0000000..63966ce --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "isolatedModules": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "rootDir": ".", + "outDir": "dist", + "paths": { + "@webui/contracts": ["./contracts/index.ts"], + "@webui/contracts/*": ["./contracts/*"], + "@webui/core": ["./core/index.ts"], + "@webui/core/*": ["./core/*"], + "@webui/components": ["./components/index.ts"], + "@webui/components/*": ["./components/*"] + } + }, + "include": [ + "contracts", + "core", + "components", + "index.ts", + "test/**/*.ts", + "test/**/*.d.ts" + ] +} diff --git a/src/tsup.config.ts b/src/tsup.config.ts new file mode 100644 index 0000000..9c062cd --- /dev/null +++ b/src/tsup.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['index.ts'], + format: ['esm', 'cjs'], + dts: { + compilerOptions: { + ignoreDeprecations: '6.0', + }, + }, + tsconfig: 'tsconfig.json', + clean: true, + external: [ + 'react', + 'react-dom', + 'react-router-dom', + 'lucide-react', + '@tanstack/react-table', + 'react-virtualized', + 'axios', + '@microsoft/signalr', + 'zod', + ], +}) diff --git a/src/vitest.config.ts b/src/vitest.config.ts index 70bc452..57369b3 100644 --- a/src/vitest.config.ts +++ b/src/vitest.config.ts @@ -23,9 +23,9 @@ export default defineConfig({ }, resolve: { alias: { - '@webui/components': path.join(srcDir, 'packages/components/src'), - '@webui/contracts': path.join(srcDir, 'packages/contracts/src'), - '@webui/core': path.join(srcDir, 'packages/core/src'), + '@webui/components': path.join(srcDir, 'components'), + '@webui/contracts': path.join(srcDir, 'contracts'), + '@webui/core': path.join(srcDir, 'core'), }, }, esbuild: { @@ -51,7 +51,7 @@ export default defineConfig({ provider: 'v8', reporter: ['text', 'html', 'json-summary'], reportsDirectory: './coverage/storybook', - include: ['packages/components/src/**/*.{ts,tsx}'], + include: ['components/**/*.{ts,tsx}'], }, }, }, diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index f20a3bf..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./src/stories/tsconfig.json", - "compilerOptions": { - "declaration": false, - "declarationMap": false - }, - "include": ["src/stories/**/*.ts", "src/stories/**/*.tsx"] -} diff --git a/utils/engines/release/scriptSettings.json b/utils/engines/release/scriptSettings.json index 82fc42c..b944584 100644 --- a/utils/engines/release/scriptSettings.json +++ b/utils/engines/release/scriptSettings.json @@ -1,14 +1,14 @@ { "$schema": "https://json-schema.org/draft-07/schema", "title": "Release Package Script Settings", - "description": "maksit-webui: npm workspace publish (@maks-it.com/webui-*). Semver from NpmReleaseVersion (src/package.json).", + "description": "maksit-webui: npm workspace publish (@maks-it.com/webui). Semver from NpmReleaseVersion (src/package.json).", "plugins": [ { "name": "NpmReleaseVersion", "stageLabel": "build", "enabled": true, "packageJsonPath": "..\\..\\..\\src\\package.json", - "syncWorkspaceVersions": true + "syncWorkspaceVersions": false }, { "name": "NpmBuild", @@ -37,10 +37,9 @@ "stageLabel": "release", "enabled": true, "workspaceRoot": "..\\..\\..\\src", + "artifactsDir": "..\\..\\..\\releases", "publishOrder": [ - "@maks-it.com/webui-contracts", - "@maks-it.com/webui-core", - "@maks-it.com/webui-components" + "@maks-it.com/webui" ] }, { @@ -61,9 +60,7 @@ "access": "public", "workspaceRoot": "..\\..\\..\\src", "publishOrder": [ - "@maks-it.com/webui-contracts", - "@maks-it.com/webui-core", - "@maks-it.com/webui-components" + "@maks-it.com/webui" ] } ] diff --git a/utils/modules/ContainerEngineSupport.psm1 b/utils/modules/ContainerEngineSupport.psm1 index 095a688..96d3f93 100644 --- a/utils/modules/ContainerEngineSupport.psm1 +++ b/utils/modules/ContainerEngineSupport.psm1 @@ -278,6 +278,11 @@ function Get-ContainerEngineDisplayName { } function Invoke-ContainerEngine { + <# + .SYNOPSIS + Runs a container CLI command. Callers must read $LASTEXITCODE immediately after — do not + assign the function result to infer success (native stdout can mix with a returned exit code). + #> param( [Parameter(Mandatory = $true)] [hashtable]$Engine, @@ -287,7 +292,6 @@ function Invoke-ContainerEngine { ) & $Engine.Executable @ArgumentList - return $LASTEXITCODE } function Invoke-ContainerEngineWithInput { diff --git a/utils/modules/ContainerImagePushSupport.psm1 b/utils/modules/ContainerImagePushSupport.psm1 index be5740b..d5bb4fc 100644 --- a/utils/modules/ContainerImagePushSupport.psm1 +++ b/utils/modules/ContainerImagePushSupport.psm1 @@ -191,27 +191,27 @@ function Invoke-ContainerImagePush { $primaryRef = "${baseName}:$($imageTags[0])" Write-Log -Level 'STEP' -Message "Building $primaryRef via $engineName ..." - $buildExitCode = Invoke-ContainerEngine -Engine $engine build -t $primaryRef -f $dockerfilePath $imgContextPath - if ($buildExitCode -ne 0) { - throw "Container build failed for $primaryRef" + Invoke-ContainerEngine -Engine $engine build -t $primaryRef -f $dockerfilePath $imgContextPath + if ($LASTEXITCODE -ne 0) { + throw "Container build failed for $primaryRef (exit code: $LASTEXITCODE)" } Write-Log -Level 'STEP' -Message "Pushing $primaryRef via $engineName ..." - $pushExitCode = Invoke-ContainerEngine -Engine $engine push $primaryRef - if ($pushExitCode -ne 0) { - throw "Container push failed for $primaryRef" + Invoke-ContainerEngine -Engine $engine push $primaryRef + if ($LASTEXITCODE -ne 0) { + throw "Container push failed for $primaryRef (exit code: $LASTEXITCODE)" } for ($ti = 1; $ti -lt $imageTags.Count; $ti++) { $aliasRef = "${baseName}:$($imageTags[$ti])" Write-Log -Level 'STEP' -Message "Tagging and pushing $aliasRef via $engineName ..." - $tagExitCode = Invoke-ContainerEngine -Engine $engine tag $primaryRef $aliasRef - if ($tagExitCode -ne 0) { - throw "Container tag failed: $primaryRef -> $aliasRef" + Invoke-ContainerEngine -Engine $engine tag $primaryRef $aliasRef + if ($LASTEXITCODE -ne 0) { + throw "Container tag failed: $primaryRef -> $aliasRef (exit code: $LASTEXITCODE)" } - $aliasPushExitCode = Invoke-ContainerEngine -Engine $engine push $aliasRef - if ($aliasPushExitCode -ne 0) { - throw "Container push failed for $aliasRef" + Invoke-ContainerEngine -Engine $engine push $aliasRef + if ($LASTEXITCODE -ne 0) { + throw "Container push failed for $aliasRef (exit code: $LASTEXITCODE)" } } } diff --git a/utils/modules/Engine/ReleaseSupport.psm1 b/utils/modules/Engine/ReleaseSupport.psm1 index fc59341..853eb74 100644 --- a/utils/modules/Engine/ReleaseSupport.psm1 +++ b/utils/modules/Engine/ReleaseSupport.psm1 @@ -89,7 +89,7 @@ function New-EngineContext { $resolvedVersion = Resolve-ReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir $version = $resolvedVersion.version $versionSource = $resolvedVersion.source - $releaseRelative = '..\..\..\release' + $releaseRelative = '..\..\..\releases' $artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $ScriptDir $releaseRelative)) $currentBranch = Get-CurrentBranch diff --git a/utils/plugins/Npm/NpmPack.psm1 b/utils/plugins/Npm/NpmPack.psm1 index 5950396..f8350ba 100644 --- a/utils/plugins/Npm/NpmPack.psm1 +++ b/utils/plugins/Npm/NpmPack.psm1 @@ -79,6 +79,12 @@ function Invoke-Plugin { throw "NpmPack plugin requires non-empty 'publishOrder' or 'packOrder' (workspace package names)." } + Import-Module (Join-Path $PSScriptRoot 'NpmPackageSupport.psm1') -Force + $useWorkspaces = Test-NpmWorkspacesConfigured -WorkspaceRoot $workspaceRoot + if (-not $useWorkspaces -and $packOrder.Count -gt 1) { + throw "NpmPack plugin requires npm workspaces when packing more than one package." + } + if (-not (Test-Path $artifactsDirectory -PathType Container)) { New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null } @@ -89,7 +95,14 @@ function Invoke-Plugin { try { foreach ($packageName in $packOrder) { Write-Log -Level "STEP" -Message "Packing npm package '$packageName'..." - $tarballName = (npm pack -w $packageName --pack-destination $artifactsDirectory 2>$null | Select-Object -Last 1) + if ($useWorkspaces) { + $tarballName = (npm pack -w $packageName --pack-destination $artifactsDirectory 2>$null | Select-Object -Last 1) + } + else { + Assert-NpmRootPackageName -WorkspaceRoot $workspaceRoot -ExpectedPackageName $packageName + $tarballName = (npm pack --pack-destination $artifactsDirectory 2>$null | Select-Object -Last 1) + } + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace([string]$tarballName)) { throw "npm pack failed for '$packageName'." } @@ -99,6 +112,11 @@ function Invoke-Plugin { throw "Could not locate pack output for '$packageName' at: $tarballPath" } + $strayTarballPath = Join-Path $workspaceRoot ([string]$tarballName).Trim() + if ((Test-Path $strayTarballPath -PathType Leaf) -and $strayTarballPath -ne $tarballPath) { + Remove-Item -LiteralPath $strayTarballPath -Force -ErrorAction SilentlyContinue + } + Write-Log -Level "OK" -Message " Package ready: $tarballPath" $releaseAssetPaths += $tarballPath } diff --git a/utils/plugins/Npm/NpmPackageSupport.psm1 b/utils/plugins/Npm/NpmPackageSupport.psm1 new file mode 100644 index 0000000..4b3a903 --- /dev/null +++ b/utils/plugins/Npm/NpmPackageSupport.psm1 @@ -0,0 +1,62 @@ +#requires -Version 7.0 +#requires -PSEdition Core + +function Get-NpmRootPackageJson { + param( + [Parameter(Mandatory = $true)] + [string]$WorkspaceRoot + ) + + $packageJsonPath = Join-Path $WorkspaceRoot 'package.json' + if (-not (Test-Path $packageJsonPath -PathType Leaf)) { + throw "package.json not found at '$packageJsonPath'." + } + + return Get-Content -Path $packageJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json +} + +function Test-NpmWorkspacesConfigured { + param( + [Parameter(Mandatory = $true)] + [string]$WorkspaceRoot + ) + + $json = Get-NpmRootPackageJson -WorkspaceRoot $WorkspaceRoot + if (-not $json.PSObject.Properties['workspaces'] -or $null -eq $json.workspaces) { + return $false + } + + $workspaces = $json.workspaces + if ($workspaces -is [string]) { + return -not [string]::IsNullOrWhiteSpace($workspaces) + } + + if ($workspaces -is [System.Collections.IEnumerable]) { + $entries = @($workspaces | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) }) + return $entries.Count -gt 0 + } + + return $false +} + +function Assert-NpmRootPackageName { + param( + [Parameter(Mandatory = $true)] + [string]$WorkspaceRoot, + + [Parameter(Mandatory = $true)] + [string]$ExpectedPackageName + ) + + $json = Get-NpmRootPackageJson -WorkspaceRoot $WorkspaceRoot + $rootPackageName = [string]$json.name + if ([string]::IsNullOrWhiteSpace($rootPackageName)) { + throw "Root package.json at '$WorkspaceRoot' is missing 'name'." + } + + if ($rootPackageName -ne $ExpectedPackageName) { + throw "publishOrder package '$ExpectedPackageName' does not match root package name '$rootPackageName' (npm workspaces not configured)." + } +} + +Export-ModuleMember -Function Test-NpmWorkspacesConfigured, Assert-NpmRootPackageName, Get-NpmRootPackageJson diff --git a/utils/plugins/Npm/NpmPublish.psm1 b/utils/plugins/Npm/NpmPublish.psm1 index 09333de..146fe85 100644 --- a/utils/plugins/Npm/NpmPublish.psm1 +++ b/utils/plugins/Npm/NpmPublish.psm1 @@ -76,6 +76,12 @@ function Invoke-Plugin { throw "NpmPublish plugin requires non-empty 'publishOrder' (workspace package names)." } + Import-Module (Join-Path $PSScriptRoot 'NpmPackageSupport.psm1') -Force + $useWorkspaces = Test-NpmWorkspacesConfigured -WorkspaceRoot $workspaceRoot + if (-not $useWorkspaces -and $publishOrder.Count -gt 1) { + throw "NpmPublish plugin requires npm workspaces when publishing more than one package." + } + if ($dryRun) { foreach ($packageName in $publishOrder) { Write-Log -Level "INFO" -Message "Dry run: would publish npm package '$packageName' to $registry" @@ -106,7 +112,14 @@ registry=$registry foreach ($packageName in $publishOrder) { Write-Log -Level "STEP" -Message "Publishing npm package '$packageName'..." - npm publish -w $packageName --access $access --userconfig $tempNpmRcPath + if ($useWorkspaces) { + npm publish -w $packageName --access $access --userconfig $tempNpmRcPath + } + else { + Assert-NpmRootPackageName -WorkspaceRoot $workspaceRoot -ExpectedPackageName $packageName + npm publish --access $access --userconfig $tempNpmRcPath + } + if ($LASTEXITCODE -ne 0) { throw "Failed to publish npm package '$packageName'." }