@team-codeme/crud-forge 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/agents/generator-engineer.md +31 -0
- package/.cursor/agents/keycloak-engineer.md +33 -0
- package/.cursor/agents/qa-engineer.md +32 -0
- package/.cursor/agents/security-reviewer.md +30 -0
- package/.cursor/mcp.json +13 -0
- package/.cursor/rules/crudforge-precautions.mdc +16 -0
- package/.cursor/rules/playwright-browser.mdc +30 -0
- package/.cursor/rules/readme-todo-sync.mdc +13 -0
- package/.env.qa.example +19 -0
- package/.github/workflows/qa-live.yml +32 -0
- package/.github/workflows/qa.yml +27 -0
- package/.vscode/settings.json +32 -0
- package/QA/IAM-PLAN.md +25 -0
- package/QA/IAM-RESULTS.md +39 -0
- package/QA/IAM-TESTCASES.md +36 -0
- package/QA/IAM-TODO.md +7 -0
- package/QA/KEYCLOAK-BOOTSTRAP.md +105 -0
- package/QA/PLAN.md +43 -0
- package/QA/RESULTS.md +59 -0
- package/QA/TESTCASES.md +65 -0
- package/QA/TODO.md +14 -0
- package/QA/pmb-dev/README.md +49 -0
- package/QA/pmb-dev/config/config.yml +152 -0
- package/QA/pmb-dev/gdl/i.gdl +17 -0
- package/README.md +851 -2
- package/Todo.md +207 -0
- package/assets/branding/crudforge-logo.png +0 -0
- package/assets/branding/crudforge-mark.png +0 -0
- package/branding/poster-crudforge.jpg +0 -0
- package/chat/last-chat/Investigating Mock Data Configuration.md +428 -0
- package/cli.js +167 -0
- package/config/default-config.yml +245 -0
- package/docs/performance/README.md +217 -0
- package/docs/performance/after.json +111 -0
- package/docs/performance/before.json +111 -0
- package/docs/quality/README.md +276 -0
- package/e2e/crudforge.e2e.yml +47 -0
- package/e2e/crudforge.field-profile.yml +33 -0
- package/e2e/crudforge.iam-live.yml +124 -0
- package/e2e/crudforge.iam.yml +62 -0
- package/e2e/tests/field-profile.spec.js +152 -0
- package/e2e/tests/generated-crud.spec.js +251 -0
- package/e2e/tests/iam-live.spec.js +149 -0
- package/e2e/tests/iam-pack.spec.js +151 -0
- package/e2e/tests/list-state.spec.js +73 -0
- package/features.csv +27 -0
- package/fuse-starter-pack/branding/crudforge-mark-source.jpg +0 -0
- package/fuse-starter-pack/branding/crudforge-mark-source.png +0 -0
- package/fuse-starter-pack/branding/crudforge-mark.png +0 -0
- package/fuse-starter-pack/branding/crudforge-wordmark-source.jpg +0 -0
- package/fuse-starter-pack/branding/crudforge-wordmark-source.png +0 -0
- package/fuse-starter-pack/branding/crudforge-wordmark.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/Dockerfile +14 -0
- package/fuse-starter-pack/fuse-v21.1/LICENSE.md +6 -0
- package/fuse-starter-pack/fuse-v21.1/README.md +59 -0
- package/fuse-starter-pack/fuse-v21.1/angular.json +113 -0
- package/fuse-starter-pack/fuse-v21.1/eslint.config.ts +150 -0
- package/fuse-starter-pack/fuse-v21.1/nginx.conf +23 -0
- package/fuse-starter-pack/fuse-v21.1/nginx.dev.conf +18 -0
- package/fuse-starter-pack/fuse-v21.1/package-lock.json +11513 -0
- package/fuse-starter-pack/fuse-v21.1/package.json +69 -0
- package/fuse-starter-pack/fuse-v21.1/postcss.config.json +5 -0
- package/fuse-starter-pack/fuse-v21.1/prettier.config.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/proxy.conf.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/proxy.dev.conf.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/public/favicon.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/Geist-Italic.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/Geist.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/GeistMono-Italic.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/GeistMono.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/i18n/en.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/public/i18n/es.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/flags/ES.svg +79 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/flags/US.svg +14 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/flags/where-to-find-other-flags.txt +5 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/logo/logo-mark.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/logo/logo.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/push.sh +27 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.config.server.ts +10 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.config.ts +64 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.routes.server.ts +8 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.routes.ts +39 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.ts +22 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/index.ts +1 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/lucide.ts +1 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/provider.ts +23 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/index.ts +2 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/local-storage.ts +109 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/provider.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/media/index.ts +2 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/media/media.ts +64 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/media/provider.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/index.ts +3 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/models/theming.ts +13 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/palette.ts +209 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/provider.ts +24 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/theming.ts +129 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/transloco/transloco-http-loader.ts +12 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/data/navigation.ts +176 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/layout.ts +77 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/language-switcher.ts +91 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/navigation.ts +238 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/notifications.ts +252 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/scheme-switcher.ts +58 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/sidebar.ts +31 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/user.ts +132 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/data/academy.ts +753 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/data/model.ts +27 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/features/course.ts +318 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/features/courses.ts +228 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/routes.ts +14 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/data/contacts.ts +4372 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/data/model.ts +34 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/features/contact.ts +239 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/features/contacts.ts +185 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/data/file-manager.ts +261 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/data/model.ts +12 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/features/file-manager.ts +131 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/routes.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/ui/list.ts +164 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/faq.ts +288 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/guide.ts +134 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/home.ts +219 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/support.ts +161 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/routes.ts +33 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/data/model.ts +18 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/data/notes.ts +324 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/features/note.ts +152 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/features/notes.ts +118 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/data/model.ts +17 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/data/tasks.ts +562 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/features/task.ts +195 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/features/tasks.ts +271 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/analytics.ts +1620 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/finance.ts +1065 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/project.ts +376 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/analytics/analytics.ts +1096 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/finance/finance.ts +658 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/project/budget-table.ts +188 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/project/project.ts +565 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/routes.ts +23 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/changelog/changelog.ts +932 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/building.ts +33 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/development.ts +42 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/installation.ts +47 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/routes.ts +45 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/error/features/error-page.ts +70 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/error/routes.ts +21 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/notifications/features/notifications.ts +262 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/notifications/routes.ts +10 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/account.ts +238 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/notifications.ts +134 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/plan-and-billing.ts +238 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/security.ts +211 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/team.ts +188 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/layout.ts +109 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/routes.ts +37 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/welcome/welcome.component.ts +22 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/routes.ts +95 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/forgot-password/forgot-password.html +54 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/forgot-password/forgot-password.ts +51 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/reset-password/reset-password.html +82 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/reset-password/reset-password.ts +69 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-in/sign-in.html +132 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-in/sign-in.ts +54 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-up/sign-up.html +183 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-up/sign-up.ts +56 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/routes.ts +34 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/features/coming-soon.ts +156 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/layout/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/modules/home/features/home.ts +23 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/modules/home/routes.ts +10 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/routes.ts +30 -0
- package/fuse-starter-pack/fuse-v21.1/src/environments/environment.mock.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/environments/environment.prod.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/environments/environment.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/index.html +66 -0
- package/fuse-starter-pack/fuse-v21.1/src/main.server.ts +11 -0
- package/fuse-starter-pack/fuse-v21.1/src/main.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/server.ts +70 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/base/base.css +63 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/base/typography.css +54 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/a.css +18 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/input.css +22 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/kbd.css +55 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/material.css +1149 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/styles.css +31 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/theme.css +147 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/utilities.css +563 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/variants.css +4 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/third-party/lucide-icons.css +12 -0
- package/fuse-starter-pack/fuse-v21.1/tsconfig.app.json +16 -0
- package/fuse-starter-pack/fuse-v21.1/tsconfig.json +37 -0
- package/fuse-starter-pack/fuse-v21.1/tsconfig.spec.json +10 -0
- package/generator/README.md +12 -0
- package/generator/issues/.gitkeep +0 -0
- package/package.json +56 -4
- package/playwright.config.js +29 -0
- package/playwright.field-profile.config.js +6 -0
- package/playwright.iam-live.config.js +31 -0
- package/playwright.iam.config.js +29 -0
- package/playwright.list-state.config.js +6 -0
- package/playwright.performance.config.js +1 -0
- package/run-config-tests.sh +5 -0
- package/scripts/e2e-paths.js +89 -0
- package/scripts/e2e-prepare-field-profile.js +17 -0
- package/scripts/e2e-prepare-iam-live.js +135 -0
- package/scripts/e2e-prepare-iam.js +65 -0
- package/scripts/e2e-prepare.js +72 -0
- package/scripts/e2e-serve-iam-live.js +28 -0
- package/scripts/e2e-serve-iam.js +28 -0
- package/scripts/e2e-serve.js +28 -0
- package/scripts/performance/generate.js +17 -0
- package/scripts/performance/measure.js +55 -0
- package/scripts/postinstall.js +9 -0
- package/scripts/prepare-quality-apps.js +34 -0
- package/scripts/run-config-tests.js +63 -0
- package/src/assets/logo-with-lable.jpeg +0 -0
- package/src/assets/logo.jpeg +0 -0
- package/src/commands/bootstrap.js +92 -0
- package/src/commands/doctor.js +403 -0
- package/src/commands/generate.js +301 -0
- package/src/commands/init-config.js +310 -0
- package/src/commands/keycloak-options.js +182 -0
- package/src/commands/validate.js +52 -0
- package/src/config/auth-config.js +183 -0
- package/src/config/default-values.js +235 -0
- package/src/config/env-refs.js +71 -0
- package/src/config/field-preferences.js +99 -0
- package/src/config/input-sources.js +104 -0
- package/src/config/load-config.js +166 -0
- package/src/config/proxy-routes.js +44 -0
- package/src/generator/access-control-tools.js +334 -0
- package/src/generator/ai-docs.js +691 -0
- package/src/generator/application-tests.js +252 -0
- package/src/generator/auth-pack.js +965 -0
- package/src/generator/embed-config.js +431 -0
- package/src/generator/entity-layout.js +30 -0
- package/src/generator/fuse-build.js +57 -0
- package/src/generator/fuse-runtime.js +344 -0
- package/src/generator/generated-banner.js +137 -0
- package/src/generator/index.js +874 -0
- package/src/generator/project-output.js +126 -0
- package/src/generator/team-pack.js +220 -0
- package/src/index.js +68 -0
- package/src/parser/gdl-parser.js +169 -0
- package/src/parser/jdl-parser.js +171 -0
- package/src/presets/pmb-test.js +139 -0
- package/src/presets/static-source.js +73 -0
- package/src/presets/store.js +96 -0
- package/src/provision/admin-service.js +148 -0
- package/src/provision/bootstrap.js +147 -0
- package/src/provision/http.js +123 -0
- package/src/provision/keycloak-admin-rest.js +335 -0
- package/src/provision/state.js +51 -0
- package/src/provision/token.js +52 -0
- package/src/templates/angular/auth/access-control.service.ts.ejs +179 -0
- package/src/templates/angular/auth/access-ctrl.pipe.ts.ejs +20 -0
- package/src/templates/angular/auth/auth.guard.ts.ejs +27 -0
- package/src/templates/angular/auth/auth.interceptor.ts.ejs +53 -0
- package/src/templates/angular/auth/auth.service.ts.ejs +594 -0
- package/src/templates/angular/auth/auth.utils.ts.ejs +28 -0
- package/src/templates/angular/auth/current-user.ts.ejs +24 -0
- package/src/templates/angular/auth/disabled-account-overlay.component.css.ejs +95 -0
- package/src/templates/angular/auth/disabled-account-overlay.component.html.ejs +33 -0
- package/src/templates/angular/auth/disabled-account-overlay.component.ts.ejs +23 -0
- package/src/templates/angular/auth/fuse-account.ts.ejs +234 -0
- package/src/templates/angular/auth/fuse-user.ts.ejs +264 -0
- package/src/templates/angular/auth/has-permission.directive.ts.ejs +51 -0
- package/src/templates/angular/auth/index.ts.ejs +24 -0
- package/src/templates/angular/auth/keycloak-admin.service.ts.ejs +670 -0
- package/src/templates/angular/auth/landing.guard.ts.ejs +61 -0
- package/src/templates/angular/auth/no-auth.guard.ts.ejs +19 -0
- package/src/templates/angular/auth/permission-attribute.util.ts.ejs +182 -0
- package/src/templates/angular/auth/sign-in.ts.ejs +60 -0
- package/src/templates/angular/entity/_entity_.model.ts.ejs +64 -0
- package/src/templates/angular/entity/_entity_.routes.ts.ejs +9 -0
- package/src/templates/angular/entity/components/forms/form/_entity_-form.component.html.ejs +168 -0
- package/src/templates/angular/entity/components/forms/form/_entity_-form.component.ts.ejs +175 -0
- package/src/templates/angular/entity/components/forms/form-page/_entity_-form-page.component.html.ejs +10 -0
- package/src/templates/angular/entity/components/forms/form-page/_entity_-form-page.component.ts.ejs +86 -0
- package/src/templates/angular/entity/components/forms/update/_entity_-form.service.ts.ejs +136 -0
- package/src/templates/angular/entity/components/interaction/actions/_entity_-actions.component.html.ejs +68 -0
- package/src/templates/angular/entity/components/interaction/actions/_entity_-actions.component.ts.ejs +69 -0
- package/src/templates/angular/entity/components/interaction/filter/_entity_-filter.component.html.ejs +111 -0
- package/src/templates/angular/entity/components/interaction/filter/_entity_-filter.component.ts.ejs +102 -0
- package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.css.ejs +33 -0
- package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.html.ejs +29 -0
- package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.ts.ejs +93 -0
- package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.css.ejs +54 -0
- package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.html.ejs +58 -0
- package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.ts.ejs +47 -0
- package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.html.ejs +419 -0
- package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.scss.ejs +48 -0
- package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.ts.ejs +592 -0
- package/src/templates/angular/entity/service/_entity_.service.ts.ejs +153 -0
- package/src/templates/angular/entity/settings/_entity_.settings.ts.ejs +49 -0
- package/src/templates/angular/enums/_enum_.model.ts.ejs +11 -0
- package/src/templates/angular/iam/change-password.component.html.ejs +39 -0
- package/src/templates/angular/iam/change-password.component.ts.ejs +44 -0
- package/src/templates/angular/iam/no-access.component.ts.ejs +17 -0
- package/src/templates/angular/iam/role-list.component.html.ejs +331 -0
- package/src/templates/angular/iam/role-list.component.scss.ejs +107 -0
- package/src/templates/angular/iam/role-list.component.ts.ejs +597 -0
- package/src/templates/angular/iam/role-management/role-header.component.html.ejs +51 -0
- package/src/templates/angular/iam/role-management/role-header.component.ts.ejs +45 -0
- package/src/templates/angular/iam/role-management/role-inheritance.component.html.ejs +15 -0
- package/src/templates/angular/iam/role-management/role-inheritance.component.ts.ejs +19 -0
- package/src/templates/angular/iam/role-management/role-permissions-panel.component.css.ejs +24 -0
- package/src/templates/angular/iam/role-management/role-permissions-panel.component.html.ejs +108 -0
- package/src/templates/angular/iam/role-management/role-permissions-panel.component.ts.ejs +122 -0
- package/src/templates/angular/iam/role-management/role-users.component.html.ejs +104 -0
- package/src/templates/angular/iam/role-management/role-users.component.ts.ejs +53 -0
- package/src/templates/angular/iam/role-management/user-select-dialog.component.html.ejs +113 -0
- package/src/templates/angular/iam/role-management/user-select-dialog.component.ts.ejs +157 -0
- package/src/templates/angular/iam/role-permission-matrix.component.html.ejs +355 -0
- package/src/templates/angular/iam/role-permission-matrix.component.scss.ejs +217 -0
- package/src/templates/angular/iam/role-permission-matrix.component.ts.ejs +800 -0
- package/src/templates/angular/iam/screen-catalog.ts.ejs +147 -0
- package/src/templates/angular/iam/shared/confirm-dialog.component.ts.ejs +83 -0
- package/src/templates/angular/iam/shared/matrix-hierarchy.util.ts.ejs +196 -0
- package/src/templates/angular/iam/shared/permission-matrix-sound.ts.ejs +59 -0
- package/src/templates/angular/iam/shared/role-display.util.ts.ejs +43 -0
- package/src/templates/angular/iam/shared/role-permission-skeleton.component.ts.ejs +54 -0
- package/src/templates/angular/iam/shared/screen-matrix.util.ts.ejs +554 -0
- package/src/templates/angular/iam/shared/screen-navigation-map.ts.ejs +187 -0
- package/src/templates/angular/iam/shared/screen-navigation.registry.ts.ejs +64 -0
- package/src/templates/angular/iam/shared/text-highlight.pipe.ts.ejs +39 -0
- package/src/templates/angular/iam/user-access-matrix.component.html.ejs +212 -0
- package/src/templates/angular/iam/user-access-matrix.component.scss.ejs +34 -0
- package/src/templates/angular/iam/user-access-matrix.component.ts.ejs +239 -0
- package/src/templates/angular/iam/user-detail.component.html.ejs +143 -0
- package/src/templates/angular/iam/user-detail.component.ts.ejs +108 -0
- package/src/templates/angular/iam/user-form.component.html.ejs +63 -0
- package/src/templates/angular/iam/user-form.component.ts.ejs +100 -0
- package/src/templates/angular/iam/user-list.component.html.ejs +369 -0
- package/src/templates/angular/iam/user-list.component.scss.ejs +242 -0
- package/src/templates/angular/iam/user-list.component.ts.ejs +446 -0
- package/src/templates/angular/iam/user-management.routes.ts.ejs +47 -0
- package/src/templates/angular/iam/user-permission-matrix.component.html.ejs +231 -0
- package/src/templates/angular/iam/user-permission-matrix.component.scss.ejs +216 -0
- package/src/templates/angular/iam/user-permission-matrix.component.ts.ejs +516 -0
- package/src/templates/angular/shared/base/audit-user-name.pipe.ts.ejs +69 -0
- package/src/templates/angular/shared/base/base-filter.component.ts.ejs +245 -0
- package/src/templates/angular/shared/base/base-form.component.ts.ejs +109 -0
- package/src/templates/angular/shared/base/base-list.component.ts.ejs +507 -0
- package/src/templates/angular/shared/base/filter-types.ts.ejs +55 -0
- package/src/templates/angular/shared/base/input-format.directive.ts.ejs +47 -0
- package/src/templates/angular/shared/base/input-format.ts.ejs +15 -0
- package/src/templates/angular/shared/confirm-with-input-dialog.component.ts.ejs +91 -0
- package/src/templates/angular/shared/confirmation-dialog.component.ts.ejs +80 -0
- package/src/templates/angular/shared/list-styles.css.ejs +103 -0
- package/src/templates/angular/shared/loading-overlay.component.ts.ejs +24 -0
- package/src/templates/angular/shared/resizable.directive.ts.ejs +115 -0
- package/src/templates/angular/shared/snackbar-styles.css.ejs +11 -0
- package/src/templates/fuse-packs/access-control-tools/access-control-sync.js +819 -0
- package/src/templates/fuse-packs/access-control-tools/build-rename-map.js +67 -0
- package/src/templates/fuse-packs/access-control-tools/inject-qa-ids.js +14 -0
- package/src/templates/fuse-packs/access-control-tools/lib/hierarchy-scanner.js +628 -0
- package/src/templates/fuse-packs/access-control-tools/lib/hierarchy-scanner.test.js +323 -0
- package/src/templates/fuse-packs/access-control-tools/lib/html-scanner.js +362 -0
- package/src/templates/fuse-packs/access-control-tools/lib/id-rules.duplicate.test.js +136 -0
- package/src/templates/fuse-packs/access-control-tools/lib/id-rules.js +1095 -0
- package/src/templates/fuse-packs/access-control-tools/lib/ts-component-index.js +482 -0
- package/src/templates/fuse-packs/access-control-tools/output/.gitkeep +0 -0
- package/src/templates/fuse-packs/access-control-tools/run.sh +223 -0
- package/src/templates/fuse-packs/access-control-tools/scan-access-control.js +14 -0
- package/src/templates/fuse-packs/iam-assets/sound/apple-play.mp3 +0 -0
- package/src/templates/fuse-packs/iam-assets/sound/remove.mp3 +0 -0
- package/src/templates/generator/README.md +12 -0
- package/src/templates/testing/ci.cjs +45 -0
- package/src/templates/testing/mock.cjs +149 -0
- package/src/templates/testing/playwright.config.cjs +25 -0
- package/src/templates/testing/serve.cjs +22 -0
- package/src/templates/testing/smoke.spec.cjs +180 -0
- package/src/utils/branding.js +39 -0
- package/src/utils/colors.js +18 -0
- package/src/utils/diff.js +109 -0
- package/src/utils/merger.js +115 -0
- package/src/validator/backend-diagnostics.js +30 -0
- package/src/validator/blueprint-validator.js +276 -0
- package/src/validator/diagnostics.js +608 -0
- package/src/validator/pattern.js +28 -0
- package/test/auth-interceptor.js +84 -0
- package/test/auth-provision-checks.js +297 -0
- package/test/field-preferences.js +145 -0
- package/test/fixtures/field-profile.gdl +7 -0
- package/test/fixtures/mock.jdl +17 -0
- package/test/fixtures/mock.png +0 -0
- package/test/fixtures/performance/config.yml +33 -0
- package/test/fixtures/performance/large.gdl +199 -0
- package/test/fixtures/performance/small.gdl +7 -0
- package/test/fixtures/quality/custom.yml +22 -0
- package/test/fixtures/quality/go-duck.yml +22 -0
- package/test/fixtures/quality/jhipster.yml +30 -0
- package/test/fixtures/quality/representative.gdl +14 -0
- package/test/list-state.js +138 -0
- package/test/micro-functions-layout.js +95 -0
- package/test/performance-generation.js +32 -0
- package/test/pmb-preset.js +127 -0
- package/test/project-workflow.js +111 -0
- package/test/quality-features.js +250 -0
- package/test/verify-commands.js +83 -0
- package/test/verify.js +959 -0
- package/test-files/GDL/example.gdl +15 -0
- package/test-files/JDL/example.jdl +15 -0
- package/test-files/config/crudforge.config.yml +127 -0
- package/test-run.sh +272 -0
|
@@ -0,0 +1,1095 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared ID rules for access-control + QA injection.
|
|
5
|
+
*
|
|
6
|
+
* Convention:
|
|
7
|
+
* screenId = entity/page key (from SCREEN or filename)
|
|
8
|
+
* componentId = {verb}[-{noun}] e.g. save, open-filter, export-excel
|
|
9
|
+
* qaId = qa-{screenId}__{componentId}
|
|
10
|
+
* accessCtrl = SCREEN | accessCtrl: '{componentId}'
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const GENERIC_SCREEN_SEGMENTS = new Set([
|
|
14
|
+
'actions',
|
|
15
|
+
'action',
|
|
16
|
+
'list',
|
|
17
|
+
'lists',
|
|
18
|
+
'filter',
|
|
19
|
+
'filters',
|
|
20
|
+
'form',
|
|
21
|
+
'forms',
|
|
22
|
+
'detail',
|
|
23
|
+
'details',
|
|
24
|
+
'dialog',
|
|
25
|
+
'dialogs',
|
|
26
|
+
'shared',
|
|
27
|
+
'component',
|
|
28
|
+
'components',
|
|
29
|
+
'page',
|
|
30
|
+
'pages',
|
|
31
|
+
'panel',
|
|
32
|
+
'panels',
|
|
33
|
+
'create',
|
|
34
|
+
'edit',
|
|
35
|
+
'update',
|
|
36
|
+
'new',
|
|
37
|
+
'view',
|
|
38
|
+
'card',
|
|
39
|
+
'cards',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/** Click / method patterns that must never become RBAC or QA targets. */
|
|
43
|
+
const NOISE_ACTION_PATTERNS = [
|
|
44
|
+
/^\$?event\.stoppropagation$/i,
|
|
45
|
+
/^stoppropagation$/i,
|
|
46
|
+
/^onbackdropclick$/i,
|
|
47
|
+
/^presentation$/i,
|
|
48
|
+
/^null$/i,
|
|
49
|
+
/^true$/i,
|
|
50
|
+
/^false$/i,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/** Exact slugified actions to skip (chrome / non-permission UI). */
|
|
54
|
+
const NOISE_ACTIONS = new Set([
|
|
55
|
+
'event-stop-propagation',
|
|
56
|
+
'stop-propagation',
|
|
57
|
+
'on-backdrop-click',
|
|
58
|
+
'on-search-term-change',
|
|
59
|
+
'toggle-pin',
|
|
60
|
+
'set-flag-menu-row',
|
|
61
|
+
'set-flag-color',
|
|
62
|
+
'isexporting',
|
|
63
|
+
'is-exporting',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Map raw method / emit names → stable semantic componentId verbs.
|
|
68
|
+
* Keys are slugified (kebab-case) method names without args.
|
|
69
|
+
*/
|
|
70
|
+
const SEMANTIC_ACTION_MAP = {
|
|
71
|
+
'on-cancel': 'cancel',
|
|
72
|
+
cancel: 'cancel',
|
|
73
|
+
'close-drawer': 'close-drawer',
|
|
74
|
+
'close-filter-drawer': 'close-filter',
|
|
75
|
+
'clear-filters': 'clear-filters',
|
|
76
|
+
'clear-field': 'clear-field',
|
|
77
|
+
'clear-all': 'clear-filters',
|
|
78
|
+
'filter-click-emit': 'open-filter',
|
|
79
|
+
'filter-click': 'open-filter',
|
|
80
|
+
filter: 'open-filter',
|
|
81
|
+
'new-click-emit': 'create',
|
|
82
|
+
'new-click': 'create',
|
|
83
|
+
new: 'create',
|
|
84
|
+
add: 'create',
|
|
85
|
+
'open-drawer-create': 'create',
|
|
86
|
+
'toggle-view-mode': 'toggle-view',
|
|
87
|
+
'set-view-mode': 'toggle-view',
|
|
88
|
+
'view-mode': 'toggle-view',
|
|
89
|
+
'toggle-filter': 'open-filter',
|
|
90
|
+
columns: 'open-columns',
|
|
91
|
+
'column-preferences': 'open-columns',
|
|
92
|
+
'export-all-data': 'export-all',
|
|
93
|
+
'export-filtered-data': 'export-filtered',
|
|
94
|
+
'export-to-excel': 'export-excel',
|
|
95
|
+
'export-to-pdf': 'export-pdf',
|
|
96
|
+
'export-excel': 'export-excel',
|
|
97
|
+
'export-pdf': 'export-pdf',
|
|
98
|
+
'generate-pdf': 'export-pdf',
|
|
99
|
+
'reset-column-preferences': 'reset-columns',
|
|
100
|
+
'open-column-preferences': 'open-columns',
|
|
101
|
+
'open-form-drawer': 'edit',
|
|
102
|
+
'view-details-for-element': 'view',
|
|
103
|
+
'view-details': 'view',
|
|
104
|
+
delete: 'delete',
|
|
105
|
+
'toggle-active': 'toggle-active',
|
|
106
|
+
'go-back': 'go-back',
|
|
107
|
+
'on-card-activate': 'open',
|
|
108
|
+
'on-view-details': 'view',
|
|
109
|
+
'on-edit': 'edit',
|
|
110
|
+
'copy-token-number': 'copy-token',
|
|
111
|
+
'assign-to-me': 'assign-to-me',
|
|
112
|
+
action: 'submit', // type=submit / bare mat-button fallback refined later
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const SCREEN_NAME_OVERRIDES = {
|
|
116
|
+
'user-management': 'I-Users',
|
|
117
|
+
institute: 'I-Institutions',
|
|
118
|
+
toplevel: 'I-Association Board',
|
|
119
|
+
'stakeholder-business': 'I-Stackholders',
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function slugify(text) {
|
|
123
|
+
return String(text || '')
|
|
124
|
+
.toLowerCase()
|
|
125
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
126
|
+
.replace(/(^-|-$)/g, '');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function camelToKebab(text) {
|
|
130
|
+
return String(text || '')
|
|
131
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
132
|
+
.replace(/[_.]/g, '-')
|
|
133
|
+
.toLowerCase();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resolveMeaningfulSegment(segments) {
|
|
137
|
+
for (let index = segments.length - 1; index >= 0; index--) {
|
|
138
|
+
const segment = segments[index];
|
|
139
|
+
if (!GENERIC_SCREEN_SEGMENTS.has(segment)) {
|
|
140
|
+
return segment;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return segments[segments.length - 1] || '';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatScreenName(folderName) {
|
|
147
|
+
if (SCREEN_NAME_OVERRIDES[folderName]) {
|
|
148
|
+
return SCREEN_NAME_OVERRIDES[folderName];
|
|
149
|
+
}
|
|
150
|
+
const words = folderName.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1));
|
|
151
|
+
return 'I-' + words.join(' ');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function formatScreenNameFromPath(baseDir, filePath, pathModule) {
|
|
155
|
+
const relativeDir = pathModule.relative(baseDir, pathModule.dirname(filePath));
|
|
156
|
+
const segments = relativeDir.split(pathModule.sep).filter(Boolean);
|
|
157
|
+
const meaningfulSegment = resolveMeaningfulSegment(segments);
|
|
158
|
+
return formatScreenName(meaningfulSegment || pathModule.basename(pathModule.dirname(filePath)));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const VIEW_SUFFIXES = [
|
|
162
|
+
'column-preferences',
|
|
163
|
+
'status-dialog',
|
|
164
|
+
'form-page',
|
|
165
|
+
'actions',
|
|
166
|
+
'filter',
|
|
167
|
+
'detail',
|
|
168
|
+
'drawer',
|
|
169
|
+
'dialog',
|
|
170
|
+
'list',
|
|
171
|
+
'form',
|
|
172
|
+
'card',
|
|
173
|
+
'page',
|
|
174
|
+
'panel',
|
|
175
|
+
'section',
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Pull human-facing copy from a template for unique ids + matrix descriptions.
|
|
180
|
+
* Prefers the first real h1/h2 title (Angular bindings stripped).
|
|
181
|
+
*/
|
|
182
|
+
function extractScreenContent(htmlContent) {
|
|
183
|
+
const html = String(htmlContent || '');
|
|
184
|
+
const candidates = [];
|
|
185
|
+
|
|
186
|
+
const headingRe = /<h[12]\b[^>]*>([\s\S]*?)<\/h[12]>/gi;
|
|
187
|
+
let match;
|
|
188
|
+
while ((match = headingRe.exec(html)) !== null) {
|
|
189
|
+
const text = cleanTemplateText(match[1]);
|
|
190
|
+
if (text) {
|
|
191
|
+
candidates.push(text);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!candidates.length) {
|
|
196
|
+
const titleRe = /<(?:div|span|p)\b[^>]*class="[^"]*(?:text-3xl|truncate text-3xl|page-title|screen-title)[^"]*"[^>]*>([\s\S]*?)<\//gi;
|
|
197
|
+
while ((match = titleRe.exec(html)) !== null) {
|
|
198
|
+
const text = cleanTemplateText(match[1]);
|
|
199
|
+
if (text) {
|
|
200
|
+
candidates.push(text);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const title = candidates[0] || '';
|
|
206
|
+
return {
|
|
207
|
+
title,
|
|
208
|
+
titles: candidates,
|
|
209
|
+
snippet: title,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function cleanTemplateText(raw) {
|
|
214
|
+
return String(raw || '')
|
|
215
|
+
.replace(/\{\{[\s\S]*?\}\}/g, ' ')
|
|
216
|
+
.replace(/@if\s*\([\s\S]*?\)\s*\{[\s\S]*?\}/g, ' ')
|
|
217
|
+
.replace(/<[^>]+>/g, ' ')
|
|
218
|
+
.replace(/ /gi, ' ')
|
|
219
|
+
.replace(/&/gi, '&')
|
|
220
|
+
.replace(/\s+/g, ' ')
|
|
221
|
+
.trim();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function detectViewSuffix(fileName) {
|
|
225
|
+
const base = String(fileName || '')
|
|
226
|
+
.replace(/\.component$/i, '')
|
|
227
|
+
.toLowerCase();
|
|
228
|
+
for (const suffix of VIEW_SUFFIXES) {
|
|
229
|
+
if (base === suffix || base.endsWith(`-${suffix}`)) {
|
|
230
|
+
return suffix;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return '';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Trailing title words that are marketing chrome, not the product entity.
|
|
238
|
+
* Keeps nav grants aligned: "Regional Office Registration" → regional-office(-list).
|
|
239
|
+
* Do not strip "management" (e.g. User Management).
|
|
240
|
+
*/
|
|
241
|
+
const TITLE_TRAILING_NOISE = new Set([
|
|
242
|
+
'registration',
|
|
243
|
+
'register',
|
|
244
|
+
'master',
|
|
245
|
+
'portal',
|
|
246
|
+
'overview',
|
|
247
|
+
'screen',
|
|
248
|
+
'module',
|
|
249
|
+
'records',
|
|
250
|
+
'record',
|
|
251
|
+
'listing',
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
/** Headings too generic to own a unique screen id — use path-canonical instead. */
|
|
255
|
+
const GENERIC_CONTENT_TITLES = new Set([
|
|
256
|
+
'advanced-filters',
|
|
257
|
+
'advanced-filter',
|
|
258
|
+
'filters',
|
|
259
|
+
'filter',
|
|
260
|
+
'search',
|
|
261
|
+
'actions',
|
|
262
|
+
'details',
|
|
263
|
+
'detail',
|
|
264
|
+
'form',
|
|
265
|
+
'list',
|
|
266
|
+
'card',
|
|
267
|
+
'dialog',
|
|
268
|
+
'drawer',
|
|
269
|
+
'loading',
|
|
270
|
+
'error',
|
|
271
|
+
'confirm',
|
|
272
|
+
'confirmation',
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
function isGenericContentTitle(title) {
|
|
276
|
+
const slug = slugify(title);
|
|
277
|
+
if (!slug) {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
if (GENERIC_CONTENT_TITLES.has(slug)) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
// Single generic token
|
|
284
|
+
if (GENERIC_CONTENT_TITLES.has(slug.replace(/-panel$/, '').replace(/-dialog$/, ''))) {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function entitySegmentFromPath(tsFilePath) {
|
|
291
|
+
const normalized = String(tsFilePath || '').replace(/\\/g, '/');
|
|
292
|
+
const match = normalized.match(/\/entities\/([^/]+)\//);
|
|
293
|
+
if (match) {
|
|
294
|
+
return match[1];
|
|
295
|
+
}
|
|
296
|
+
const pages = normalized.match(/\/pages\/([^/]+)\//);
|
|
297
|
+
return pages ? pages[1] : '';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* micro-functions module slug for cross-module SCREEN disambiguation.
|
|
302
|
+
* identityAccessModule → identity-access, referenceModule → reference
|
|
303
|
+
*/
|
|
304
|
+
function moduleSegmentFromPath(tsFilePath) {
|
|
305
|
+
const normalized = String(tsFilePath || '').replace(/\\/g, '/');
|
|
306
|
+
const match = normalized.match(/\/micro-functions\/([^/]+)\//);
|
|
307
|
+
if (!match) {
|
|
308
|
+
return '';
|
|
309
|
+
}
|
|
310
|
+
const raw = match[1].replace(/Module$/i, '');
|
|
311
|
+
return slugify(raw.replace(/([a-z0-9])([A-Z])/g, '$1-$2'));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Lower = keep the shared id when clones collide.
|
|
316
|
+
* Prefer Keycloak-stable identityAccess, then routed referenceModule, then domain modules.
|
|
317
|
+
*/
|
|
318
|
+
function duplicatePrimaryRank(tsFilePath) {
|
|
319
|
+
const normalized = String(tsFilePath || '').replace(/\\/g, '/');
|
|
320
|
+
if (normalized.includes('/identityAccessModule/')) {
|
|
321
|
+
return 0;
|
|
322
|
+
}
|
|
323
|
+
if (normalized.includes('/referenceModule/')) {
|
|
324
|
+
return 1;
|
|
325
|
+
}
|
|
326
|
+
if (normalized.includes('/warehouseModule/')) {
|
|
327
|
+
return 2;
|
|
328
|
+
}
|
|
329
|
+
if (normalized.includes('/millerModule/')) {
|
|
330
|
+
return 3;
|
|
331
|
+
}
|
|
332
|
+
if (normalized.includes('/paddy-selling-and-movement/')) {
|
|
333
|
+
return 4;
|
|
334
|
+
}
|
|
335
|
+
if (normalized.includes('/notificationModule/')) {
|
|
336
|
+
return 5;
|
|
337
|
+
}
|
|
338
|
+
if (normalized.includes('/dad-mock-module/')) {
|
|
339
|
+
return 9;
|
|
340
|
+
}
|
|
341
|
+
return 6;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* When multiple work items share a SCREEN id, keep one primary and rewrite the rest
|
|
346
|
+
* to path/module-unique ids. Never invent content titles — path/module only.
|
|
347
|
+
*
|
|
348
|
+
* @param {Array<{ tsFile: string, screenId: string }>} workItems mutated in place
|
|
349
|
+
* @param {object} pathModule node:path
|
|
350
|
+
* @returns {{ resolved: Array<{ from: string, to: string, tsFile: string, keptPrimary: boolean }>, remainingDuplicates: Record<string, string[]> }}
|
|
351
|
+
*/
|
|
352
|
+
function disambiguateDuplicateScreenIds(workItems, pathModule) {
|
|
353
|
+
const resolved = [];
|
|
354
|
+
const byId = new Map();
|
|
355
|
+
for (const item of workItems || []) {
|
|
356
|
+
if (!item?.screenId) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const list = byId.get(item.screenId) || [];
|
|
360
|
+
list.push(item);
|
|
361
|
+
byId.set(item.screenId, list);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const used = new Set(
|
|
365
|
+
[...byId.keys()].filter((id) => (byId.get(id) || []).length === 1)
|
|
366
|
+
);
|
|
367
|
+
// Primaries of duplicate groups also keep their id in `used`.
|
|
368
|
+
for (const [screenId, group] of byId.entries()) {
|
|
369
|
+
if (group.length <= 1) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
group.sort((a, b) => {
|
|
373
|
+
const rank = duplicatePrimaryRank(a.tsFile) - duplicatePrimaryRank(b.tsFile);
|
|
374
|
+
if (rank !== 0) {
|
|
375
|
+
return rank;
|
|
376
|
+
}
|
|
377
|
+
// Prefer a file that already embeds this id in source (Keycloak-stable).
|
|
378
|
+
const aHas = new RegExp(`SCREEN\\s*=\\s*['"]${screenId}['"]`).test(a.fileContent || '');
|
|
379
|
+
const bHas = new RegExp(`SCREEN\\s*=\\s*['"]${screenId}['"]`).test(b.fileContent || '');
|
|
380
|
+
if (aHas !== bHas) {
|
|
381
|
+
return aHas ? -1 : 1;
|
|
382
|
+
}
|
|
383
|
+
return String(a.tsFile).localeCompare(String(b.tsFile));
|
|
384
|
+
});
|
|
385
|
+
const primary = group[0];
|
|
386
|
+
used.add(primary.screenId);
|
|
387
|
+
resolved.push({
|
|
388
|
+
from: screenId,
|
|
389
|
+
to: screenId,
|
|
390
|
+
tsFile: primary.tsFile,
|
|
391
|
+
keptPrimary: true,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
for (let i = 1; i < group.length; i++) {
|
|
395
|
+
const item = group[i];
|
|
396
|
+
const pathId = deriveCanonicalScreenId(item.tsFile, pathModule);
|
|
397
|
+
const moduleSlug = moduleSegmentFromPath(item.tsFile);
|
|
398
|
+
const entity = slugify(entitySegmentFromPath(item.tsFile));
|
|
399
|
+
const candidates = [];
|
|
400
|
+
if (pathId) {
|
|
401
|
+
candidates.push(pathId);
|
|
402
|
+
}
|
|
403
|
+
if (moduleSlug && pathId && !pathId.startsWith(`${moduleSlug}-`)) {
|
|
404
|
+
candidates.push(`${moduleSlug}-${pathId}`);
|
|
405
|
+
}
|
|
406
|
+
if (moduleSlug && entity) {
|
|
407
|
+
const fileName = pathModule
|
|
408
|
+
.basename(item.tsFile, '.ts')
|
|
409
|
+
.replace(/\.component$/, '');
|
|
410
|
+
if (fileName && !fileName.startsWith(`${moduleSlug}-`)) {
|
|
411
|
+
candidates.push(`${moduleSlug}-${fileName}`);
|
|
412
|
+
}
|
|
413
|
+
candidates.push(`${moduleSlug}-${entity}-${detectViewSuffix(fileName) || 'screen'}`);
|
|
414
|
+
}
|
|
415
|
+
candidates.push(`${moduleSlug || 'dup'}-${pathId || 'screen'}-${i}`);
|
|
416
|
+
|
|
417
|
+
let next = '';
|
|
418
|
+
for (const candidate of candidates) {
|
|
419
|
+
if (candidate && !used.has(candidate)) {
|
|
420
|
+
next = candidate;
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (!next) {
|
|
425
|
+
next = `dup-${slugify(pathModule.basename(item.tsFile, '.ts'))}-${i}`;
|
|
426
|
+
}
|
|
427
|
+
// Final uniqueness guard
|
|
428
|
+
let unique = next;
|
|
429
|
+
let n = 2;
|
|
430
|
+
while (used.has(unique)) {
|
|
431
|
+
unique = `${next}-${n}`;
|
|
432
|
+
n++;
|
|
433
|
+
}
|
|
434
|
+
resolved.push({
|
|
435
|
+
from: screenId,
|
|
436
|
+
to: unique,
|
|
437
|
+
tsFile: item.tsFile,
|
|
438
|
+
keptPrimary: false,
|
|
439
|
+
});
|
|
440
|
+
item.screenId = unique;
|
|
441
|
+
used.add(unique);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const remainingDuplicates = {};
|
|
446
|
+
const recount = new Map();
|
|
447
|
+
for (const item of workItems || []) {
|
|
448
|
+
if (!item?.screenId) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
recount.set(item.screenId, (recount.get(item.screenId) || 0) + 1);
|
|
452
|
+
}
|
|
453
|
+
for (const item of workItems || []) {
|
|
454
|
+
if (!item?.screenId || (recount.get(item.screenId) || 0) <= 1) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const list = remainingDuplicates[item.screenId] || [];
|
|
458
|
+
list.push(item.tsFile);
|
|
459
|
+
remainingDuplicates[item.screenId] = list;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return { resolved: resolved.filter((r) => !r.keptPrimary || r.from !== r.to), remainingDuplicates, allResolutions: resolved };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Modules that keep path/filename SCREEN ids (no content-based rewrite). */
|
|
466
|
+
const CONTENT_ID_EXCLUDED_PATH_PARTS = [
|
|
467
|
+
'/dad-mock-module/',
|
|
468
|
+
'/accessControlModule/',
|
|
469
|
+
'/notificationModule/',
|
|
470
|
+
];
|
|
471
|
+
|
|
472
|
+
function isExcludedFromContentIds(tsFilePath) {
|
|
473
|
+
const normalized = String(tsFilePath || '').replace(/\\/g, '/');
|
|
474
|
+
return CONTENT_ID_EXCLUDED_PATH_PARTS.some((part) => normalized.includes(part));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Build a unique, human-readable screen id from page title + view type.
|
|
479
|
+
* Example: "Regional Office Registration" + list → regional-office-list
|
|
480
|
+
*/
|
|
481
|
+
function deriveContentScreenId(contentTitle, fileName, pathFallback) {
|
|
482
|
+
const view = detectViewSuffix(fileName);
|
|
483
|
+
let base = slugify(contentTitle);
|
|
484
|
+
if (!base) {
|
|
485
|
+
return pathFallback || '';
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Drop trailing view words already present in the title slug.
|
|
489
|
+
for (const suffix of VIEW_SUFFIXES) {
|
|
490
|
+
if (base.endsWith(`-${suffix}`)) {
|
|
491
|
+
base = base.slice(0, -(suffix.length + 1));
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Drop one trailing noise token (Registration, Portal, …).
|
|
497
|
+
const parts = base.split('-').filter(Boolean);
|
|
498
|
+
while (parts.length > 1 && TITLE_TRAILING_NOISE.has(parts[parts.length - 1])) {
|
|
499
|
+
parts.pop();
|
|
500
|
+
}
|
|
501
|
+
base = parts.join('-');
|
|
502
|
+
|
|
503
|
+
if (view && base) {
|
|
504
|
+
return `${base}-${view}`;
|
|
505
|
+
}
|
|
506
|
+
return base || pathFallback || '';
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Path-aware unique screen id. Prefer this over a bare filename so cloned
|
|
511
|
+
* entities (collecter / dad-mock / pages mirrors) do not collide.
|
|
512
|
+
*/
|
|
513
|
+
function deriveCanonicalScreenId(tsFilePath, pathModule) {
|
|
514
|
+
const normalized = tsFilePath.replace(/\\/g, '/');
|
|
515
|
+
const fileName = pathModule.basename(tsFilePath, '.ts').replace(/\.component$/, '');
|
|
516
|
+
|
|
517
|
+
const reportColPref = normalized.match(/\/pages\/reports\/([^/]+)\/list\/column-preferences-dialog/);
|
|
518
|
+
if (reportColPref) {
|
|
519
|
+
return `${reportColPref[1]}-column-preferences`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (fileName === 'region-floating-actions') {
|
|
523
|
+
return 'region-floating-actions';
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Service modules nested beneath the common micro-functions container.
|
|
527
|
+
const serviceEntity = normalized.match(
|
|
528
|
+
/\/modules\/apps\/micro-functions\/([^/]+)\/entities\/([^/]+)\//
|
|
529
|
+
);
|
|
530
|
+
if (serviceEntity) {
|
|
531
|
+
return `${serviceEntity[1]}/${serviceEntity[2]}`;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// CRUDForge Fuse entities: apps/{micro-functions|microservice}/{entity}/…
|
|
535
|
+
// Keep SCREEN aligned with nav ACL ids: `{folder}/{entity}`.
|
|
536
|
+
const crudforgeEntity = normalized.match(
|
|
537
|
+
/\/modules\/apps\/(micro-functions|microservice)\/([^/]+)\//
|
|
538
|
+
);
|
|
539
|
+
if (crudforgeEntity) {
|
|
540
|
+
return `${crudforgeEntity[1]}/${crudforgeEntity[2]}`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Identity Regional Office lives under entities/region but product ids are regional-office-*.
|
|
544
|
+
if (normalized.includes('/identityAccessModule/entities/region/')) {
|
|
545
|
+
const orgTree = normalized.match(/\/region-org-tree-drawer\/([^/]+)/);
|
|
546
|
+
if (orgTree) {
|
|
547
|
+
return `regional-office-${fileName.replace(/^region-/, '')}`;
|
|
548
|
+
}
|
|
549
|
+
if (fileName.startsWith('region-')) {
|
|
550
|
+
return `regional-office-${fileName.slice('region-'.length)}`;
|
|
551
|
+
}
|
|
552
|
+
if (fileName === 'region') {
|
|
553
|
+
return 'regional-office';
|
|
554
|
+
}
|
|
555
|
+
return fileName.startsWith('regional-office') ? fileName : `regional-office-${fileName}`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (normalized.includes('/modules/apps/pages/')) {
|
|
559
|
+
if (normalized.includes('/pages/reports/warehouse-capacity/')) {
|
|
560
|
+
return `report-${fileName}`;
|
|
561
|
+
}
|
|
562
|
+
if (
|
|
563
|
+
normalized.includes('/pages/stock-transfer/') ||
|
|
564
|
+
normalized.includes('/pages/paddy-selling-and-movement/') ||
|
|
565
|
+
normalized.includes('/pages/purchase-policy/') ||
|
|
566
|
+
normalized.includes('/pages/purchasing-rate-card/')
|
|
567
|
+
) {
|
|
568
|
+
return fileName.startsWith('page-') ? fileName : `page-${fileName}`;
|
|
569
|
+
}
|
|
570
|
+
// Brand / vendor nested under dashboard components (e.g. …/components/PMB/<card>/)
|
|
571
|
+
const nestedBrand = normalized.match(/\/components\/([^/]+)\/([^/]+)\//);
|
|
572
|
+
if (
|
|
573
|
+
nestedBrand &&
|
|
574
|
+
nestedBrand[2] === fileName &&
|
|
575
|
+
slugify(nestedBrand[1]) &&
|
|
576
|
+
slugify(nestedBrand[1]) !== fileName
|
|
577
|
+
) {
|
|
578
|
+
return `${slugify(nestedBrand[1])}-${fileName}`;
|
|
579
|
+
}
|
|
580
|
+
return fileName;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const orgTree = normalized.match(
|
|
584
|
+
/\/(region-org-tree-drawer|stakeholder-business-org-tree-drawer)\/([^/]+)/
|
|
585
|
+
);
|
|
586
|
+
if (orgTree) {
|
|
587
|
+
const scope = orgTree[1] === 'region-org-tree-drawer' ? 'regional-office' : 'stakeholder-business';
|
|
588
|
+
return `${scope}-${fileName}`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (normalized.includes('/dad-mock-module/entities/')) {
|
|
592
|
+
const entityMatch = normalized.match(/\/entities\/([^/]+)\//);
|
|
593
|
+
const entity = entityMatch ? entityMatch[1] : '';
|
|
594
|
+
if (['district', 'gn', 'region'].includes(entity)) {
|
|
595
|
+
return fileName.startsWith('dad-') ? fileName : `dad-${fileName}`;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const entityMatch = normalized.match(/\/entities\/([^/]+)\//);
|
|
600
|
+
if (entityMatch) {
|
|
601
|
+
const entity = entityMatch[1];
|
|
602
|
+
if (entity.endsWith('-collecter')) {
|
|
603
|
+
const baseEntity = entity.replace(/-collecter$/, '');
|
|
604
|
+
if (fileName === 'intake-status-dialog') {
|
|
605
|
+
return `${entity}-status-dialog`;
|
|
606
|
+
}
|
|
607
|
+
if (fileName === baseEntity || fileName.startsWith(`${baseEntity}-`)) {
|
|
608
|
+
return fileName.replace(new RegExp(`^${baseEntity}`), entity);
|
|
609
|
+
}
|
|
610
|
+
return `${entity}-${fileName}`;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return fileName;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Resolve screen id.
|
|
619
|
+
*
|
|
620
|
+
* Included modules (default when forceContentIds):
|
|
621
|
+
* heading + view → human id (rewrites existing SCREEN), else path-canonical
|
|
622
|
+
*
|
|
623
|
+
* Excluded modules (dad-mock / accessControl / notification):
|
|
624
|
+
* keep existing SCREEN, else path-canonical (never content rewrite)
|
|
625
|
+
*/
|
|
626
|
+
function deriveScreenId(fileContent, tsFilePath, className, pathModule, options = {}) {
|
|
627
|
+
const htmlContent = options.htmlContent || '';
|
|
628
|
+
const excluded = isExcludedFromContentIds(tsFilePath);
|
|
629
|
+
const forceContentIds = Boolean(options.forceContentIds) && !excluded;
|
|
630
|
+
const preferExisting = excluded || options.preferExistingScreen !== false;
|
|
631
|
+
const fileName = pathModule.basename(tsFilePath, '.ts').replace(/\.component$/, '');
|
|
632
|
+
const existing = (String(fileContent || '').match(/SCREEN\s*=\s*['"]([^'"]+)['"]/i) || [])[1];
|
|
633
|
+
const pathCanonical = deriveCanonicalScreenId(tsFilePath, pathModule);
|
|
634
|
+
const normalizedPath = String(tsFilePath || '').replace(/\\/g, '/');
|
|
635
|
+
|
|
636
|
+
// Identity Regional Office: product SCREEN ids are regional-office-* even when
|
|
637
|
+
// headings still say "Region Details" / similar.
|
|
638
|
+
if (normalizedPath.includes('/identityAccessModule/entities/region/')) {
|
|
639
|
+
return pathCanonical || existing || fileName;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const content = extractScreenContent(htmlContent);
|
|
643
|
+
const useContentTitle = !excluded && content.title && !isGenericContentTitle(content.title);
|
|
644
|
+
let contentId = useContentTitle
|
|
645
|
+
? deriveContentScreenId(content.title, fileName, pathCanonical)
|
|
646
|
+
: '';
|
|
647
|
+
|
|
648
|
+
// Optional disambiguator when the same heading appears on multiple entities.
|
|
649
|
+
if (options.disambiguateWithEntity && contentId) {
|
|
650
|
+
const entity = slugify(entitySegmentFromPath(tsFilePath));
|
|
651
|
+
if (entity && !contentId.startsWith(`${entity}-`)) {
|
|
652
|
+
contentId = `${entity}-${contentId}`;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Excluded / stable mode: never thrash Keycloak-facing ids.
|
|
657
|
+
if (excluded || (preferExisting && existing && !forceContentIds)) {
|
|
658
|
+
return existing || pathCanonical || fileName;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Rewrite / create human-readable ids from on-screen title.
|
|
662
|
+
if (forceContentIds) {
|
|
663
|
+
return contentId || pathCanonical || existing || fileName;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (!existing && contentId) {
|
|
667
|
+
return contentId;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (pathCanonical) {
|
|
671
|
+
return pathCanonical;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (existing) {
|
|
675
|
+
return existing;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (fileName) {
|
|
679
|
+
return fileName;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return className
|
|
683
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
684
|
+
.toLowerCase()
|
|
685
|
+
.replace(/-component$/, '');
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function extractRawAction(tagContent) {
|
|
689
|
+
const clickMatch = tagContent.match(/(?:\(click\)|on-click|\[click\])\s*=\s*["']([^"']+)["']/);
|
|
690
|
+
if (clickMatch) {
|
|
691
|
+
const raw = clickMatch[1].trim();
|
|
692
|
+
// Strip args: foo($event) / bar.emit() / baz(element)
|
|
693
|
+
const method = raw.replace(/\(.*$/, '').replace(/\.emit$/, '').trim();
|
|
694
|
+
if (method && !NOISE_ACTION_PATTERNS.some((re) => re.test(method.replace(/\s+/g, '')))) {
|
|
695
|
+
return camelToKebab(method);
|
|
696
|
+
}
|
|
697
|
+
if (NOISE_ACTION_PATTERNS.some((re) => re.test(raw.replace(/\s+/g, '')))) {
|
|
698
|
+
return { skip: true, reason: 'noise-click', raw };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const tooltipMatch = tagContent.match(/matTooltip\s*=\s*["']([^"']+)["']/i);
|
|
703
|
+
if (tooltipMatch) {
|
|
704
|
+
return slugify(tooltipMatch[1]);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const ariaLabelMatch = tagContent.match(/aria-label\s*=\s*["']([^"']+)["']/i);
|
|
708
|
+
if (ariaLabelMatch) {
|
|
709
|
+
return slugify(ariaLabelMatch[1]);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const routerMatch = tagContent.match(/routerLink\s*=\s*["']([^"']+)["']/i);
|
|
713
|
+
if (routerMatch) {
|
|
714
|
+
return slugify(routerMatch[1].replace(/^\//, '').replace(/\//g, '-'));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Button text between > and < on same-ish tag context is hard; use type/role hints
|
|
718
|
+
if (/\btype\s*=\s*["']submit["']/i.test(tagContent)) {
|
|
719
|
+
return 'submit';
|
|
720
|
+
}
|
|
721
|
+
if (/matMenuTriggerFor/i.test(tagContent)) {
|
|
722
|
+
return 'open-menu';
|
|
723
|
+
}
|
|
724
|
+
if (/matStepperNext/i.test(tagContent)) {
|
|
725
|
+
return 'next-step';
|
|
726
|
+
}
|
|
727
|
+
if (/matStepperPrevious/i.test(tagContent)) {
|
|
728
|
+
return 'prev-step';
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return '';
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function isNoiseAction(actionSlug) {
|
|
735
|
+
if (!actionSlug) {
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
if (NOISE_ACTIONS.has(actionSlug)) {
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
if (actionSlug.includes('stop-propagation')) {
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
if (actionSlug.includes('backdrop-click')) {
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Resolve a componentId (without qa- prefix).
|
|
752
|
+
* Returns null when the tag should be skipped.
|
|
753
|
+
*
|
|
754
|
+
* - rewriteIds=false: keep existing accessCtrl / data-component-id verbatim (stable)
|
|
755
|
+
* - rewriteIds=true: normalize legacy noisy ids to clean {verb} form
|
|
756
|
+
* - no existing id: generate clean semantic id for new injections
|
|
757
|
+
*/
|
|
758
|
+
function resolveComponentId(tagContent, screenId, options = {}) {
|
|
759
|
+
const { rewriteIds = false } = options;
|
|
760
|
+
|
|
761
|
+
// Prefer explicit data-action override
|
|
762
|
+
const dataAction = tagContent.match(/data-action\s*=\s*["']([^"']+)["']/i);
|
|
763
|
+
if (dataAction) {
|
|
764
|
+
return slugify(dataAction[1]);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const accessCtrlMatch = tagContent.match(/accessCtrl\s*:\s*['"]([^'"]+)['"]/);
|
|
768
|
+
if (accessCtrlMatch) {
|
|
769
|
+
const existing = accessCtrlMatch[1];
|
|
770
|
+
if (rewriteIds) {
|
|
771
|
+
return normalizeLegacyComponentId(existing, screenId);
|
|
772
|
+
}
|
|
773
|
+
// Still skip if the existing id is pure noise
|
|
774
|
+
const normalized = normalizeLegacyComponentId(existing, screenId);
|
|
775
|
+
if (normalized === null) {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
return existing.replace(/^qa-/, '');
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const dataComponentId = tagContent.match(/data-component-id\s*=\s*["']([^"']+)["']/i);
|
|
782
|
+
if (dataComponentId) {
|
|
783
|
+
const existing = dataComponentId[1];
|
|
784
|
+
if (rewriteIds) {
|
|
785
|
+
return normalizeLegacyComponentId(existing, screenId);
|
|
786
|
+
}
|
|
787
|
+
const normalized = normalizeLegacyComponentId(existing, screenId);
|
|
788
|
+
if (normalized === null) {
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
return existing.replace(/^qa-/, '');
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const raw = extractRawAction(tagContent);
|
|
795
|
+
if (raw && typeof raw === 'object' && raw.skip) {
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
let action = typeof raw === 'string' ? raw : '';
|
|
800
|
+
if (!action) {
|
|
801
|
+
const tagNameMatch = tagContent.match(/^<([a-zA-Z0-9-]+)/);
|
|
802
|
+
const tagName = tagNameMatch ? tagNameMatch[1].toLowerCase() : '';
|
|
803
|
+
if (tagName === 'a') {
|
|
804
|
+
action = 'link';
|
|
805
|
+
} else if (tagName === 'button' || /mat-(raised-|icon-|stroked-|flat-|mini-)?button/i.test(tagContent)) {
|
|
806
|
+
action = 'action';
|
|
807
|
+
} else {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
action = slugify(action).replace(/-(btn|button)$/, '');
|
|
813
|
+
if (isNoiseAction(action)) {
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (SEMANTIC_ACTION_MAP[action]) {
|
|
818
|
+
action = SEMANTIC_ACTION_MAP[action];
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Strip redundant screen fragment from action
|
|
822
|
+
const coreScreenId = screenId.replace(/-(list|form|filter|actions|detail|card)$/, '');
|
|
823
|
+
if (action.endsWith(`-${screenId}`)) {
|
|
824
|
+
action = action.slice(0, -(screenId.length + 1));
|
|
825
|
+
}
|
|
826
|
+
if (action.endsWith(`-${coreScreenId}`)) {
|
|
827
|
+
action = action.slice(0, -(coreScreenId.length + 1));
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
action = slugify(action) || 'action';
|
|
831
|
+
return action;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** Strip legacy `-screenId-btn` / `-N-btn` suffixes to the clean verb form. */
|
|
835
|
+
function normalizeLegacyComponentId(componentId, screenId) {
|
|
836
|
+
let id = String(componentId || '');
|
|
837
|
+
id = id.replace(/^qa-/, '');
|
|
838
|
+
id = id.replace(/-\d+-btn$/, '').replace(/-btn$/, '');
|
|
839
|
+
|
|
840
|
+
if (screenId && id.endsWith(`-${screenId}`)) {
|
|
841
|
+
id = id.slice(0, -(screenId.length + 1));
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const coreScreenId = screenId.replace(/-(list|form|filter|actions|detail|card)$/, '');
|
|
845
|
+
if (coreScreenId && id.endsWith(`-${coreScreenId}`)) {
|
|
846
|
+
id = id.slice(0, -(coreScreenId.length + 1));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
id = slugify(id);
|
|
850
|
+
if (SEMANTIC_ACTION_MAP[id]) {
|
|
851
|
+
id = SEMANTIC_ACTION_MAP[id];
|
|
852
|
+
}
|
|
853
|
+
if (isNoiseAction(id)) {
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
return id || null;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function toQaId(screenId, componentId) {
|
|
860
|
+
return `qa-${screenId}__${componentId}`;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function deduceComponentName(componentId, screenId) {
|
|
864
|
+
if (componentId === screenId) {
|
|
865
|
+
return 'Screen Access';
|
|
866
|
+
}
|
|
867
|
+
const lower = componentId.toLowerCase();
|
|
868
|
+
if (lower === 'create' || lower === 'add' || lower === 'new' || lower.includes('create')) return 'Create';
|
|
869
|
+
if (lower === 'edit' || lower.includes('edit') || lower.includes('update')) return 'Edit';
|
|
870
|
+
if (lower === 'delete' || lower.includes('delete') || lower.includes('remove')) return 'Delete';
|
|
871
|
+
if (lower.includes('reset') && lower.includes('password')) return 'Reset Password';
|
|
872
|
+
if (lower.includes('change') && lower.includes('password')) return 'Change Password';
|
|
873
|
+
if (lower === 'toggle-view' || lower === 'view-mode') return 'Toggle View';
|
|
874
|
+
if (lower === 'open-columns' || lower === 'columns') return 'Columns';
|
|
875
|
+
if (lower === 'view' || lower.includes('view') || lower.includes('detail')) return 'View';
|
|
876
|
+
if (lower === 'submit' || lower === 'save') return 'Save';
|
|
877
|
+
if (lower === 'cancel') return 'Cancel';
|
|
878
|
+
if (lower === 'open-filter' || lower === 'filter') return 'Filter';
|
|
879
|
+
if (lower.startsWith('export')) return 'Export';
|
|
880
|
+
|
|
881
|
+
return componentId
|
|
882
|
+
.split('-')
|
|
883
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
884
|
+
.join(' ');
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function deduceComponentType(componentId, screenId) {
|
|
888
|
+
if (componentId === screenId) {
|
|
889
|
+
return 'SCREEN';
|
|
890
|
+
}
|
|
891
|
+
if (componentId.toLowerCase().includes('panel')) {
|
|
892
|
+
return 'PANEL';
|
|
893
|
+
}
|
|
894
|
+
return 'BUTTON';
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function ensureUniqueId(baseId, usedIds) {
|
|
898
|
+
if (!usedIds.has(baseId)) {
|
|
899
|
+
usedIds.add(baseId);
|
|
900
|
+
return baseId;
|
|
901
|
+
}
|
|
902
|
+
let counter = 2;
|
|
903
|
+
let candidate = `${baseId}-${counter}`;
|
|
904
|
+
while (usedIds.has(candidate)) {
|
|
905
|
+
counter++;
|
|
906
|
+
candidate = `${baseId}-${counter}`;
|
|
907
|
+
}
|
|
908
|
+
usedIds.add(candidate);
|
|
909
|
+
return candidate;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function titleCaseFromSlug(slug) {
|
|
913
|
+
return String(slug || '')
|
|
914
|
+
.replace(/^i-/i, '')
|
|
915
|
+
.replace(/^(dad-|ref-|page-|report-)/, '')
|
|
916
|
+
.split('-')
|
|
917
|
+
.filter(Boolean)
|
|
918
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
919
|
+
.join(' ');
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function entityLabelFromScreenId(screenId) {
|
|
923
|
+
let id = String(screenId || '');
|
|
924
|
+
id = id.replace(/^(dad-|ref-|page-|report-)/, '');
|
|
925
|
+
id = id.replace(
|
|
926
|
+
/-(list|form|filter|actions|detail|card|form-page|drawer|dialog|page|section|panel|status-dialog|column-preferences)$/g,
|
|
927
|
+
''
|
|
928
|
+
);
|
|
929
|
+
id = id.replace(/-collecter$/, ' Collector');
|
|
930
|
+
return titleCaseFromSlug(id) || 'This feature';
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const VIEW_TYPE_PHRASES = {
|
|
934
|
+
list: 'master list screen used to search, browse, and manage records',
|
|
935
|
+
form: 'form screen used to create or edit a record',
|
|
936
|
+
'form-page': 'full-page form used to create or edit a record',
|
|
937
|
+
filter: 'filter panel used to narrow down records on this screen',
|
|
938
|
+
actions: 'toolbar actions for this screen (search, filter, create, view mode)',
|
|
939
|
+
detail: 'detail screen showing full information for a single record',
|
|
940
|
+
card: 'card view item for a single record in this feature',
|
|
941
|
+
drawer: 'side drawer related to this feature',
|
|
942
|
+
dialog: 'dialog used for a focused action in this feature',
|
|
943
|
+
page: 'application page for this feature',
|
|
944
|
+
section: 'section of a larger screen for this feature',
|
|
945
|
+
panel: 'panel within this feature',
|
|
946
|
+
'column-preferences': 'dialog to choose which table columns are visible',
|
|
947
|
+
'status-dialog': 'dialog to update or view status for this feature',
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Human-readable description for Permission Matrix (non-technical readers).
|
|
952
|
+
* When `contentTitle` is present (from the template heading), it is included
|
|
953
|
+
* so the matrix row matches what users see on the screen.
|
|
954
|
+
*/
|
|
955
|
+
function generateScreenDescription(screenId, screenName, contentTitle) {
|
|
956
|
+
const id = String(screenId || '');
|
|
957
|
+
const entity = entityLabelFromScreenId(id);
|
|
958
|
+
const friendlyName = String(screenName || '')
|
|
959
|
+
.replace(/^I-/, '')
|
|
960
|
+
.trim();
|
|
961
|
+
const onScreen = String(contentTitle || '').trim();
|
|
962
|
+
|
|
963
|
+
const prefixes = [];
|
|
964
|
+
if (id.startsWith('dad-')) {
|
|
965
|
+
prefixes.push('DAD mock');
|
|
966
|
+
}
|
|
967
|
+
if (id.startsWith('ref-')) {
|
|
968
|
+
prefixes.push('reference data');
|
|
969
|
+
}
|
|
970
|
+
if (id.startsWith('page-')) {
|
|
971
|
+
prefixes.push('custom page');
|
|
972
|
+
}
|
|
973
|
+
if (id.startsWith('report-')) {
|
|
974
|
+
prefixes.push('report');
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
let viewKey = '';
|
|
978
|
+
for (const key of Object.keys(VIEW_TYPE_PHRASES)) {
|
|
979
|
+
if (id.endsWith(`-${key}`) || id === key) {
|
|
980
|
+
viewKey = key;
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const scope = prefixes.length ? `${prefixes.join(' ')} ` : '';
|
|
986
|
+
const viewPhrase = VIEW_TYPE_PHRASES[viewKey];
|
|
987
|
+
const headline = onScreen || friendlyName || entity;
|
|
988
|
+
|
|
989
|
+
if (viewPhrase) {
|
|
990
|
+
if (onScreen) {
|
|
991
|
+
return `${scope}${onScreen} — ${viewPhrase}.`;
|
|
992
|
+
}
|
|
993
|
+
return `${scope}${entity}: ${viewPhrase}.`;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (onScreen) {
|
|
997
|
+
return `${scope}${onScreen} screen. Controls who can open and use this area.`;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (friendlyName && friendlyName.toLowerCase() !== entity.toLowerCase()) {
|
|
1001
|
+
return `${scope}${friendlyName} (${entity}) screen. Controls who can open and use this area.`;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return `${scope}${headline} screen. Controls who can open and use this area.`;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function indefiniteArticle(noun) {
|
|
1008
|
+
const word = String(noun || '').trim();
|
|
1009
|
+
return /^[aeiou]/i.test(word) ? 'an' : 'a';
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function generateComponentDescription(componentId, componentName, componentType, screenId) {
|
|
1013
|
+
if (componentType === 'SCREEN' || componentId === screenId) {
|
|
1014
|
+
return generateScreenDescription(screenId, '');
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const action = String(componentName || titleCaseFromSlug(componentId) || 'Action').trim();
|
|
1018
|
+
const entity = entityLabelFromScreenId(screenId);
|
|
1019
|
+
const article = indefiniteArticle(entity);
|
|
1020
|
+
const lower = String(componentId || '').toLowerCase();
|
|
1021
|
+
|
|
1022
|
+
if (lower === 'create' || lower === 'add') {
|
|
1023
|
+
return `Allows creating ${article} new ${entity} record.`;
|
|
1024
|
+
}
|
|
1025
|
+
if (lower === 'edit' || lower.startsWith('edit')) {
|
|
1026
|
+
return `Allows editing an existing ${entity} record.`;
|
|
1027
|
+
}
|
|
1028
|
+
if (lower === 'delete' || lower.startsWith('delete') || lower.startsWith('remove')) {
|
|
1029
|
+
return `Allows deleting ${article} ${entity} record.`;
|
|
1030
|
+
}
|
|
1031
|
+
if (lower === 'view' || lower.startsWith('view')) {
|
|
1032
|
+
return `Allows viewing ${entity} details.`;
|
|
1033
|
+
}
|
|
1034
|
+
if (lower === 'submit' || lower === 'save') {
|
|
1035
|
+
return `Allows saving changes on the ${entity} form.`;
|
|
1036
|
+
}
|
|
1037
|
+
if (lower === 'cancel') {
|
|
1038
|
+
return `Allows cancelling and closing the ${entity} form without saving.`;
|
|
1039
|
+
}
|
|
1040
|
+
if (lower === 'open-filter') {
|
|
1041
|
+
return `Allows opening filters for the ${entity} list.`;
|
|
1042
|
+
}
|
|
1043
|
+
if (lower.startsWith('export')) {
|
|
1044
|
+
return `Allows exporting ${entity} data.`;
|
|
1045
|
+
}
|
|
1046
|
+
if (lower === 'toggle-view') {
|
|
1047
|
+
return `Allows switching between table and card view for ${entity}.`;
|
|
1048
|
+
}
|
|
1049
|
+
if (lower === 'reset-columns') {
|
|
1050
|
+
return `Allows resetting visible table columns for ${entity}.`;
|
|
1051
|
+
}
|
|
1052
|
+
if (lower === 'clear-filters' || lower === 'clear-field' || lower === 'close-filter') {
|
|
1053
|
+
return `Allows managing filters on the ${entity} screen.`;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return `Allows the "${action}" action on the ${entity} screen.`;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
module.exports = {
|
|
1060
|
+
GENERIC_SCREEN_SEGMENTS,
|
|
1061
|
+
NOISE_ACTIONS,
|
|
1062
|
+
SEMANTIC_ACTION_MAP,
|
|
1063
|
+
VIEW_SUFFIXES,
|
|
1064
|
+
TITLE_TRAILING_NOISE,
|
|
1065
|
+
GENERIC_CONTENT_TITLES,
|
|
1066
|
+
CONTENT_ID_EXCLUDED_PATH_PARTS,
|
|
1067
|
+
slugify,
|
|
1068
|
+
isGenericContentTitle,
|
|
1069
|
+
entitySegmentFromPath,
|
|
1070
|
+
moduleSegmentFromPath,
|
|
1071
|
+
duplicatePrimaryRank,
|
|
1072
|
+
disambiguateDuplicateScreenIds,
|
|
1073
|
+
camelToKebab,
|
|
1074
|
+
resolveMeaningfulSegment,
|
|
1075
|
+
formatScreenName,
|
|
1076
|
+
formatScreenNameFromPath,
|
|
1077
|
+
extractScreenContent,
|
|
1078
|
+
detectViewSuffix,
|
|
1079
|
+
isExcludedFromContentIds,
|
|
1080
|
+
deriveContentScreenId,
|
|
1081
|
+
deriveCanonicalScreenId,
|
|
1082
|
+
deriveScreenId,
|
|
1083
|
+
extractRawAction,
|
|
1084
|
+
isNoiseAction,
|
|
1085
|
+
resolveComponentId,
|
|
1086
|
+
normalizeLegacyComponentId,
|
|
1087
|
+
toQaId,
|
|
1088
|
+
deduceComponentName,
|
|
1089
|
+
deduceComponentType,
|
|
1090
|
+
ensureUniqueId,
|
|
1091
|
+
titleCaseFromSlug,
|
|
1092
|
+
entityLabelFromScreenId,
|
|
1093
|
+
generateScreenDescription,
|
|
1094
|
+
generateComponentDescription,
|
|
1095
|
+
};
|