(feature): release as unified npm package @maks-it.com/webui, deepDelta fixes
Some checks failed
Storybook tests / storybook-tests (push) Has been cancelled

This commit is contained in:
Maksym Sadovnychyy 2026-07-07 18:24:53 +02:00
parent b74e9250a7
commit 25ae18bab6
211 changed files with 1433 additions and 761 deletions

View File

@ -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"

View File

@ -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`
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`.

2
.gitignore vendored
View File

@ -267,6 +267,8 @@ __pycache__/
node_modules/
dist/
coverage/
src/coverage/
src/dist-test-dts/
storybook-static/
*.tsbuildinfo
.DS_Store

View File

@ -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"
}

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -1,21 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Branch Coverage: 78%">
<title>Branch Coverage: 78%</title>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Branch Coverage: 78.2%">
<title>Branch Coverage: 78.2%</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="r">
<rect width="147.5" height="20" rx="3" fill="#fff"/>
<rect width="150" height="20" rx="3" fill="#fff"/>
</clipPath>
<g clip-path="url(#r)">
<rect width="107.5" height="20" fill="#555"/>
<rect x="107.5" width="40" height="20" fill="#97ca00"/>
<rect width="147.5" height="20" fill="url(#s)"/>
<rect x="107.5" width="42.5" height="20" fill="#97ca00"/>
<rect width="150" height="20" fill="url(#s)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Branch Coverage</text>
<text x="53.75" y="14" fill="#fff">Branch Coverage</text>
<text aria-hidden="true" x="127.5" y="15" fill="#010101" fill-opacity=".3">78%</text>
<text x="127.5" y="14" fill="#fff">78%</text>
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">78.2%</text>
<text x="128.75" y="14" fill="#fff">78.2%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 82.2%">
<title>Line Coverage: 82.2%</title>
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 82.5%">
<title>Line Coverage: 82.5%</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
@ -15,7 +15,7 @@
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text aria-hidden="true" x="47.25" y="15" fill="#010101" fill-opacity=".3">Line Coverage</text>
<text x="47.25" y="14" fill="#fff">Line Coverage</text>
<text aria-hidden="true" x="115.75" y="15" fill="#010101" fill-opacity=".3">82.2%</text>
<text x="115.75" y="14" fill="#fff">82.2%</text>
<text aria-hidden="true" x="115.75" y="15" fill="#010101" fill-opacity=".3">82.5%</text>
<text x="115.75" y="14" fill="#fff">82.5%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 62.4%">
<title>Method Coverage: 62.4%</title>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 58.2%">
<title>Method Coverage: 58.2%</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
@ -9,13 +9,13 @@
</clipPath>
<g clip-path="url(#r)">
<rect width="107.5" height="20" fill="#555"/>
<rect x="107.5" width="42.5" height="20" fill="#97ca00"/>
<rect x="107.5" width="42.5" height="20" fill="#a4a61d"/>
<rect width="150" height="20" fill="url(#s)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Method Coverage</text>
<text x="53.75" y="14" fill="#fff">Method Coverage</text>
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">62.4%</text>
<text x="128.75" y="14" fill="#fff">62.4%</text>
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">58.2%</text>
<text x="128.75" y="14" fill="#fff">58.2%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -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'
<WebUiProvider
api={{
getData: (url) => getData(url),
@ -39,9 +52,27 @@ import { WebUiProvider, Loader, Authorization } from '@maks-it.com/webui-compone
</WebUiProvider>
```
## 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`.

View File

@ -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:

6
src/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
coverage/
dist/
dist-test-dts/
storybook-static/
*.tsbuildinfo
*.tgz

View File

@ -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<string, string> | 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

View File

@ -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/**/*"
]
}

54
src/README.md Normal file
View File

@ -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

View File

@ -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'

View File

@ -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'

View File

@ -1,4 +1,4 @@
import type { SearchEntityScopeEntry } from '@maks-it.com/webui-contracts'
import type { SearchEntityScopeEntry } from '@webui/contracts'
export interface EntityScopesSummaryProps<TScopeEntityType extends number = number> {
entries: SearchEntityScopeEntry<TScopeEntityType>[]

View File

@ -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'

View File

@ -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'

View File

@ -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
}) => (
<button
type="button"
className="w-full flex items-center justify-center gap-2 rounded border border-gray-300 bg-white px-4 py-2 text-sm hover:bg-gray-50"
onClick={onClick}
>
{iconSrc && <img src={iconSrc} alt="" className="h-5 w-5" />}
<span>{label}</span>
</button>
)
const ExternalLoginButtons: FC<ExternalLoginButtonsProps> = ({
providers,
onSelect,
className,
renderButton = defaultRenderButton,
}) => (
<div className={className ?? 'flex flex-col gap-2'}>
{providers.map(({ provider, label, iconSrc }) =>
renderButton({
provider,
label,
iconSrc,
onClick: () => onSelect(provider),
}),
)}
</div>
)
export { ExternalLoginButtons }

View File

@ -0,0 +1,14 @@
import type { FC } from 'react'
import { useIdpLoginCallback, type UseIdpLoginCallbackOptions } from '@webui/core'
export type IdpLoginCallbackHandlerProps = UseIdpLoginCallbackOptions
const IdpLoginCallbackHandler: FC<IdpLoginCallbackHandlerProps> = (props) => {
useIdpLoginCallback(props)
return null
}
export { IdpLoginCallbackHandler }
export type { LoginExternalExchangeResponse } from '@webui/contracts'
/** @deprecated Use `LoginExternalExchangeResponse`. */
export type { IdpRedeemLoginResponse } from '@webui/contracts'

View File

@ -0,0 +1,11 @@
import type { FC } from 'react'
import { useMailboxOAuthReturn, type UseMailboxOAuthReturnOptions } from '@webui/core'
export type MailboxOAuthReturnHandlerProps = UseMailboxOAuthReturnOptions
const MailboxOAuthReturnHandler: FC<MailboxOAuthReturnHandlerProps> = (props) => {
useMailboxOAuthReturn(props)
return null
}
export { MailboxOAuthReturnHandler }

View File

@ -0,0 +1,15 @@
export {
ExternalLoginButtons,
type ExternalLoginButtonsProps,
type ExternalLoginProviderConfig,
} from './ExternalLoginButtons'
export {
IdpLoginCallbackHandler,
type IdpLoginCallbackHandlerProps,
} from './IdpLoginCallbackHandler'
export {
MailboxOAuthReturnHandler,
type MailboxOAuthReturnHandlerProps,
} from './MailboxOAuthReturnHandler'

View File

@ -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<string, string>
sortBy?: string
isAscending?: boolean
}
@ -17,7 +17,6 @@ export const PagedRequestSchema: ZodType<PagedRequest> = intersection(
pageSize: number().optional(),
pageNumber: number().optional(),
filters: string().optional(),
collectionFilters: record(string(), string()).optional(),
sortBy: string().optional(),
isAscending: boolean().optional(),
})

View File

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

View File

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

View File

@ -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<LogoutRequest> = RequestModelBaseSchema.and(
object({
logoutFromAllDevices: boolean().optional(),
})
)

View File

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

View File

@ -0,0 +1,6 @@
import type { LoginExternalExchangeResponse } from './LoginExternalExchangeResponse'
/**
* @deprecated Use {@link LoginExternalExchangeResponse}. Legacy FiPlan.Auth snake_case shape removed.
*/
export type IdpRedeemLoginResponse = LoginExternalExchangeResponse

View File

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

View File

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

View File

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

View File

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

View File

@ -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<RedeemLoginExternalRequest> =
RequestModelBaseSchema.and(
object({
code: string().min(1),
}),
)

View File

@ -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<RefreshMailboxAccessTokenRequest> =
RequestModelBaseSchema.and(
object({
provider: string().min(1),
refreshToken: string().min(1),
}),
)

View File

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

View File

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

View File

@ -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'

View File

@ -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'

View File

@ -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<TScopeEntityType = number> {
scopeEntityType: TScopeEntityType
entityId: string

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