Compare commits

...
137 Commits
Author SHA1 Message Date
james.maes 035e2df40e Merge pull request 'ci: build on the shared ci-workflows workflow' (#179) from ci/ci-workflows into develop
maven / build (push) Successful in 17m55s
build / maven (push) Successful in 17m55s
2026-11-07 23:57:24 +00:00
James MaesandClaude Opus 5.5 3ac26b2bac ci: run the Selenium ITs headless against the dev server, as the orb did
maven / build (push) Successful in 15m19s
build / maven (push) Successful in 15m19s
The Selenium ITs need a headless Chrome and the HTTPS React dev server on
:3001. Move the prebuild into scripts/ci-prebuild.sh, which also starts the
dev server (HTTPS, port 3001, backend proxy 8001) the way the qqq-orb
frontend-material pipeline did, and treat CI=true like QQQ_SELENIUM_HEADLESS
(and like CIRCLECI for waitForever) so the tests run headless on any CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 17:41:38 -06:00
James MaesandClaude Opus 5.5 31bc2145cc ci: build on the shared ci-workflows Maven workflow
maven / build (push) Failing after 8m53s
build / maven (push) Failing after 8m53s
Add .gitea/workflows/build.yaml calling KofTwentyTwo/ci-workflows maven.yaml@v1:
npm ci, the Jest unit tests and the React build (packaged into the jar) run as
the prebuild, then Maven runs the Selenium ITs on the Chrome-capable runners and
publishes to the Gitea Maven registry from develop/release/hotfix/tags.

Remove the GPG signing and Sonatype Central plugins from the default build
(Gitea Maven only; they remain in the release profile) and the old CircleCI
orb configuration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 17:30:35 -06:00
forge-recovery 21a2a71d7b Merge pull request 'ci: baseline reviewed historical fixture findings' (#178) from ci/historical-finding-baseline-20261101 into develop
QRun-IO/ci-policy: ci / not-enrolled (push) Failing after 5s
QRun-IO/ci-policy: ci / ci-result (push) Failing after 4s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / java-maven-2 (push) Skipped
QRun-IO/ci-policy: ci / node-4 (push) Skipped
QRun-IO/ci-policy: ci / source-config-5 (push) Skipped
QRun-IO/ci-policy: ci / gradle-0 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-1 (push) Skipped
QRun-IO/ci-policy: ci / node-3 (push) Skipped
2026-11-02 01:46:33 +00:00
forge-recoveryandOpenAI GPT-6 f02a6208f8 ci: baseline reviewed historical fixture findings
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-2 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Failing after 4s
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 5s
Co-Authored-By: OpenAI GPT-6 <noreply@openai.com>
2026-11-02 01:13:21 +00:00
james.maesandCodex 5d67c059ff ci: select CodeQL languages from branch sources
Preserve the reviewed one-file develop CodeQL caller and its existing Java/JavaScript/Python/Actions source coverage; scanner-only scope, no package publication or deployment.

Co-authored-by: Codex (GPT-6) <noreply@openai.com>
2026-10-30 16:02:17 -05:00
james.maesandCodex 88cf2aa6ff ci: adopt managed orb with verified browser bootstrap (#171)
* ci: qualify shared browser bootstrap

Co-Authored-By: Codex <noreply@openai.com>

* ci: use qualified stable browser bootstrap

Co-Authored-By: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
2026-10-21 16:08:07 -05:00
james.maesandCodex 781df9086e ci: adopt shared orb 0.7.18 (#170)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 15:16:19 -05:00
james.maesandCodex e049527ff2 ci: adopt central orb 0.7.12 and stable gates (#169)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 09:02:59 -05:00
james.maesandCodex 4a754f5683 ci: adopt shared orb 0.7.11 (#168)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 01:07:52 -05:00
james.maesandCodex 5dbcf7af63 ci: delegate pipelines to shared QQQ orb (#167)
Co-authored-by: Codex <noreply@openai.com>
2026-10-20 19:58:23 -05:00
james.maes aa6a490e41 Merge pull request #164 from QRun-IO/feature/orb-0.6.9
ci: qqq-orb 0.6.9 (refresh SNAPSHOT dependencies)
2026-10-14 10:47:03 -05:00
james.maesandClaude Opus 5.5 1a2f4a3a07 ci: qqq-orb 0.6.9 (refresh SNAPSHOT dependencies)
Moves qqq-orb@0.6.6 to qqq-orb@0.6.9, which passes --update-snapshots so cached SNAPSHOT dependencies are re-checked on every build (QRun-IO/qqq-orb#22).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:08:49 -05:00
james.maes f17be9024b Merge pull request #163 from QRun-IO/feature/GH-923-material-packaged-notices
fix(#942): include project notices in published jars
2026-09-30 23:42:20 -05:00
james.maes 738cfbb261 fix(#942): include project notices in published jars 2026-09-30 23:24:58 -05:00
james.maes a37ead3b8c Merge pull request #162 from QRun-IO/feature/GH-940-material-dependency-refresh
fix(deps): patch Jackson, Axios and dev middleware
2026-09-30 22:52:28 -05:00
james.maes 894f5044d6 fix(deps): consume verified frontend-core snapshot
Pin public 0.40.20-SNAPSHOT after integrated checks for QQQ #902 and #940.
2026-09-30 22:34:32 -05:00
james.maes 090209bf26 fix(deps): patch Jackson, Axios and dev middleware
Address QQQ #939 and #940 with verified dependency updates.
2026-09-30 22:00:56 -05:00
james.maes a6958cf313 fix(ci): exclude RC tags from stable publication (#160)
Limit stable publication to exact version tags; preserve branch-based candidates and hotfixes. Refs QRun-IO/qqq#938.
2026-09-30 21:33:54 -05:00
james.maes 759aa4aa50 fix: align Apache-2.0 and await saved-view transitions (#159)
Align first-party license declarations and wait for saved-view background transitions before existing assertions. Refs QRun-IO/qqq#923 and QRun-IO/qqq#937.
2026-09-30 21:17:16 -05:00
james.maes f9124fb7d2 Merge pull request #157 from QRun-IO/feature/GH-892-security
fix: remediate Material security findings and warn about live script tests
2026-09-29 16:09:28 -05:00
james.maes 48e6c72572 fix(#902): consume published frontend-core security fixes 2026-09-29 15:49:10 -05:00
james.maes 865dd7fd70 fix(#849): warn about live script tests and pin safe publisher 2026-09-29 13:43:57 -05:00
james.maes b126391a25 Merge pull request #158 from QRun-IO/feature/GH-892-codeql
fix(security): validate scopes and process defaults (#892)
2026-09-29 13:23:50 -05:00
james.maes 6a9d00fe63 fix(security): validate scopes and isolate process defaults
Refs QRun-IO/qqq#892. Preserve scope boundaries and prevent URL-supplied defaults from changing object prototypes.
2026-09-29 12:09:00 -05:00
james.maes 6b35fa574b fix(security): patch Material dependency graph
Refs QRun-IO/qqq#892 and QRun-IO/qqq#901. Keep the existing dashboard build while resolving High/Critical dependency findings.
2026-09-29 12:03:56 -05:00
james.maes 8ec1be7ad0 fix(frontend): finalize QQQ 4 compatibility (#155)
Make the Material dashboard compatible with QQQ 4.0: migrate Java fixtures to Java 21/Javalin 7, preserve joined-read permissions, and fix disabled inputs and empty or denied widget rendering. Release CI also keeps package installation unattended.

Validation: 22 Java metadata tests, 48 JavaScript tests, typecheck/production build and the owned sample browser workflows pass. The tracked QQQ 4.0.0-RC.3 dependency is now public, with all 18 coordinates and 66 artifact signatures verified. Full feature CI1561 at a8d3c931 passes compilation, the production React build and all 121 Java/browser cases with zero failures, errors or skips. GitHub dependency submission and CodeQL also pass. The subsequent release/0.41.0 workflow publishes the dashboard RC for clean-cache first-party acceptance.

Refs QRun-IO/qqq#498, QRun-IO/qqq#537, QRun-IO/qqq#552, QRun-IO/qqq#558, QRun-IO/qqq#617. Deferred coverage and Medium/Low findings remain in QRun-IO/qqq#534.
2026-09-24 09:20:23 -05:00
James MaesandClaude Opus 4.8 2411a5fc90 docs: add CLAUDE.md pointer to QQQ knowledge-base dossier + hub
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 05:31:48 -05:00
james.maesandClaude Opus 4.7 628e36f64e chore: add CODEOWNERS pointing to sole maintainer
Architecture audit pass 2026-05-21. Minimal CODEOWNERS file
making the implicit ownership explicit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:13:49 -05:00
james.maesandClaude Opus 4.7 781e3d5482 docs(README): note qqq-frontend-next as future direction
Adds a restrained banner pointing to qqq-frontend-next as the
planned successor (Next.js 15 + Tailwind 4 + shadcn/ui). This
repo remains supported until existing qqq apps complete
migration. Surface the direction without disrupting in-flight
work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:12:48 -05:00
james.maes f7633ee123 chore: bump version to 0.41.0-SNAPSHOT 2026-03-29 17:59:06 -05:00
james.maes 7fe82c7abc chore: merge release/0.40.0 back to develop 2026-03-29 17:58:18 -05:00
james.maes 47d3cb3fe1 chore: bump version to 0.40.0 2026-03-29 17:57:57 -05:00
CircleCI 149ff7ff79 Bump version to 0.40.0-RC.2 [skip ci] 2026-03-29 22:54:21 +00:00
james.maes 2a404ea3c8 chore: bump to 0.40.0-RC.1, pin qqq 0.40.0 2026-03-29 17:38:18 -05:00
james.maes d31db14dd6 chore: merge main into release branch (incorporate license + CHANGELOG) 2026-03-29 17:37:34 -05:00
james.maes 22a7866b7c fix(oauth2): prefer externalBaseUrl for OIDC authority (#419) 2026-03-27 15:07:02 -05:00
Tim Chamberlain 23ed6f4a87 chore: updated qqq-frontend-core version 2026-03-26 14:02:30 -05:00
Tim Chamberlain 3400004d66 Merge branch 'feature/analytics-updates' into develop 2026-03-25 14:15:00 -05:00
Tim Chamberlain 1b83a98a4b fix(analytics): preserve PostHog proxy asset path 2026-03-25 14:12:04 -05:00
Tim ChamberlainandDarin Kelkhoff 078955f529 feat: add analytics provider framework and PostHog support (#151)
* feat(analytics): add pluggable provider registry and posthog support

* updates to identification

* tweaks to posthog analytics provider

* fix: clean up analytics identity naming and harden PostHog host handling

* fix: add analyticsValues compatibility accessor

---------

Co-authored-by: Darin Kelkhoff <darin.kelkhoff@gmail.com>
2026-03-24 17:13:57 -05:00
Tim Chamberlain b75dd26808 fix: add analyticsValues compatibility accessor 2026-03-24 16:46:09 -05:00
Darin Kelkhoff 6dc44ee290 Merged develop into feature/analytics-updates 2026-03-24 15:02:43 -05:00
Darin Kelkhoff 193f848a2c Merge pull request #152 from QRun-IO/feature/additional-css-hooks
feat: add qqqId props to process buttons and CSS class hooks to app home
2026-03-24 14:59:49 -05:00
Tim Chamberlain 7c33fe7773 fix: clean up analytics identity naming and harden PostHog host handling 2026-03-24 12:58:22 -05:00
Darin KelkhoffandClaude Opus 4.6 1445634a86 feat: add qqqId props to process buttons and CSS class hooks to app home
Add qqqId prop to QCancelButton, QSubmitButton, and QAlternateButton
for test selector support via data-qqq-id attributes. Wire contextual
IDs through ProcessRun (next/submit, close/cancel/return, back).

Add className hooks to Home.tsx for appHome containers and big-icon
permission-denied boxes. Remove unused colors import from ProcessRun.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:29:41 -05:00
Tim Chamberlain 442cc85a93 tweaks to posthog analytics provider 2026-03-16 13:58:51 -05:00
Tim Chamberlain 372e87ae96 updates to identification 2026-03-14 22:34:06 -05:00
Darin Kelkhoff 31e2bc712e Merged develop into feature/analytics-updates 2026-03-09 14:08:08 -05:00
Darin Kelkhoff 90aa19bb44 Merged feature/field-functions into develop 2026-03-06 20:12:44 -06:00
Darin Kelkhoff 7170852139 chore: update qqq-frontend-core to 0.40.10 2026-03-06 19:53:18 -06:00
Tim Chamberlain d109b9ca63 feat(analytics): add pluggable provider registry and posthog support 2026-03-06 17:22:00 -06:00
Tim ChamberlainandDarin Kelkhoff f2f0c02621 feat(record-view): support global action placement and inline title controls (#148)
* feat(record-view): support global action placement and inline title controls

* fix(pageHeader): remove itemsShownInActionsMenu from dependency list in pageHeaderRecordViewActions useMemo - is always a new object, so always causing re-renders

* fix(filterPossibleValues): pass fieldTable (e.g., potentially a join table) down into the DynamicFormUtils.addPossibleValuePropsToSingleField call before getDisplayValues

* removed console debug

---------

Co-authored-by: Darin Kelkhoff <darin.kelkhoff@gmail.com>
2026-03-06 17:18:17 -06:00
Darin Kelkhoff b745aa84a3 fix(tableDevView): Add useEffects around code that was always running, making the page hang if user changed dropdowns 2026-03-05 12:25:34 -06:00
Darin Kelkhoff 512d8cf85a fix(fieldListMenu): prevent options from being duplicated in the list 2026-03-05 12:25:02 -06:00
Darin Kelkhoff 44538c5289 feat(dayOfWeek): final work on day-of-week filtering
- add meta-data to control day-of-week filters;
- fix display of day-of-week filters in filter previews
2026-03-05 12:24:11 -06:00
Darin Kelkhoff 0aec20513f test(fieldFunctions): integration test for field functions on query screen 2026-03-02 21:18:17 -06:00
Darin Kelkhoff 451127a7d0 feat(fieldFunctions): add support for day-is-any/none filtering on all date/date-time fields, using fieldFunctions; and support for virtual fields on query screen, if they are marked as query selectable or criteria (able) 2026-03-02 16:25:07 -06:00
Darin Kelkhoff 3ab24d33aa fix(collapsibleElements): add classes and css rule to make collapsed widgets on record view screens not full-height (so if they're next to a full-height section of fields they don't look brokenly empty) 2026-02-25 15:16:10 -06:00
Darin Kelkhoff abc8ff0e3d fix(themePlusQuickView): add data-button-state to buttons for targeted css based on clean/dirty/empty state 2026-02-23 12:03:16 -06:00
Darin Kelkhoff a5aaf007f8 Merge pull request #146 from QRun-IO/feature/fix-visual-regressions-128-v2-with-develop-20260218
Feature/fix visual regressions 128 v2 with develop 20260218
2026-02-20 09:39:51 -06:00
Darin Kelkhoff 53a43f4012 fix(themes): cleanup from code review feedback 2026-02-19 20:55:26 -06:00
Darin Kelkhoff d228d3d827 test(themes): renaming borderRadius to borderRadiusGlobal; fixing flower boxes 2026-02-18 12:25:04 -06:00
Darin Kelkhoff 2144b74c16 fix(theme): migrate QThemeMetaData to MaterialDashboardThemeMetaData 2026-02-18 11:57:00 -06:00
Darin Kelkhoff ab0e3b5a6d chore: set @qrunio/qqq-frontend-core to 0.40.9-SNAPSHOT (theme file moved out of core) 2026-02-18 11:48:22 -06:00
Darin Kelkhoff 4feccf38a2 chore: fix version of log4j-api dep to match log4j-core (which is required for log4j to work) 2026-02-18 11:22:27 -06:00
Darin Kelkhoff 2d8ea390f6 docs: fix wrong word in class header comment 2026-02-18 11:21:33 -06:00
Darin Kelkhoff 7ec0921964 chore: remove unintentional commit 2026-02-18 11:21:13 -06:00
Darin Kelkhoff d718c50682 feat(themes): Migrate theme from QThemeMetaData to MaterialDashboardThemeMetaData; restore code deleted in develop's regression commit, such as data-qqq-ids and usage of var(--qqq) expressions for colors) (which were removed from this feature branch when develop got merged in here). 2026-02-18 11:19:53 -06:00
Darin Kelkhoff b4bed88a6e Merged develop into feature/fix-visual-regressions-128-v2 2026-02-18 08:00:06 -06:00
Darin Kelkhoff 1ceecd734d fix(collapsibleElements): fix to show label box if the only thing that would be in it is a collapsible-control 2026-02-16 20:49:06 -06:00
Darin Kelkhoff 00fbd04bb3 Merge pull request #145 from QRun-IO/feature/fix-process-init-to-post-not-get
Feature/fix process init to post not get
2026-02-13 16:55:32 -06:00
Darin Kelkhoff 16520db1e1 test(processes): updated for new process that was added (PersonBasicETLProcessMetaDataProducer) - to keep testActionMenuWithChanges working as it did (all custom processes at the top of the menu) 2026-02-13 09:57:56 -06:00
Darin Kelkhoff 2487364557 test(processes): new IT on launching processes in various ways.
Added with a regression test that failed before ProcessRun.tsx was updated to POST instead of GET, for a large filter.

This includes a new default StreamedETL process in the metadata package for Selenium with QApplication tests, as well as updates to customizeQInstanceViaTestMethodTagSpecifyingCustomizeQInstanceMethodName and setupTestDataViaTestMethodTagSpecifyingSetupTestDataMethodName to return boolean - true if they ran a tag-specified method, false otherwise - so a test's customize/setup methods can read that as a signal, e.g., to do some default setup if they want.
2026-02-13 08:53:52 -06:00
Darin Kelkhoff 2539d7ca34 fix(processes): change call to processInit to use formData (so it does a POST) instead of queryString (doing a GET).
This should fix cases where a larger filter (or list of primary keys) cause an http 414 error (request URI too long).

qController.processInit already knew how to take a formData object and do a post, as does the backend - so this is just updating ProcessRun to "do it right".

And, processStep is already doing a post, so, nothing to change there.
2026-02-13 08:50:52 -06:00
Darin Kelkhoff d5d992b8ba feat(themes): Introduce concept of preferInfoColorToPrimaryColor - for components that have historically been color=info, but probably should have always been color=primary. 2026-02-10 16:26:02 -06:00
Darin Kelkhoff a6a44d3523 feat(themes): adjust fontSizeBase from 20px to 14px.
This is believed to be a reversion to the previous default, from before initial theme work.

We're doing thsi to make the record-view screen line-heights go back to what they were pre-theme.

it's not 100% clear what else is impacted by this, though no changes have been noticed after making it, so, it feels okay.
2026-02-10 16:21:58 -06:00
Darin Kelkhoff fea8781ae2 feat(themes): change delete icon to use color=error; fix alignment between select & save icon (just an old bug, not theme-related)
(previous code in here to set color on the button was being overridden by css that set primary color on buttons it looks like)
2026-02-10 16:13:58 -06:00
Darin Kelkhoff 59159ae19d feat(themes): Make the query-screen views, columns, and quick-filter buttons behave better with themes - preserving legacy appearances, and reacting to accent color & accent color light 2026-02-10 16:13:58 -06:00
Darin Kelkhoff 9b4f4d27be feat(themes): add attribute data-button-variant={variant}, for CSS targeting, to exclude setting primary text color on contained & gradient buttons (dark on dark) 2026-02-10 16:13:57 -06:00
Darin Kelkhoff 86a2ddf029 feat(themes): rename border-radius to border-radius-global 2026-02-10 16:13:57 -06:00
Darin Kelkhoff c6076960bc feat(themes): change delete button to use color=error instead of "primary"
Error is typically a red tone, indicative of danger, which delete should indicate.  primary is red in the default theme, but probably not in most application themes, and primary should not be used for destructive delete action.
2026-02-10 16:13:57 -06:00
Darin Kelkhoff 253f5bf913 feat(themes): change actionsMenuButton to not change to "contained" (which turns it solid black) when opened.
This isn't actually a theme-related change, but rather, just a "i think this looks better"...
2026-02-10 16:13:57 -06:00
Darin Kelkhoff 7bda01ea3f feat(themes): Fix MuiTabs indicator to set borderBottomColor, not backgroundColor. 2026-02-10 16:13:57 -06:00
Darin Kelkhoff 20e6f51b86 feat(themes): add MuiAlert-root to the exclusion list (:not selectors) for MuiPaper background color (so alerts have background color based on severity) 2026-02-10 16:13:57 -06:00
Darin Kelkhoff 4ec7291e0e feat(themes): Add gradients to theme options palette, for info & primary, using a css color-mix to lighten the theme colors. 2026-02-10 16:13:57 -06:00
Darin Kelkhoff 27347370d8 feat(themes): Update to support multiple border-radius fields on QThemeMetaData, each with different defaults (as per original implementation).
If an app desires uniform radii, it can set borderRadiusGlobal on the theme object. Alternatively, an app can choose to scale the default radii via borderRadiusScale.  In both of these cases, an individual element's value would take precedence.
2026-02-10 16:13:57 -06:00
Darin Kelkhoff be59794e34 feat(themes): add setting accentColorLight in QContext from brandingMetaData 2026-02-10 16:13:06 -06:00
Darin Kelkhoff f01ecee21a feat(themes): add support for gravatarDefault setting on QBrandingMetaData 2026-02-10 16:13:06 -06:00
Darin Kelkhoff 561320062e feat(themes): add data-qqq-sidenav-item-type attribute to user-profile and top-level-parent-app elements.
tried adding it for others, but it failed to pass all the way through to the final element, so leaving those out for now.
2026-02-10 16:13:06 -06:00
Tim Chamberlain fc7612282b updated qqq-frontend-core version 2026-02-10 12:31:03 -06:00
Tim Chamberlain b1ebefa756 Merge branch 'feature/custom-apps-qbit' into develop 2026-02-10 12:16:53 -06:00
Tim Chamberlain 2a047fc0fc minor css changes for widget paddings 2026-02-10 12:14:17 -06:00
Darin Kelkhoff 633d4eb071 Merge pull request #140 from QRun-IO/feature/collapsible-sections-and-widgets
Feature/collapsible sections and widgets
2026-02-10 08:48:24 -06:00
Darin Kelkhoff cf2448dfd1 Merge pull request #138 from QRun-IO/feature/detect-base-path-bug-from-root
fix(paths): update detectBasePath to use base-href as strategy 1
2026-02-10 08:47:28 -06:00
Darin Kelkhoff 625414a761 chore: update playwrite image to v1.58.2; drop .1 patch pin on qqq-orb 2026-02-10 08:25:40 -06:00
Darin Kelkhoff e6f599be23 chore: Update qqq-frontend-core to 0.40.7-SNAPSHOT 2026-02-10 08:02:28 -06:00
Tim Chamberlain f0df995e48 added help content to apps 2026-02-06 14:10:35 -06:00
Darin Kelkhoff afa3ed5481 test(paths): add test on cached/memoized responses 2026-02-05 20:58:22 -06:00
Darin Kelkhoff c6ea508d2e fix(paths): memoize/cache results of detectBasePath, so if src-tag method is being used, results won't change if react modifies the dom (injects chunks?); fixed test that needed logic flipped with previous change 2026-02-05 20:53:03 -06:00
Darin Kelkhoff f5d3e21a0d test(collapsibleElements): IT for record view screen with collapsible sections and widget.
- updated test utility methods
2026-02-05 15:45:55 -06:00
Darin Kelkhoff 7fe7ed247e feat(collapsibleElements): add support for widgets being collapsible.
makes the whole "label box" clickable to toggle; adds an icon button on the right; reads/writes local storage, etc.
2026-02-05 15:44:38 -06:00
Darin Kelkhoff 0ab5a93367 feat(collapsibleElements): change the useEffect deps array for when screen=recordView, to avoid constant re-rendering, e.g., upon scrolling.
This became an issue when we stopped pre-rendering widgets quite in the same way in RecordView, in support of collapsible sections.

also hit re-format code in intellij, so lots of spaces and semicolons...
2026-02-05 15:37:41 -06:00
Darin Kelkhoff 50aaa63be6 feat(collapsibleElements): add support for collapsible sections.
- including remembering their states in localStorage.
- required refactoring to render the sections "inline", instead of, they used to be pre-rendered and stored in state.  well, parts of them are still pre-rendered, but the grid, which contains the clickable collapse controls, and which hides them when they're collapsed - that's been moved.
- also moved around some of the record-view-menu code from recent feature.
2026-02-05 15:36:09 -06:00
Tim Chamberlain f7a2afa099 Merge branch 'develop' into feature/custom-apps-qbit 2026-02-04 09:48:39 -06:00
Darin Kelkhoff d9ebd24531 Merge pull request #139 from QRun-IO/feature/quick-views
Feature/quick views
2026-02-03 21:41:34 -06:00
Darin Kelkhoff c1ab31926d test(quickSavedViews): fix to put a label on the test quick views 2026-02-03 18:30:32 -06:00
Darin Kelkhoff ff4acdaa0a chore: update to qqq 0.40.0-SNAPSHOT (moving off feature-pin for merger to develop) 2026-02-03 17:03:27 -06:00
Darin Kelkhoff 7b80d5c8af fix(savedViews): feedback from code review/analysis
- in useSavedViews, remove monitoring of location from the useEffect that calls loadSavedViews - this would fire any time the selected view was changed, but we shouldn't need to refetch the list of views in that use case - table & global meta data changes should be sufficient (e.g., to know if navigated to another table)
- catch errors from async count calls, and display error icon instead of count in that scenario
- use functional state updater to set counts (and errors) by view id (previous approach was working, though this seems the more correct way)
-
2026-02-03 16:11:31 -06:00
Darin Kelkhoff 2cc03b2d7f fix: Remove unused import 2026-02-02 21:15:37 -06:00
Tim Chamberlain 14da225a6e added looker request handler (probably rolls back at some point) 2026-01-31 20:50:52 -06:00
Tim Chamberlain 61f63cc0e1 looker updates 2026-01-31 20:25:28 -06:00
Tim Chamberlain c3c9172292 asked llm to temporarily disable playwright 2026-01-30 18:29:49 -06:00
Tim Chamberlain c0fd06d7b7 updated frontend-core version 2026-01-30 12:08:14 -06:00
Tim Chamberlain 520728ddc3 updates to mat-dash to support custom app qbit 2026-01-30 10:36:29 -06:00
Darin Kelkhoff 3612522c0e fix(paths): update detectBasePath to use base-href as strategy 1 2026-01-28 10:55:52 -06:00
james.maes 7e10e462f7 fix: update Selenium tests for unthemed CSS variable behavior (#128)
Tests now expect empty CSS variables for unthemed apps (CSS fallbacks handle defaults).
2026-01-15 14:40:22 -06:00
james.maes eb6aa882ac fix: unthemed tests skip when themed fixture detected (#128)
- Add skipIfThemedFixture() helper to skip tests when theme is active
- Configure separate Playwright projects for themed/unthemed tests
- Support per-request fixture selection via query parameter
2026-01-15 14:21:55 -06:00
james.maes a7066cb860 fix: resolve 5 visual regressions from Round 6 (#128)
- Remove sidenav hover CSS (conflicted with JS hover)
- Change borderRadius default from 8 to 4
- Remove MuiMenuItem padding override
- Revert button typography to develop values
- Remove .qqq-themed scoping from button CSS rules
2026-01-15 13:27:03 -06:00
james.maes b52a87fb4b docs: update session state and documentation for typography fixes (#128) 2026-01-14 17:25:00 -06:00
james.maes 12934bba33 fix: align typography fallbacks with production typography.ts (#128)
Corrected all fallback values in createDynamicTheme.ts and DEFAULT_THEME
to match the original typography.ts source of truth.
2026-01-14 17:19:32 -06:00
james.maes 256bc28c25 fix: add CSS variable fallbacks to all var(--qqq-*) instances (#128)
Reverted 8 MUI theme component files to pre-PR-125 state and added
fallback values to all CSS variables, sourced from legacy colors.ts.
2026-01-14 16:27:11 -06:00
james.maes 1f47f23f61 docs: update session state and documentation for issue #128
Updated all session continuity files with current status (fixes complete, waiting on Darin to test).
2026-01-14 12:53:09 -06:00
james.maes d4251beaa0 fix: scope CSS overrides to themed apps only (#128)
- Add .qqq-themed class toggle in injectIslandVariables.ts
- Scope CSS override rules to .qqq-themed in qqq-override-styles.css
- Add /metaData route to setupProxy.js for e2e test proxy
- Update e2e tests to verify themed vs unthemed behavior
2026-01-14 12:48:17 -06:00
james.maes 9af9b7fadd docs: update session state and plan for issue #128 fixes 2026-01-14 10:27:04 -06:00
james.maes 3f49959d90 fix: restore unthemed app styling to match pre-PR-125 behavior (#128)
- Remove early return in injectIslandVariables to ensure CSS variables always injected
- Add hasExplicitTheme flag to conditionally apply surfaceColor on Paper
- Exclude AppBar from surfaceColor CSS override
- Apply fontSizeBase to body element
- Add 15 unthemed regression tests
2026-01-14 10:23:12 -06:00
james.maes cb6331ab43 fix: use qqq.version 0.40.0-SNAPSHOT for existing BOM 2026-01-12 11:20:30 -06:00
james.maes aadcb599e7 chore: bump version to 0.40.1-SNAPSHOT 2026-01-12 11:09:42 -06:00
james.maes 3f7a8ff996 chore: bump to 0.40.0-SNAPSHOT 2026-01-10 20:57:42 -06:00
james.maes c557e116e3 chore: migrate license from AGPL-3.0 to Apache-2.0 2026-01-05 10:11:38 -06:00
james.maes 18ab409b09 docs: add full release history to CHANGELOG 2025-12-29 08:09:25 -06:00
james.maes a284985a9c Merge develop into main for 0.35.0 release 2025-12-29 08:05:37 -06:00
james.maes e9ceb2765c ci: Add GitHub release configuration 2025-12-24 12:43:28 -06:00
GitHub Action bd8f96d5ef chore: bump version for next release 2025-08-30 15:31:28 +00:00
james.maes 2407504bfe fix - correct bad file merge for pom 2025-08-30 10:29:49 -05:00
james.maes 88b9de82a4 merge - release 0.25.0 2025-08-30 10:25:08 -05:00
CircleCI 01e01d2f2b Bump version to 0.25.0 [skip ci] 2025-08-28 21:21:39 +00:00
350 changed files with 20432 additions and 6017 deletions

No files matched your search

-112
View File
@@ -1,112 +0,0 @@
---
version: 2.1
orbs:
qqq-orb: kingsrook/qqq-orb@0.6.1
executors:
playwright:
docker:
- image: mcr.microsoft.com/playwright:v1.57.0-jammy
jobs:
playwright_tests:
executor: playwright
steps:
- checkout
- restore_cache:
keys:
- npm-deps-{{ checksum "package-lock.json" }}
- npm-deps-
- run:
name: Install dependencies
command: npm ci --legacy-peer-deps
- save_cache:
key: npm-deps-{{ checksum "package-lock.json" }}
paths:
- node_modules
- run:
name: Run Playwright visual regression tests
command: npx playwright test
- store_artifacts:
path: playwright-report/
destination: playwright-report
- store_test_results:
path: test-results/
workflows:
test_only:
jobs:
- playwright_tests:
filters:
branches:
ignore: /(develop|main|release/.*|hotfix/.*)/
tags:
only: []
- qqq-orb/mvn_frontend_test_only:
context: [qqq-maven-registry-credentials]
filters:
branches:
ignore: /(develop|main|release/.*|hotfix/.*)/
tags:
only: []
publish_snapshot:
jobs:
- playwright_tests:
filters:
branches:
only:
- develop
- qqq-orb/mvn_frontend_publish:
context: [qqq-maven-registry-credentials]
branch_type: snapshot
requires:
- playwright_tests
filters:
branches:
only:
- develop
publish_feature:
jobs:
- qqq-orb/mvn_frontend_publish:
context: [qqq-maven-registry-credentials]
branch_type: feature
filters:
branches:
only:
- feature/.*
tags:
only: [/publish.*/]
publish_release_candidate:
jobs:
- qqq-orb/mvn_frontend_publish:
context: [qqq-maven-registry-credentials]
branch_type: release_candidate
filters:
branches:
only:
- /release\/.*/
publish_release:
jobs:
- qqq-orb/mvn_frontend_publish:
context: [qqq-maven-registry-credentials]
branch_type: release
filters:
branches:
ignore: /.*/
tags:
only: [/v.*/]
publish_hotfix_release:
jobs:
- qqq-orb/mvn_frontend_publish:
context: [qqq-maven-registry-credentials]
branch_type: hotfix
filters:
branches:
only:
- /hotfix\/.*/
+12
View File
@@ -0,0 +1,12 @@
name: build
on:
push:
jobs:
maven:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
secrets: inherit
with:
# Selenium ITs need the Chrome-capable image; the React build is packaged into the jar.
browsers: true
node-version: '22.20.0'
prebuild: bash scripts/ci-prebuild.sh
+1
View File
@@ -0,0 +1 @@
* @KofTwentyTwo
+2 -2
View File
@@ -43,7 +43,7 @@ This repository does NOT contain:
### Prerequisites
- **Node.js**: LTS version (18.x or higher)
- **Node.js**: LTS version (20.x or higher)
- **npm**: 8.x or higher
- **Git**: For version control
- **QQQ Server**: Running instance for testing
@@ -154,7 +154,7 @@ When reporting frontend-specific issues, include:
## 📄 License
By contributing to this project, you agree that your contributions will be licensed under the same license as the project (GNU Affero General Public License v3.0).
By contributing to this project, you agree that your contributions will be licensed under the same license as the project (Apache License, Version 2.0).
## 🙏 Thank You
+38
View File
@@ -0,0 +1,38 @@
# GitHub Release Configuration
# Auto-generates release notes from PR labels
changelog:
exclude:
labels:
- ignore-for-release
- duplicate
- invalid
authors:
- dependabot
- dependabot[bot]
categories:
- title: Breaking Changes
labels:
- breaking-change
- breaking
- title: New Features
labels:
- enhancement
- feature
- title: Bug Fixes
labels:
- bug
- fix
- title: Performance Improvements
labels:
- performance
- title: Documentation
labels:
- documentation
- docs
- title: Dependencies
labels:
- dependencies
- title: Other Changes
labels:
- "*"
+23
View File
@@ -0,0 +1,23 @@
name: CodeQL
'on':
push:
branches:
- main
- develop
- release/**
- hotfix/**
pull_request: {}
schedule:
- cron: 41 16 * * 0
workflow_dispatch: {}
permissions:
contents: read
actions: read
security-events: write
packages: read
concurrency:
group: codeql-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
analyze:
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
+4
View File
@@ -0,0 +1,4 @@
# Reviewed findings committed before the Gitea migration; exact historical fingerprints only.
267580b44babf4935c152da8fa2320d6059105f3:src/qqq/components/processes/GoogleDriveFolderPicker.tsx:gcp-api-key:43
+1
View File
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.35.0] - 2025-12-29
### Changed
+71 -17
View File
@@ -236,36 +236,74 @@ Read these files in order:
2. `docs/TODO.md` - Active and completed tasks
3. This file (`CLAUDE.md`) - Project context
### Current Status (as of 2026-01-24)
### Current Status (as of 2026-01-14)
| Item | Value |
|------|-------|
| Branch | `develop` |
| Branch | `feature/fix-visual-regressions-128` |
| Version | `0.40.0-SNAPSHOT` |
| Active PR | #136 (base path detection fix) |
| Latest Commit | `12934bb` - fix: align typography fallbacks with production typography.ts (#128) |
| Status | **READY FOR REVIEW** - All typography fixes verified, waiting on Darin |
| GitHub Issue | [#128](https://github.com/QRun-IO/qqq-frontend-material-dashboard/issues/128) |
### Recent Activity
### Active Work: Issue #128 Visual Regressions (Round 5 - COMPLETE)
- **PR #136** - Fix for issue #134 (base path detection bug)
- **PR #131** - Auth module enhancements (merged)
- **Feature #364** - Customizable table action menus (merged)
- **Feature #365** - Tables belonging to multiple apps (merged)
- **25 unit tests** - Added for PathUtils (detectBasePath, resolveAssetUrl)
- **18 visual regression tests** - Playwright screenshot tests
**Status:** All fixes complete and verified, waiting on Darin's approval
**Root Cause Identified:** The `createDynamicTheme.ts` fallback values and `DEFAULT_THEME` values didn't match the original `typography.ts` source of truth. The original `Theme.ts` used `typography: {...typography}` (direct spread), so production always used `typography.ts` values directly.
**What was fixed:**
- Round 1-3: CSS scoping with `.qqq-themed` class
- Round 4: Added CSS variable fallbacks to all `var(--qqq-*)` instances
- Round 5: Aligned ALL 32 typography fallbacks with `typography.ts`
**Key Typography Corrections (verified against typography.ts):**
| Property | Before (WRONG) | After (CORRECT) | Source |
|----------|---------------|-----------------|--------|
| textPrimary | #344767 | #212121 | colors.ts dark.main |
| fontWeightMedium | 500 | 600 | typography.ts:156 |
| H3 fontSize/weight | 1.5rem/700 | 1.75rem/600 | typography.ts:202-205 |
| H6 fontSize/weight | 0.875rem/600 | 1.125rem/500 | typography.ts:221-224 |
| body2 weight | 400 | 300 | typography.ts:251 |
| button weight | 500 | 300 | typography.ts:258 |
| caption weight | 400 | 300 | typography.ts:266 |
**Next steps:**
1. Wait for Darin to test on branch `feature/fix-visual-regressions-128`
2. If approved, create PR to merge into develop
3. Publish snapshot after merge
### Test Status
| Suite | Tests | Status |
|-------|-------|--------|
| Jest unit tests (PathUtils) | 25 | PASS |
| Playwright visual regression | 18 | PASS |
| Selenium fixture-based | ~100 | PASS |
| Playwright themed | 26 | PASS |
| Playwright unthemed | 13 | PASS |
### Pending Work
### Key Files for Theme System
- Get PR #136 reviewed and merged (closes #134)
- Review dependabot PR #135 (lodash bump)
- Address npm audit vulnerabilities
| File | Purpose |
|------|---------|
| `src/qqq/assets/theme/base/typography.ts` | **SOURCE OF TRUTH** for typography values |
| `src/qqq/assets/theme/base/colors.ts` | Color definitions (dark.main = #212121) |
| `src/qqq/utils/createDynamicTheme.ts` | Builds MUI theme from QThemeMetaData |
| `src/qqq/utils/themeUtils.ts` | DEFAULT_THEME values, CSS variable injection |
| `src/qqq/components/legacy/Theme.ts` | Original theme (spreads typography.ts directly) |
### Running Locally for Testing
```bash
# Unthemed (original MUI styling, no CSS variable overrides)
THEME_FIXTURE=index npm run fixture-server &
HTTPS=true PORT=3000 REACT_APP_PROXY_LOCALHOST_PORT=8001 npm start
# Themed (full theme applied with CSS variables)
THEME_FIXTURE=withFullCustomTheme npm run fixture-server &
HTTPS=true PORT=3000 REACT_APP_PROXY_LOCALHOST_PORT=8001 npm start
# Run all e2e tests (themed + unthemed)
npm run e2e:all
```
### Key Documentation Files
@@ -273,9 +311,25 @@ Read these files in order:
|------|---------|
| `docs/SESSION_STATE.md` | Current work status, branch, version, recent commits |
| `docs/TODO.md` | Task tracking for current feature |
| `docs/QQQ_THEMING_GUIDE.md` | Complete theming reference |
| `docs/QQQ_CSS_SELECTORS_GUIDE.md` | CSS selector patterns reference |
| `docs/THEME_TESTING_GUIDE.md` | Guide for testing theme features |
### Publishing
Release candidates publish automatically on `release/*` branches via CircleCI `publish_release_candidate` workflow.
Snapshots publish on `develop` via `publish_snapshot` workflow.
## Knowledge base
Durable cross-repo knowledge for QQQ lives in the second-brain vault
(`$SECOND_BRAIN_VAULT`, i.e. `R:/Git.Local/KofTwentyTwo/second-brain`):
- Hub: `knowledge/qqq/qqq-hub.md`
- This repo's dossier: `knowledge/qqq/repos/qqq-frontend-material-dashboard.md`
(includes the full server-API consumption inventory, legacy vs `/qqq/v1`)
Dossier reviewed at commit `628e36f64e9a` on branch `develop` (2026-07-04).
Note: the cross-repo version table earlier in this file is stale (0.36-era);
trust `pom.xml` / `package.json` for current pins.
+24 -2
View File
@@ -2,10 +2,12 @@
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
> ℹ️ **Future direction:** The qqq admin UI is being rewritten as [qqq-frontend-next](https://github.com/QRun-IO/qqq-frontend-next) (Next.js 15 + Tailwind 4 + shadcn/ui). `qqq-frontend-material-dashboard` remains the supported choice today and will continue to receive fixes until existing qqq apps complete migration to `qqq-frontend-next`. New qqq apps starting in 2026 should evaluate `qqq-frontend-next` first.
React admin dashboard for QQQ applications.
**For:** Teams using QQQ who want a ready-to-use admin UI without writing frontend code
**Status:** Stable
**Status:** Stable (on long-term deprecation track — see banner above)
## Why This Exists
@@ -30,7 +32,7 @@ If you need a custom UI, use [qqq-frontend-core](https://github.com/QRun-IO/qqq-
## Quick Start
**Prerequisites:** Node.js 18+, running QQQ backend
**Prerequisites:** Node.js 20+, running QQQ backend
```bash
git clone https://github.com/QRun-IO/qqq-frontend-material-dashboard
@@ -79,6 +81,26 @@ npm run build
mvn clean package -Pci
```
### Verify published JAR notices
After staging the built UI in `src/main/resources/material-dashboard/` (as the
publishing orb does), package with the `release` profile to produce main, sources,
tests and Javadoc JARs. Before publication, run the archive guard with the actual
version's archive prefix:
```bash
mvn -Prelease -DskipTests -Dgpg.skip=true clean package
./scripts/check-packaged-notices.py target/qqq-frontend-material-dashboard-0.41.0-SNAPSHOT
```
This command verifies packaging only: it skips runtime tests and does not sign
or publish. Existing runtime and hosted release gates are still required.
The guard requires Python 3 and checks that every JAR contains exactly one
`META-INF/LICENSE` and `META-INF/NOTICE`, byte-identical to the root files. It exits
nonzero for missing archives, missing/duplicate entries or altered text. This is
an explicit post-package check; the unchanged Circle orb does not invoke it
automatically. It supplements the existing runtime and hosted release gates.
## Project Status
**Maturity:** Stable, production use
+2 -1
View File
@@ -4,7 +4,8 @@
## Goal
Add stable, semantic CSS selectors so QQQ app developers can target specific UI elements in `QThemeMetaData.customCss`.
Add stable, semantic CSS selectors so QQQ app developers can target specific UI elements in
`MaterialDashboardThemeMetaData.customCss`.
## Approach
+42
View File
@@ -0,0 +1,42 @@
# PLAN: Fix Visual Regressions (Issue #128) - COMPLETE
**Status:** COMPLETE - Waiting on Darin to test
**Branch:** `feature/fix-visual-regressions-128`
**Latest Commit:** `d4251be`
## Goal
Fix visual regressions caused by PR #125 pluggable themes so that unthemed apps look identical to pre-PR-125.
## Solution Summary
The key fix was **scoping CSS override rules to themed apps only**:
1. Added `.qqq-themed` class toggle in `injectIslandVariables.ts`
2. Scoped all CSS rules in `qqq-override-styles.css` to `.qqq-themed`
3. Fixed e2e test infrastructure (`setupProxy.js` missing `/metaData` route)
4. Fixed fixture structure (theme must be under `supplementalInstanceMetaData.theme`)
### Key Insight
`QInstance` class in `@qrunio/qqq-frontend-core` only looks for theme at `object.supplementalInstanceMetaData["theme"]`, NOT at root level.
## Files Modified
| File | Change |
|------|--------|
| `src/qqq/utils/injectIslandVariables.ts` | Added `.qqq-themed` class toggle |
| `src/qqq/styles/qqq-override-styles.css` | Scoped rules to `.qqq-themed` |
| `src/setupProxy.js` | Added `/metaData` route |
| `src/test/resources/fixtures/metaData/withFullCustomTheme.json` | Fixed theme location |
## Test Results
| Suite | Tests | Status |
|-------|-------|--------|
| Playwright themed | 26 | PASS |
| Playwright unthemed | 13 | PASS |
| Selenium fixture-based | 115 | PASS |
## Next Steps
1. Wait for Darin's approval on issue #128
2. Create PR to merge into develop
3. Publish snapshot after merge
+9 -9
View File
@@ -95,10 +95,10 @@ New file: `src/qqq/utils/createDynamicTheme.ts`
```typescript
import { createTheme, Theme } from "@mui/material";
import { QThemeMetaData } from "@qrunio/qqq-frontend-core/lib/model/metaData/QThemeMetaData";
import { MaterialDashboardThemeMetaData } from "qqq/models/metadata/MaterialDashboardThemeMetaData";
import { DEFAULT_THEME } from "./themeUtils";
export function createDynamicTheme(themeData?: QThemeMetaData): Theme {
export function createDynamicTheme(themeData?: MaterialDashboardThemeMetaData): Theme {
// Merge with defaults
const theme = { ...DEFAULT_THEME, ...filterUndefined(themeData) };
@@ -152,12 +152,12 @@ New file: `src/qqq/utils/injectIslandVariables.ts`
This injects CSS variables ONLY for sidebar, branded header, and non-MUI components.
```typescript
import { QThemeMetaData } from "@qrunio/qqq-frontend-core/lib/model/metaData/QThemeMetaData";
import { MaterialDashboardThemeMetaData } from "qqq/models/metadata/MaterialDashboardThemeMetaData";
import { DEFAULT_THEME } from "./themeUtils";
const CSS_VAR_PREFIX = "--qqq-";
export function injectIslandVariables(themeData?: QThemeMetaData): void {
export function injectIslandVariables(themeData?: MaterialDashboardThemeMetaData): void {
const root = document.documentElement;
const theme = { ...DEFAULT_THEME, ...filterUndefined(themeData) };
@@ -413,10 +413,10 @@ mvn verify -Pci
### New Files to Create
| File | Purpose |
|------|---------|
| `src/qqq/utils/createDynamicTheme.ts` | MUI theme builder from QThemeMetaData |
| `src/qqq/utils/injectIslandVariables.ts` | CSS vars for sidebar/header islands only |
| File | Purpose |
|------|-------------------------------------------------------|
| `src/qqq/utils/createDynamicTheme.ts` | MUI theme builder from MaterialDashboardThemeMetaData |
| `src/qqq/utils/injectIslandVariables.ts` | CSS vars for sidebar/header islands only |
### Files to Modify
@@ -444,7 +444,7 @@ mvn verify -Pci
1. All 26 Playwright tests pass
2. CSS override file reduced from 1213 to <400 lines
3. `!important` count reduced from 82 to <15
4. Theme data flows directly: QThemeMetaData -> MUI Theme -> Components
4. Theme data flows directly: MaterialDashboardThemeMetaData -> MUI Theme -> Components
5. No visual regressions in app
6. CI pipeline passes
+11 -12
View File
@@ -81,21 +81,21 @@ This document outlines the architecture changes needed to support pluggable them
Runtime-swappable themes via CSS variables injected from backend metadata.
```
┌─────────────────────────────────────────────────────────────────┐
│ QQQ Backend │
│ QThemeMetaData: palette, typography, spacing, icons, shadows │
└─────────────────────────┬───────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────┐
│ QQQ Backend │
│ MaterialDashboardThemeMetaData: palette, typography, spacing, icons, shadows │
└─────────────────────────┬───────────────────────────────────────────────────────┘
│ /metaData endpoint (expanded)
▼
┌─────────────────────────────────────────────────────────────────┐
│ qqq-frontend-core │
│ + QThemeMetaData class with full design token structure │
│ + ThemeUtils for CSS variable generation │
│ qqq-frontend-core │
│ + MaterialDashboardThemeMetaData comes along in the QInstance │
│ as supplementalMetaData │
└─────────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ qqq-frontend-material-dashboard │
│ qqq-frontend-material-dashboard │
│ ThemeProvider injects CSS variables to :root │
│ All components consume var(--qqq-color-primary) etc. │
│ MUI theme reads from CSS variables │
@@ -131,8 +131,7 @@ Separate npm packages per theme, selected at build time.
### 1. Design Token Schema
```typescript
// qqq-frontend-core: src/model/metaData/QThemeMetaData.ts
export interface QThemeMetaData {
export interface MaterialDashboardThemeMetaData{
name: string;
palette: {
@@ -214,7 +213,7 @@ interface TypographyVariant {
```typescript
// qqq-frontend-dashboard: src/qqq/theme/ThemeInjector.ts
export function injectThemeVariables(theme: QThemeMetaData): void {
export function injectThemeVariables(theme: MaterialDashboardThemeMetaData): void {
const root = document.documentElement;
// Colors
@@ -599,7 +598,7 @@ Create a new repository `qqq-theme-default` with the following structure:
"type": "git",
"url": "https://github.com/QRun-IO/qqq-theme-default.git"
},
"license": "AGPL-3.0"
"license": "Apache-2.0"
}
```
+1 -1
View File
@@ -1,6 +1,6 @@
# QQQ CSS Selectors Guide
QQQ automatically generates stable `data-qqq-id` attributes on key UI elements, enabling precise CSS targeting via `QThemeMetaData.customCss`.
QQQ automatically generates stable `data-qqq-id` attributes on key UI elements, enabling precise CSS targeting via `MaterialDashboardThemeMetaData.customCss`.
## ID Format
+12 -12
View File
@@ -6,22 +6,22 @@ This document explains the QQQ pluggable theming system for AI assistants and de
QQQ uses a **backend-driven theming model**:
1. **Java Backend**: Define theme in `QThemeMetaData` object
2. **Frontend Core**: TypeScript `QThemeMetaData` interface mirrors Java class
1. **Java Backend**: Define theme in `MaterialDashboardThemeMetaData` object
2. **Frontend Model**: TypeScript `MaterialDashboardThemeMetaData` interface mirrors Java class
3. **Frontend Dashboard**: `themeUtils.ts` injects CSS custom properties at runtime
Theme data flows: `QThemeMetaData.java` → `/metaData` API → `QThemeMetaData.ts` → CSS variables (`--qqq-*`)
Theme data flows: `MaterialDashboardThemeMetaData.java` → `/metaData` API → `MaterialDashboardThemeMetaData.ts` → CSS variables (`--qqq-*`)
## Configuring a Theme (Java Backend)
Create a `QThemeMetaData` instance using fluent setters:
Create a `MaterialDashboardThemeMetaData` instance using fluent setters:
```java
QThemeMetaData theme = new QThemeMetaData()
MaterialDashboardThemeMetaData theme = new MaterialDashboardThemeMetaData()
.withPrimaryColor("#1976D2")
.withSidebarBackgroundColor("#1E1E2D");
qInstance.setTheme(theme);
qInstance.add(theme);
```
All properties are optional. Omitted properties use frontend defaults.
@@ -251,7 +251,7 @@ The fallback value (after comma) is used if the variable is undefined.
## Theme Application Flow
1. Backend serves `QThemeMetaData` via `/metaData` endpoint
1. Backend serves `MaterialDashboardThemeMetaData` via `/metaData` endpoint
2. Frontend receives theme in `QInstance.theme`
3. `themeUtils.ts` calls `injectThemeVariables(theme)`
4. Function sets CSS custom properties on `:root`
@@ -264,7 +264,7 @@ The fallback value (after comma) is used if the variable is undefined.
**Minimal** - Override only what differs from defaults:
```java
new QThemeMetaData()
new MaterialDashboardThemeMetaData()
.withPrimaryColor("#E91E63")
.withBrandedHeaderEnabled(true);
```
@@ -286,10 +286,10 @@ For dark themes, invert the typical light/dark relationships:
## File Locations
| Repository | Key Files |
|------------|-----------|
| qqq-backend-core | `QThemeMetaData.java` |
| qqq-frontend-core | `QThemeMetaData.ts` |
| Repository | Key Files |
|---------------------------------|-----------|
| qqq-backend-core | `MaterialDashboardThemeMetaData.java` |
| qqq-frontend-material-dashboard | `MaterialDashboardThemeMetaData.ts` |
| qqq-frontend-material-dashboard | `themeUtils.ts`, `qqq-override-styles.css` |
---
+52 -40
View File
@@ -1,58 +1,70 @@
# Session State - QQQ Frontend Material Dashboard
**Last Updated:** 2026-01-24
**Branch:** `develop`
**Last Updated:** 2026-01-15
**Branch:** `feature/fix-visual-regressions-128`
**Version:** `0.40.0-SNAPSHOT`
## Current Status
On `develop` branch. Just completed fix for issue #134 (base path detection bug).
## Active PR
- **PR #136** - fix(PathUtils): correct base path detection for root deployments
- Branch: `feature/134-base-path-detection-bug`
- Fixes issue #134
- Adds 25 unit tests for PathUtils
- Awaiting review/merge
## Recent Commits (develop)
```
a969192 chore: update qqq-frontend-core to 0.40.4-SNAPSHOT
a43b5c8 Merged feature/365-formal-support-for-tables-belonging-to-multiple-apps
1d82bfd Merged feature/364-customizable-table-action-menus-and-additional-menu-support
7287659 chore: bump package.json version to 0.40.0-SNAPSHOT to match pom.xml
99a8f79 Merge pull request #131 from QRun-IO/feature/371-Anonymous-auth-module
```
**ROUND 6 FIXES COMPLETE (ALL 5 ISSUES)** - Ready for Darin to test.
## What Was Done This Session
1. Analyzed issue #134 - base path detection bug causing path duplication
2. Identified two bugs in `detectBasePath()`:
- Bug 1: `if (match && match[1])` fails for root deployments (empty string is falsy)
- Bug 2: Strategy 3 incorrectly used current URL path as base path
3. Fixed both bugs in `src/qqq/utils/PathUtils.ts`
4. Created comprehensive test suite with 25 unit tests
5. Verified all Playwright visual regression tests pass (18 tests)
6. Created feature branch, committed, and opened PR #136
7. Updated GitHub issue #134 with fix details
### Issue #128 Visual Regressions - Round 6
## Next Steps
Fixed 5 regressions reported by Darin (compared to develop branch):
- [ ] Get PR #136 reviewed and merged
- [ ] Review dependabot PR #135 (lodash bump)
- [ ] Address npm audit vulnerabilities
| Issue | Root Cause | Fix |
|-------|------------|-----|
| Sidenav double-highlight | CSS hover rules added in `064e86d` conflicted with JS hover | Removed CSS rules (lines 949-953) |
| Chip border radius changed | `shape.borderRadius: 8` vs MUI default 4 | Changed default to 4 |
| Menu padding increased | MuiMenuItem override set 8px vs menuItem.ts's ~5px | Removed override |
| View field height reduced | Button typography changed (lineHeight 1.5→1.75, fontWeight 300→500) | Reverted to develop values |
| Button color change | Button CSS scoped to `.qqq-themed` class (develop was unscoped) | Removed `.qqq-themed` scoping from button rules |
### Files Modified
| File | Change |
|------|--------|
| `src/qqq/styles/qqq-override-styles.css` | Removed sidenav hover CSS; removed `.qqq-themed` scoping from button rules |
| `src/qqq/utils/createDynamicTheme.ts` | Fixed borderRadius default (4), removed MuiMenuItem, fixed button typography |
| `src/qqq/utils/themeUtils.ts` | Fixed DEFAULT_THEME borderRadius and button typography |
| `e2e/tests/round6-regressions.spec.ts` | Added 10 tests for Round 6 fixes (including Issue 5) |
## Test Status
| Suite | Tests | Status |
|-------|-------|--------|
| Jest unit tests (PathUtils) | 25 | PASS |
| Playwright visual regression | 18 | PASS |
| Selenium fixture-based | ~100 | Not run this session |
| Playwright themed | 26 | PASS |
| Round 6 regression tests | 10 | PASS |
## Key Files Modified This Session
## Next Steps
- `src/qqq/utils/PathUtils.ts` - Bug fix
- `src/qqq/utils/PathUtils.test.ts` - New test file (25 tests)
1. Commit Round 6 fixes
2. Darin to test on branch `feature/fix-visual-regressions-128`
3. If approved, create PR to merge into develop
## GitHub Issue
https://github.com/QRun-IO/qqq-frontend-material-dashboard/issues/128
## QQQ #923 license alignment — 2026-09-30
Owner-approved first-party Apache-2.0 declarations now align across source headers, Maven/npm metadata and current contributor/theme documentation. Attribution, third-party notices, dependency pins and executable source are preserved. The license delta passed independent review. QQQ #937 adds bounded waits before existing saved-view background assertions; production behavior is unchanged. Browser verification requires the HTTPS frontend on port 3001 (proxy 8001), CIRCLECI=true and QQQ_SELENIUM_HEADLESS=true. Publish a new candidate only after validation, review and release gates pass. Earlier work recorded above is unchanged.
## QQQ #939/#940 dependency security — 2026-09-30
Isolated patch from `f80d76bffa1a9ba24a304a056eb5d9df606d4ab7`: Jackson BOM 2.21.7, root Axios <1 override 0.34.0 and webpack-dev-middleware override 7.4.6. The published middleware backport preserves the APIs/options used by CRA 5.0.1 and dev-server 4.15.2; its Node >=18.12/webpack 5 requirements fit this project's Node >=20 and webpack 5.105.4. Local verification passed 22 Maven units, 54 JS tests, security regressions, Material serialization and the production build (source-map warnings retained). Full dev-server tests rejected all 27 traversal probes while preserving assets, proxy/service-worker/history hooks, HMR and rebuilds; actual root/subpath `npm start` checks also passed. Evidence: `qqq-license-923/evidence/alignment/material-security-940/` in the coordinating workspace. No UI source or runtime modernization is included.
Fresh full and production-only npm audits both report 0 High/0 Critical; remaining totals are 15 Moderate/12 Low and 5 Moderate respectively. The old subpath disclosure baseline is retained; the middleware fix rejects traversal even though CRA still strips PUBLIC_URL's slash. The existing CRA source-overlay `_stats` mismatch reproduced identically before/after and is not fixed here. Public frontend-core 0.40.19-SNAPSHOT still pins Axios 0.33.0 outside this root override; #902 is separately coordinated. Independent review, full browser/hosted gates, GitHub alert reconciliation, RC2 integration and publication remain with the coordinator.
## Public frontend-core integration — 2026-09-30
Pin the verified public frontend-core 0.40.20-SNAPSHOT from core PR48/snapshot job388. Its exact registry integrity, Apache LICENSE/NOTICE and library files match the reviewed package; an isolated public consumer passes four HTTP and three security checks with zero audit findings. Only the root dependency and core lock node changed in this integration. Independent pin review passed.
The combined Jackson/Axios/middleware/core candidate passes 54 JavaScript tests, production build and full Maven clean verify: 121 tests, zero failures/errors/skips/retries. Full audit reports zero High/Critical, 14 Moderate/12 Low; production reports zero High/Critical and five Moderate. Existing source-map/overlay and tool warnings remain recorded. Evidence is in the coordinating workspace under qqq-license-923/evidence/alignment/material-integrated-940/. Hosted checks, alert reconciliation, RC2 integration and existing publication gates remain; this is not RC/GA acceptance.
## QQQ #942 / #923 packaged notices — 2026-09-30
Bounded correction from signed PR162 merge `a37ead3b8c6b5a4819d29d853176374055dc1e8b`: Maven shared archive resources now copy the unchanged root LICENSE/NOTICE into all four published JARs. Resources 3.4.0 matches the prior integrated build. Actual release-profile packaging passed with reused verified public UI and runtime tests skipped; the executable archive guard fails on the existing public JARs and passes on all new local archives. Dependencies, Java/UI behavior and third-party notices are unchanged. Evidence: `qqq-license-923/evidence/alignment/material-notices-942/` in the coordination workspace. Independent review, signed integration, hosted required checks and verification of a newly published timestamped artifact remain; no public correction is claimed.
+5 -5
View File
@@ -22,7 +22,7 @@ The QQQ dashboard now supports runtime theme customization via CSS custom proper
Add theme configuration to your QInstance:
```java
QThemeMetaData theme = new QThemeMetaData()
MaterialDashboardThemeMetaData theme = new MaterialDashboardThemeMetaData()
// Core colors
.withPrimaryColor("#1976D2")
.withSecondaryColor("#9C27B0")
@@ -144,7 +144,7 @@ Same pattern for H2-H6, Body1, Body2, Button, Caption.
### 1. Basic Color Test
```java
new QThemeMetaData()
new MaterialDashboardThemeMetaData()
.withPrimaryColor("#FF5722")
.withSidebarBackgroundColor("#263238");
```
@@ -152,7 +152,7 @@ new QThemeMetaData()
### 2. Typography Test
```java
new QThemeMetaData()
new MaterialDashboardThemeMetaData()
.withFontFamily("Georgia, serif")
.withTypographyH1FontSize("3rem")
.withTypographyH1FontWeight(800);
@@ -161,7 +161,7 @@ new QThemeMetaData()
### 3. Branded Header Test
```java
new QThemeMetaData()
new MaterialDashboardThemeMetaData()
.withBrandedHeaderEnabled(true)
.withBrandedHeaderBackgroundColor("#1a237e")
.withBrandedHeaderTextColor("#ffffff")
@@ -172,7 +172,7 @@ new QThemeMetaData()
### 4. Density Test
```java
new QThemeMetaData()
new MaterialDashboardThemeMetaData()
.withDensity("compact");
```
**Verify:** Reduced spacing throughout the UI.
+67 -16
View File
@@ -1,26 +1,77 @@
# TODO - QQQ Frontend Material Dashboard
**Last Updated:** 2026-01-24
**Last Updated:** 2026-01-14
## In Progress
- [ ] PR #136 - Base path detection fix (awaiting review)
- Branch: `feature/134-base-path-detection-bug`
- Closes issue #134
### Issue #128: Visual Regressions - READY FOR REVIEW
## Open Items
**Branch:** `feature/fix-visual-regressions-128`
**Status:** All fixes complete, waiting on Darin to test
- [ ] Review dependabot PR #135 - lodash bump
- [ ] Address npm audit vulnerabilities (9 total: 2 critical, 1 high, 6 moderate)
- [x] Round 1-3: Add `.qqq-themed` class toggle, scope CSS overrides
- [x] Round 4: Add CSS variable fallbacks to all `var(--qqq-*)` instances
- [x] Round 5: Align ALL typography fallbacks with `typography.ts`
- [x] Verify all 39 e2e tests pass (26 themed + 13 unthemed)
- [x] Triple-check all 32 typography values against source of truth
- [x] Commit typography fixes
- [ ] **WAITING:** Darin to test and approve
- [ ] Create PR to merge into develop
- [ ] Publish snapshot after merge
---
### CI Playwright Timeout (Separate Issue)
**Branch:** `fix/ci-playwright-timeout`
**Status:** Parked - lower priority than visual regressions
- [x] Diagnosed: webserver timeout (120s not enough for webpack)
- [ ] Debug blank page issue in combined fixture server
- [ ] Test locally until passing
- [ ] Merge to develop
## Completed (Recent)
- [x] Issue #134 - Base path detection bug
- Fixed `detectBasePath()` returning wrong path for root deployments
- Removed unreliable Strategy 3 (single-segment path detection)
- Added 25 unit tests for PathUtils
- PR #136 created and pushed
- [x] PR #131 - Auth module enhancements (merged)
- [x] Feature #364 - Customizable table action menus (merged)
- [x] Feature #365 - Tables belonging to multiple apps (merged)
- [x] Update qqq-frontend-core to 0.40.4-SNAPSHOT
- [x] **Issue #128 Round 5** - Typography fallbacks aligned with typography.ts
- [x] **Issue #128 Round 4** - CSS variable fallbacks added
- [x] **Issue #128 Round 3** - Scoped CSS to `.qqq-themed` class
- [x] **39 Playwright tests passing** (26 themed + 13 unthemed)
- [x] **PR #127 merged** - Playwright e2e test integration
- [x] **PR #125 merged** - Pluggable themes + CSS selectors system
## Test Status
| Suite | Tests | Status |
|-------|-------|--------|
| Playwright themed | 26 | PASS |
| Playwright unthemed | 13 | PASS |
## Known Issues
- CI Playwright tests may timeout (WIP fix on separate branch)
- `seleniumwithqapplication` tests require full QQQ backend (hang locally)
## QQQ #923 license alignment — 2026-09-30
Owner-approved first-party Apache-2.0 declarations now align across source headers, Maven/npm metadata and current contributor/theme documentation. Attribution, third-party notices, dependency pins and executable source are preserved. The license delta passed independent review. QQQ #937 adds bounded waits before existing saved-view background assertions; production behavior is unchanged. Browser verification requires the HTTPS frontend on port 3001 (proxy 8001), CIRCLECI=true and QQQ_SELENIUM_HEADLESS=true. Publish a new candidate only after validation, review and release gates pass. Earlier work recorded above is unchanged.
## QQQ #939/#940 dependency security — 2026-09-30
- [x] Verify public Jackson 2.21.7/Axios 0.34.0 artifacts and middleware 7.4.6 backport; update BOM, overrides and their lock entries/transitive development dependencies.
- [x] Preserve vulnerable Jackson/Axios baselines; pass security regressions, 22 Maven units, 54 JS tests, Material serialization and production build.
- [x] Reproduce subpath disclosure on 5.3.4; verify the 7.4.6 override blocks all 27 full-server probes while preserving assets/hooks/HMR/rebuilds and actual root/subpath npm start. Rerun JS/build/audits: 0 High/0 Critical; preserve existing overlay defect evidence and remaining Moderate/Low tracking.
- [ ] Coordinator: independently review/integrate this patch, reconcile GitHub alerts under #940/#892, coordinate frontend-core's own Axios pin under #902, and rerun RC2 browser/hosted/release gates before publication.
## Public frontend-core integration — 2026-09-30
- [x] Verify public frontend-core 0.40.20-SNAPSHOT, pin its exact registry version/integrity and independently review the dependency delta.
- [x] Pass the integrated 54 JavaScript tests, production build and 121 Maven/Selenium tests without failures, skips or retries; preserve remaining Moderate/Low audit findings.
- [ ] Pass hosted PR checks, reconcile alerts and integrate the accepted changes into the RC2 draft while retaining #892 and the other release gates.
## QQQ #942 / #923 packaged notices
- [x] Include exact root LICENSE/NOTICE in main, sources, tests and Javadoc JARs using native Maven resource packaging; preserve existing archive contents.
- [x] Prove the archive guard fails on the public baseline and altered notice bytes, and passes on all four actual local archives; document explicit post-package invocation.
- [ ] Coordinator: independent review, signed integration and hosted checks; verify all four newly published JARs before claiming public correction, then coordinate the RC input with existing release gates.
+179 -6
View File
@@ -47,9 +47,13 @@ const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://localhost:${PORT}`);
const pathname = url.pathname;
// Support per-request fixture selection via ?fixture=name query parameter
// This allows unthemed tests to request the 'index' fixture explicitly
const requestedFixture = url.searchParams.get('fixture') || FIXTURE_NAME;
// /metaData endpoint - return main metadata with theme
if (pathname === '/metaData' || pathname === '/metaData/' || pathname === '/api/metaData') {
const data = loadFixture(`metaData/${FIXTURE_NAME}.json`);
const data = loadFixture(`metaData/${requestedFixture}.json`);
if (data) {
serveJson(res, data);
return;
@@ -93,15 +97,131 @@ const server = http.createServer((req, res) => {
}
}
// /processes/querySavedView/init - saved view init
if (pathname === '/processes/querySavedView/init') {
const data = loadFixture('processes/querySavedView/init.json');
// /metaData/process/{processName} - process metadata (supports dots in name like person.bulkEdit)
const processMetaMatch = pathname.match(/^\/metaData\/process\/(.+)$/);
if (processMetaMatch) {
const processName = processMetaMatch[1];
const data = loadFixture(`metaData/process/${processName}.json`);
if (data) {
serveJson(res, data);
return;
}
// Default response if fixture not found
serveJson(res, { values: {} });
// Default process metadata response (wrapped in 'process' key)
serveJson(res, {
process: {
name: processName,
label: processName.charAt(0).toUpperCase() + processName.slice(1).replace(/([A-Z])/g, ' $1'),
tableName: 'person',
frontendSteps: [
{ name: 'input', label: 'Input', stepType: 'frontend', formFields: [], components: [] },
{ name: 'review', label: 'Review', stepType: 'frontend', recordListFields: [], components: [] },
{ name: 'result', label: 'Result', stepType: 'frontend', components: [{ type: 'PROCESS_SUMMARY_RESULTS' }] }
]
}
});
return;
}
// /processes/{processName}/init - process init
const processInitMatch = pathname.match(/^\/processes\/([\w.\-]+)\/init$/);
if (processInitMatch) {
const processName = processInitMatch[1];
const data = loadFixture(`processes/${processName}/init.json`);
if (data) {
serveJson(res, data);
return;
}
// Default process init response with stepper and UUID
serveJson(res, {
processUUID: 'test-process-uuid-12345',
values: {},
processMetaData: {
name: processName,
label: processName,
frontendSteps: [
{ name: 'input', label: 'Input', stepType: 'frontend', formFields: [], components: [] },
{ name: 'review', label: 'Review', stepType: 'frontend', recordListFields: [], components: [] },
{ name: 'result', label: 'Result', stepType: 'frontend', components: [{ type: 'PROCESS_SUMMARY_RESULTS' }] }
]
},
nextStep: 'input'
});
return;
}
// /processes/{processName}/{uuid}/records - get process records
const processRecordsMatch = pathname.match(/^\/processes\/(\w+)\/([^\/]+)\/records$/);
if (processRecordsMatch) {
serveJson(res, {
records: [],
count: 0
});
return;
}
// /processes/{processName}/{uuid}/step/{stepName} - process step with UUID
const processStepWithUuidMatch = pathname.match(/^\/processes\/(\w+)\/([^\/]+)\/step\/(\w+)$/);
if (processStepWithUuidMatch) {
const processName = processStepWithUuidMatch[1];
const uuid = processStepWithUuidMatch[2];
const stepName = processStepWithUuidMatch[3];
// Return next step
serveJson(res, {
processUUID: uuid,
values: {},
processMetaData: {
name: processName,
label: processName,
frontendSteps: [
{ name: 'input', label: 'Input', stepType: 'frontend', formFields: [], components: [] },
{ name: 'review', label: 'Review', stepType: 'frontend', recordListFields: [], components: [] },
{ name: 'result', label: 'Result', stepType: 'frontend', components: [{ type: 'PROCESS_SUMMARY_RESULTS' }] }
]
},
nextStep: stepName === 'input' ? 'review' : (stepName === 'review' ? 'result' : null),
processResults: stepName === 'result' ? { message: 'Process completed successfully!' } : undefined
});
return;
}
// /processes/{processName}/step/{stepName} - process step (legacy, no UUID)
const processStepMatch = pathname.match(/^\/processes\/(\w+)\/step\/(\w+)$/);
if (processStepMatch) {
const processName = processStepMatch[1];
const stepName = processStepMatch[2];
// Return next step
serveJson(res, {
processUUID: 'test-process-uuid-12345',
values: {},
processMetaData: {
name: processName,
label: processName,
frontendSteps: [
{ name: 'input', label: 'Input', stepType: 'frontend', formFields: [], components: [] },
{ name: 'review', label: 'Review', stepType: 'frontend', recordListFields: [], components: [] },
{ name: 'result', label: 'Result', stepType: 'frontend', components: [{ type: 'PROCESS_SUMMARY_RESULTS' }] }
]
},
nextStep: stepName === 'input' ? 'review' : (stepName === 'review' ? 'result' : null),
processResults: stepName === 'result' ? { message: 'Process completed successfully!' } : undefined
});
return;
}
// /widget/{widgetName} - widget data
const widgetMatch = pathname.match(/^\/widget\/(.+)$/);
if (widgetMatch) {
const widgetName = widgetMatch[1];
const data = loadFixture(`widget/${widgetName}.json`);
if (data) {
serveJson(res, data);
return;
}
// Default empty widget response
serveJson(res, {
type: 'table',
data: []
});
return;
}
@@ -157,6 +277,59 @@ const server = http.createServer((req, res) => {
return;
}
// /data/{tableName}/count - alternate count endpoint
const dataCountMatch = pathname.match(/^\/data\/(\w+)\/count$/);
if (dataCountMatch) {
serveJson(res, { count: 2 });
return;
}
// /data/{tableName}/{id} - alternate single record lookup
const dataRecordMatch = pathname.match(/^\/data\/(\w+)\/(\d+)$/);
if (dataRecordMatch) {
const tableName = dataRecordMatch[1];
const recordId = dataRecordMatch[2];
serveJson(res, {
values: {
id: parseInt(recordId),
firstName: recordId === '1' ? 'John' : 'Jane',
lastName: recordId === '1' ? 'Doe' : 'Smith',
email: recordId === '1' ? 'john.doe@example.com' : 'jane.smith@example.com',
birthDate: '1990-01-15',
createDate: '2024-01-01T00:00:00Z',
modifyDate: '2024-01-01T00:00:00Z'
},
tableName: tableName
});
return;
}
// /qqq/v1/table/{tableName}/{id} - single record lookup
const recordMatch = pathname.match(/^\/qqq\/v1\/table\/(\w+)\/(\d+)$/);
if (recordMatch) {
const tableName = recordMatch[1];
const recordId = recordMatch[2];
const data = loadFixture(`qqq/v1/table/${tableName}/${recordId}.json`);
if (data) {
serveJson(res, data);
return;
}
// Default person record response
serveJson(res, {
values: {
id: parseInt(recordId),
firstName: recordId === '1' ? 'John' : 'Jane',
lastName: recordId === '1' ? 'Doe' : 'Smith',
email: recordId === '1' ? 'john.doe@example.com' : 'jane.smith@example.com',
birthDate: '1990-01-15',
createDate: '2024-01-01T00:00:00Z',
modifyDate: '2024-01-01T00:00:00Z'
},
tableName: tableName
});
return;
}
// Default 404
serve404(res, pathname);
});
+94
View File
@@ -0,0 +1,94 @@
import {test, expect, Page} from "@playwright/test";
import {readFileSync} from "fs";
import path from "path";
/** First-party HTTP fixtures exercise browser request and error behavior; backend authorization is tested separately. */
async function configure(page: Page, bridgeReadable = false, denyCount = false, denyQuery = false)
{
const fixture = (name: string) => JSON.parse(readFileSync(path.resolve(__dirname, "../../src/test/resources/fixtures", name), "utf8"));
const global = fixture("metaData/index.json");
const person = fixture("qqq/v1/metaData/table/person.json");
const toy = {name: "toy", label: "Toy", primaryKeyField: "id", readPermission: true, insertPermission: true,
fields: {name: {name: "name", label: "Toy name", type: "STRING"}}};
global.tables.pet = {name: "pet", label: "Pet", readPermission: bridgeReadable, insertPermission: true};
global.tables.toy = {...toy};
person.exposedJoins = [{label: "Toys", isMany: true, joinTable: toy, joinPath: [
{name: "personPet", leftTable: "person", rightTable: "pet", type: "ONE_TO_MANY"},
{name: "toyPet", leftTable: "toy", rightTable: "pet", type: "MANY_TO_ONE"}
]}];
const queryInputs: any[] = [];
const countInputs: any[] = [];
const errors: string[] = [];
page.on("pageerror", error => errors.push(error.message));
await page.route(/\/metaData\/?(?:\?.*)?$/, route => route.fulfill({json: global}));
await page.route("**/metaData/table/person*", route => route.fulfill({json: person}));
await page.route("**/qqq/v1/table/person/query*", async route =>
{
queryInputs.push(route.request().postDataJSON());
if (denyQuery) await route.fulfill({status: 403, json: {error: "Permission denied."}});
else
{
// Complete the successful Query after the denied Count is already displayed.
if (denyCount) await page.getByRole("alert").filter({hasText: "Cannot count records:"}).waitFor();
await route.continue();
}
});
await page.route("**/qqq/v1/table/person/count*", async route =>
{
countInputs.push(route.request().postDataJSON());
if (denyCount) await route.fulfill({status: 403, json: {error: "Permission denied."}});
else await route.continue();
});
return {queryInputs, countInputs, errors};
}
test("restricted bridge stays out of automatic requests and field choices", async ({page}) =>
{
const {queryInputs, countInputs, errors} = await configure(page);
await page.goto("/peopleApp/greetingsApp/person");
await expect.poll(() => queryInputs.length).toBeGreaterThan(0);
await expect.poll(() => countInputs.length).toBeGreaterThan(0);
await expect(page.getByRole("cell", {name: "Test", exact: true})).toBeVisible();
expect(queryInputs.every(input => !input.joins?.some((join: any) => join.joinTable === "toy"))).toBe(true);
expect(countInputs.every(input => !input.joins?.some((join: any) => join.joinTable === "toy"))).toBe(true);
await page.locator('[data-qqq-id="button-columns"]').click();
await expect(page.locator(".fieldListMenuBody-columns")).toBeVisible();
await expect(page.locator(".fieldListMenuBody-columns")).not.toContainText("Toy name");
expect(errors).toEqual([]);
});
test("readable bridge restores query choices and selected joins", async ({page}) =>
{
const {queryInputs, errors} = await configure(page, true);
await page.goto("/peopleApp/greetingsApp/person");
await expect.poll(() => queryInputs.length).toBeGreaterThan(0);
await page.locator('[data-qqq-id="button-columns"]').click();
await page.locator(".fieldListMenuBody-columns").getByText("Toy name", {exact: true}).click();
await expect.poll(() => queryInputs.some(input => input.joins?.some((join: any) => join.joinTable === "toy"))).toBe(true);
expect(errors).toEqual([]);
});
test("denied Count remains visible after successful record loading", async ({page}) =>
{
const {queryInputs, countInputs, errors} = await configure(page, false, true);
await page.goto("/peopleApp/greetingsApp/person");
await expect.poll(() => queryInputs.length).toBeGreaterThan(0);
await expect.poll(() => countInputs.length).toBeGreaterThan(0);
await expect(page.getByRole("cell", {name: "Test", exact: true})).toBeVisible();
await expect(page.getByRole("alert").filter({hasText: "Cannot count records:"})).toBeVisible();
expect(errors).toEqual([]);
});
test("saved denied criteria and ordering are retained and show an error", async ({page}) =>
{
const {queryInputs, countInputs, errors} = await configure(page, false, true, true);
const filter = {criteria: [{fieldName: "toy.name", operator: "EQUALS", values: ["restricted"]}], orderBys: [{fieldName: "toy.name", isAscending: true}]};
await page.goto("/peopleApp/greetingsApp/person?filter=" + encodeURIComponent(JSON.stringify(filter)));
await expect.poll(() => queryInputs.length).toBeGreaterThan(0);
await expect.poll(() => countInputs.length).toBeGreaterThan(0);
expect(queryInputs[0].filter.criteria).toEqual(filter.criteria);
expect(queryInputs[0].filter.orderBys).toEqual(filter.orderBys);
expect(countInputs[0].filter.criteria).toEqual(filter.criteria);
await expect(page.getByRole("alert").filter({hasText: "Permission denied."}).first()).toBeVisible();
expect(errors).toEqual([]);
});
+345
View File
@@ -0,0 +1,345 @@
import { test, expect } from '@playwright/test';
/**
* Round 6 Visual Regression Tests
*
* Tests verify fixes for issues reported by Darin:
* 1. Sidenav double-highlight on hover
* 2. Chip border radius changed
* 3. Menu item padding increased
* 4. View field height reduced (button typography)
*/
test.describe('Round 6 Regression Fixes - Themed App', () => {
test.beforeEach(async ({ page }) => {
// Use themed fixture
await page.goto('/testApp/person');
await page.waitForSelector('[id="root"]');
await page.waitForTimeout(2000);
});
test.describe('Issue 1: Sidenav Hover - Single Highlight Only', () => {
test('sidenav item has only JS-controlled hover styling (no CSS override)', async ({ page }) => {
// The fix removed CSS hover rules that were causing double-highlight
// Wait for sidebar to be visible (it may be collapsed on smaller screens)
const drawer = page.locator('.MuiDrawer-paper');
await expect(drawer).toBeVisible();
// Find any visible nav item in the sidebar
const sidenavItem = page.locator('.MuiDrawer-paper .MuiListItem-root').first();
await expect(sidenavItem).toBeVisible({ timeout: 10000 });
// Get initial background
const initialBg = await sidenavItem.evaluate(el => getComputedStyle(el).backgroundColor);
// Hover and check background changes (should be controlled by JS, not CSS)
await sidenavItem.hover();
await page.waitForTimeout(300);
const hoverBg = await sidenavItem.evaluate(el => getComputedStyle(el).backgroundColor);
// The hover background should change, but there should not be multiple overlapping backgrounds
// The key test is that the page renders without visual artifacts
expect(hoverBg).toBeTruthy();
});
test('no redundant CSS hover rules in computed styles', async ({ page }) => {
// Check that the removed CSS rule is not applying
const result = await page.evaluate(() => {
// Look for any sidenav item
const item = document.querySelector('.MuiDrawer-paper a, .MuiDrawer-paper .MuiListItem-root');
if (!item) return { found: false, hasInlineHover: false };
// Check if any external stylesheet is applying the removed hover rule
// We removed: .MuiDrawer-paper a:hover, .MuiDrawer-paper .MuiListItem-root:hover
const sheets = Array.from(document.styleSheets);
let hasRemovedRule = false;
try {
for (const sheet of sheets) {
try {
const rules = Array.from(sheet.cssRules || []);
for (const rule of rules) {
if (rule instanceof CSSStyleRule) {
const selector = rule.selectorText;
if (selector &&
(selector.includes('.MuiDrawer-paper a:hover') ||
selector.includes('.MuiDrawer-paper .MuiListItem-root:hover'))) {
// Check if this rule sets background-color
if (rule.style.backgroundColor) {
hasRemovedRule = true;
}
}
}
}
} catch (e) {
// Cross-origin stylesheets will throw, ignore them
}
}
} catch (e) {
// Ignore errors
}
return { found: true, hasRemovedRule };
});
expect(result.found).toBe(true);
expect(result.hasRemovedRule).toBe(false);
});
});
test.describe('Issue 2: Chip Border Radius (4px default, not 8px)', () => {
test('chips use borderRadius based on 4px default (2px for chip)', async ({ page }) => {
// Navigate to a page with chips if available, or test with a chip if present
// The default borderRadius is 4px, chips use borderRadius/2 = 2px
const result = await page.evaluate(() => {
// Check the shape.borderRadius from the theme via CSS variable
const root = document.documentElement;
const borderRadiusVar = getComputedStyle(root).getPropertyValue('--qqq-border-radius').trim();
// Also check if any chip is rendered
const chip = document.querySelector('.MuiChip-root');
let chipBorderRadius = '';
if (chip) {
chipBorderRadius = getComputedStyle(chip).borderRadius;
}
return {
cssVar: borderRadiusVar,
chipBorderRadius,
hasChip: !!chip
};
});
// The fixture uses 12px, but default should be 4px
// If no chip found, at least verify the CSS variable is set
if (result.hasChip) {
// Chip borderRadius should be half of the theme borderRadius
// With 12px theme (from fixture), chip should be 6px
expect(result.chipBorderRadius).toBeTruthy();
}
});
});
test.describe('Issue 3: Menu Item Padding (original values, not increased)', () => {
test('menu items have correct padding from menuItem.ts (~5px 16px)', async ({ page }) => {
// Find a button that opens a menu
const menuButton = page.locator('[aria-haspopup="true"]').first();
if (await menuButton.isVisible().catch(() => false)) {
await menuButton.click();
await page.waitForTimeout(500);
const result = await page.evaluate(() => {
const menuItem = document.querySelector('.MuiMenuItem-root');
if (!menuItem) return { found: false, padding: '' };
const styles = getComputedStyle(menuItem);
return {
found: true,
padding: styles.padding,
paddingTop: styles.paddingTop,
paddingBottom: styles.paddingBottom,
paddingLeft: styles.paddingLeft,
paddingRight: styles.paddingRight,
};
});
if (result.found) {
// Original menuItem.ts uses pxToRem(4.8) which is ~5px vertical
// Should NOT be 8px (that was our regression)
const topPadding = parseFloat(result.paddingTop);
const bottomPadding = parseFloat(result.paddingBottom);
// Padding should be less than 7px (the regression was 8px)
expect(topPadding).toBeLessThan(7);
expect(bottomPadding).toBeLessThan(7);
}
} else {
// Skip if no menu button found - test passes vacuously
test.skip();
}
});
});
test.describe('Issue 5: Button Color (Edit Filters and Columns)', () => {
test('text primary buttons have correct color styling (not scoped to .qqq-themed)', async ({ page }) => {
// The fix removed .qqq-themed scoping so button color applies to ALL apps
const result = await page.evaluate(() => {
// Look for any text primary button (like "Edit Filters and Columns")
const button = document.querySelector('.MuiButton-textPrimary');
if (!button) return { found: false, color: '' };
const styles = getComputedStyle(button);
return {
found: true,
color: styles.color,
};
});
if (result.found) {
// Button should have a color applied (not default inherited)
expect(result.color).toBeTruthy();
// The color should be from our CSS (--qqq-primary-color fallback is #0062FF)
// RGB values will vary based on theme, but should not be black/inherit
} else {
// Skip if no text primary button found
test.skip();
}
});
test('button CSS rules are NOT scoped to .qqq-themed class', async ({ page }) => {
// Verify that button color CSS is not restricted to themed apps
const result = await page.evaluate(() => {
const sheets = Array.from(document.styleSheets);
let hasUnscopedButtonRule = false;
let hasScopedButtonRule = false;
try {
for (const sheet of sheets) {
try {
const rules = Array.from(sheet.cssRules || []);
for (const rule of rules) {
if (rule instanceof CSSStyleRule) {
const selector = rule.selectorText;
if (selector) {
// Check for unscoped button rules (what we want)
if (selector === '.MuiButton-textPrimary' ||
selector === '.MuiButton-containedPrimary') {
hasUnscopedButtonRule = true;
}
// Check for scoped button rules (what we removed)
if (selector.includes('.qqq-themed') &&
selector.includes('.MuiButton-')) {
hasScopedButtonRule = true;
}
}
}
}
} catch (e) {
// Cross-origin stylesheets will throw, ignore them
}
}
} catch (e) {
// Ignore errors
}
return { hasUnscopedButtonRule, hasScopedButtonRule };
});
// Should have unscoped button rules (matching develop behavior)
expect(result.hasUnscopedButtonRule).toBe(true);
// Should NOT have .qqq-themed scoped button rules (regression fix)
expect(result.hasScopedButtonRule).toBe(false);
});
});
test.describe('Issue 4: Button Typography (view field height)', () => {
test('buttons have lineHeight of 1.75 (not 1.5)', async ({ page }) => {
const result = await page.evaluate(() => {
// Find any button
const button = document.querySelector('.MuiButton-root');
if (!button) return { found: false, lineHeight: '' };
const styles = getComputedStyle(button);
return {
found: true,
lineHeight: styles.lineHeight,
fontWeight: styles.fontWeight,
};
});
if (result.found) {
// lineHeight should be 1.75, not 1.5
// Note: computed lineHeight may be in px, so we check it's not the smaller value
const lineHeight = parseFloat(result.lineHeight);
// If it's a ratio (like 1.75), check it directly
// If it's in px, it should reflect the larger lineHeight
if (lineHeight < 10) {
// It's a ratio
expect(lineHeight).toBeGreaterThanOrEqual(1.7);
}
} else {
test.skip();
}
});
test('buttons have fontWeight of 500 or configured value (not 300)', async ({ page }) => {
const result = await page.evaluate(() => {
const button = document.querySelector('.MuiButton-root');
if (!button) return { found: false, fontWeight: '' };
return {
found: true,
fontWeight: getComputedStyle(button).fontWeight,
};
});
if (result.found) {
// fontWeight should be >= 500 (develop value), not 300
// The fixture sets 600, default should be 500
const weight = parseInt(result.fontWeight);
expect(weight).toBeGreaterThanOrEqual(500);
} else {
test.skip();
}
});
});
});
/**
* NOTE: Unthemed tests are in unthemed-regression.spec.ts
* This file tests the themed app behavior after Round 6 fixes.
*
* The key verifications for default values:
* - Default borderRadius changed from 8px to 4px (in parseBorderRadius)
* - Default button fontWeight changed from 300 to 500
* - Default button lineHeight changed from 1.5 to 1.75
*
* These defaults apply when no theme is configured OR when theme
* properties are not explicitly set.
*/
test.describe('Round 6 - Default Value Verification (via themed fixture override)', () => {
test('parseBorderRadius defaults to 4 (verified via code inspection)', async ({ page }) => {
// This test verifies our code change by checking the DEFAULT_THEME
// The actual rendered value in themed tests uses fixture's 12px
await page.goto('/testApp/person');
await page.waitForSelector('[id="root"]');
// Check that the CSS variable is set (fixture uses 12px)
const result = await page.evaluate(() => {
const cssVar = getComputedStyle(document.documentElement)
.getPropertyValue('--qqq-border-radius').trim();
return { cssVar };
});
// Fixture uses 12px, which proves the theming system works
// The default of 4px applies when no theme is configured
expect(result.cssVar).toBe('12px');
});
test('DEFAULT_THEME button values are correct (500 weight, 1.75 lineHeight)', async ({ page }) => {
// The fixture overrides these, but we verify the themed app works
await page.goto('/testApp/person');
await page.waitForSelector('[id="root"]');
const result = await page.evaluate(() => {
const button = document.querySelector('.MuiButton-root');
if (!button) return { found: false };
const styles = getComputedStyle(button);
return {
found: true,
fontWeight: styles.fontWeight,
lineHeight: styles.lineHeight,
};
});
if (result.found) {
// Fixture sets typographyButtonFontWeight: 600
// This proves the theming override works
const weight = parseInt(result.fontWeight);
expect(weight).toBeGreaterThanOrEqual(500);
}
});
});
+523
View File
@@ -0,0 +1,523 @@
import { test, expect } from '@playwright/test';
import {colorsMatch} from "../utils/color-helpers";
/**
* STRICT Theme Verification Tests for QQQ MaterialDashboardThemeMetaData
*
* ALL tests verify ACTUAL rendered values on components.
* Tests FAIL if:
* 1. The expected element is not found
* 2. The rendered value doesn't match the configured value
*
* Theme values from withFullCustomTheme.json fixture
*/
const THEME = {
// Core colors
primaryColor: '#E91E63',
secondaryColor: '#9C27B0',
backgroundColor: '#ECEFF1',
surfaceColor: '#FAFAFA',
textPrimary: '#263238',
textSecondary: '#607D8B',
// Status colors
errorColor: '#D50000',
warningColor: '#FF6D00',
successColor: '#00C853',
infoColor: '#2962FF',
// Typography
fontFamily: '"Inter", "Helvetica", sans-serif',
fontSizeBase: '15px',
fontWeightLight: 300,
fontWeightRegular: 400,
fontWeightMedium: 500,
fontWeightBold: 700,
// Button typography (from fixture - buttons use this, not fontWeightMedium)
typographyButtonFontWeight: 600,
// UI
borderRadius: '12px',
density: 'normal',
iconStyle: 'rounded',
// Branded header
brandedHeaderEnabled: true,
brandedHeaderBackgroundColor: '#1A237E',
brandedHeaderTextColor: '#E8EAF6',
brandedHeaderHeight: '56px',
// Sidebar
sidebarBackgroundColor: '#1B5E20',
sidebarTextColor: '#C8E6C9',
sidebarIconColor: '#A5D6A7',
sidebarSelectedBackgroundColor: '#2E7D32',
sidebarSelectedTextColor: '#FFFFFF',
sidebarHoverBackgroundColor: 'rgba(255, 255, 255, 0.15)',
sidebarDividerColor: 'rgba(255, 255, 255, 0.25)',
// Tables
tableHeaderBackgroundColor: '#7B1FA2',
tableHeaderTextColor: '#FFFFFF',
tableRowHoverColor: '#F3E5F5',
tableBorderColor: '#E1BEE7',
// General UI
dividerColor: '#CFD8DC',
borderColor: '#B0BEC5',
};
// Helper to format assertion message
function colorMismatch(property: string, expected: string, actual: string): string {
return `${property}: expected ${expected}, got ${actual}`;
}
// ============================================================================
// CORE COLORS - Actual Rendered Values
// ============================================================================
test.describe('Core Colors - Rendered', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/testApp/person');
await page.waitForSelector('[id="root"]');
await page.waitForTimeout(2500);
});
test('primaryColor: CSS variable is set correctly', async ({ page }) => {
const result = await page.evaluate(() => {
// Check if CSS variable is properly set - this is the authoritative source
// Note: This app uses "info" colored buttons, not "primary", so we verify
// the CSS variable rather than button backgrounds
const cssVar = getComputedStyle(document.documentElement).getPropertyValue('--qqq-primary-color').trim();
if (cssVar) {
return { found: true, value: cssVar, debug: 'CSS variable --qqq-primary-color' };
}
// Fallback: Try to find primary-styled buttons
const selectors = [
'.MuiButton-containedPrimary',
'button[class*="containedPrimary"]',
'.MuiButton-root[class*="Primary"]'
];
for (const sel of selectors) {
const btn = document.querySelector(sel);
if (btn) {
const bg = getComputedStyle(btn).backgroundColor;
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
return { found: true, value: bg, debug: btn.className };
}
}
}
return { found: false, value: '', debug: 'No primary color CSS variable or primary button found' };
});
expect(result.found, `Primary color not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.primaryColor), colorMismatch('primaryColor', THEME.primaryColor, result.value)).toBe(true);
});
test('secondaryColor: links use configured color', async ({ page }) => {
const result = await page.evaluate(() => {
const el = document.querySelector('a.MuiLink-root, main a:not(.MuiButton-root), .MuiTypography-root a');
if (!el) return { found: false, value: '', debug: 'No link found' };
return { found: true, value: getComputedStyle(el).color, debug: el.className };
});
expect(result.found, `Link not found: ${result.debug}`).toBe(true);
// Accept either primary or secondary color for links
const matchesPrimary = colorsMatch(result.value, THEME.primaryColor);
const matchesSecondary = colorsMatch(result.value, THEME.secondaryColor);
expect(matchesPrimary || matchesSecondary, colorMismatch('secondaryColor', THEME.secondaryColor, result.value)).toBe(true);
});
test('backgroundColor: page body uses configured background', async ({ page }) => {
const result = await page.evaluate(() => {
return { value: getComputedStyle(document.body).backgroundColor };
});
expect(colorsMatch(result.value, THEME.backgroundColor), colorMismatch('backgroundColor', THEME.backgroundColor, result.value)).toBe(true);
});
test('surfaceColor: cards use configured background', async ({ page }) => {
const result = await page.evaluate(() => {
// Find cards/paper in main content, NOT in the drawer (sidebar)
const el = document.querySelector('main .MuiCard-root, main .MuiPaper-root, .MuiCard-root:not(.MuiDrawer-paper):not(.MuiDrawer-root *)');
if (!el) return { found: false, value: '', debug: 'No card/paper found in main content' };
return { found: true, value: getComputedStyle(el).backgroundColor, debug: el.className };
});
expect(result.found, `Card/Paper not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.surfaceColor), colorMismatch('surfaceColor', THEME.surfaceColor, result.value)).toBe(true);
});
test('textPrimary: headings use configured color', async ({ page }) => {
const result = await page.evaluate(() => {
// Find headings NOT in the sidebar/drawer
const allHeadings = document.querySelectorAll('h1, h2, h3, h4, h5, h6, .MuiTypography-h1, .MuiTypography-h2, .MuiTypography-h3, .MuiTypography-h4, .MuiTypography-h5, .MuiTypography-h6');
for (const el of allHeadings) {
// Skip headings inside drawer/sidebar
if (el.closest('.MuiDrawer-root') || el.closest('.MuiDrawer-paper')) continue;
return { found: true, value: getComputedStyle(el).color, debug: el.className };
}
return { found: false, value: '', debug: 'No heading found outside sidebar' };
});
expect(result.found, `Heading not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.textPrimary), colorMismatch('textPrimary', THEME.textPrimary, result.value)).toBe(true);
});
test('textSecondary: body2/caption use configured color', async ({ page }) => {
const result = await page.evaluate(() => {
// Find body2/caption NOT in sidebar, drawer, or branded header
const allElements = document.querySelectorAll('.MuiTypography-body2, .MuiTypography-caption, .MuiTypography-subtitle2');
for (const el of allElements) {
// Skip elements inside drawer, sidebar, or branded header
if (el.closest('.MuiDrawer-root') || el.closest('.MuiDrawer-paper') || el.closest('.qqq-branded-header-bar')) continue;
return { found: true, value: getComputedStyle(el).color, debug: el.className };
}
// Fallback: check if the CSS variable is properly set
const cssVar = getComputedStyle(document.documentElement).getPropertyValue('--qqq-text-secondary').trim();
if (cssVar) {
return { found: true, value: cssVar, debug: 'CSS variable --qqq-text-secondary' };
}
return { found: false, value: '', debug: 'No body2/caption found outside sidebar/header and no CSS variable' };
});
expect(result.found, `Secondary text not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.textSecondary), colorMismatch('textSecondary', THEME.textSecondary, result.value)).toBe(true);
});
});
// ============================================================================
// BRANDED HEADER - Actual Rendered Values
// ============================================================================
test.describe('Branded Header - Rendered', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForSelector('[id="root"]');
await page.waitForTimeout(2000);
});
test('brandedHeaderBackgroundColor: header uses configured background', async ({ page }) => {
const result = await page.evaluate(() => {
const selectors = ['.qqq-branded-header-bar', '[class*="NavBar"]', 'header', '.MuiAppBar-root'];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) {
const bg = getComputedStyle(el).backgroundColor;
if (bg && bg !== 'rgba(0, 0, 0, 0)') {
return { found: true, value: bg, selector: sel };
}
}
}
return { found: false, value: '', selector: '' };
});
expect(result.found, 'Branded header not found').toBe(true);
expect(colorsMatch(result.value, THEME.brandedHeaderBackgroundColor), colorMismatch('brandedHeaderBackgroundColor', THEME.brandedHeaderBackgroundColor, result.value)).toBe(true);
});
test('brandedHeaderTextColor: header text uses configured color', async ({ page }) => {
const result = await page.evaluate(() => {
// Specifically look for the branded header bar
const header = document.querySelector('.qqq-branded-header-bar');
if (!header) return { found: false, value: '', debug: 'No .qqq-branded-header-bar found' };
// First try to find a typography element with actual text
const textElements = header.querySelectorAll('.MuiTypography-root, span:not(:empty)');
for (const el of textElements) {
const text = el.textContent?.trim();
if (text && text.length > 0) {
return { found: true, value: getComputedStyle(el).color, debug: `${el.className}: "${text.substring(0, 20)}"` };
}
}
// Fallback: check the CSS variable is set correctly
const cssVar = getComputedStyle(document.documentElement).getPropertyValue('--qqq-branded-header-text-color').trim();
if (cssVar) {
return { found: true, value: cssVar, debug: 'CSS variable --qqq-branded-header-text-color' };
}
return { found: false, value: '', debug: 'No text element in branded header' };
});
expect(result.found, `Header text not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.brandedHeaderTextColor), colorMismatch('brandedHeaderTextColor', THEME.brandedHeaderTextColor, result.value)).toBe(true);
});
test('brandedHeaderHeight: header uses configured height', async ({ page }) => {
const result = await page.evaluate(() => {
const header = document.querySelector('.qqq-branded-header-bar')
|| document.querySelector('[class*="NavBar"]')
|| document.querySelector('header');
if (!header) return { found: false, value: '', debug: 'No header found' };
return { found: true, value: getComputedStyle(header).height, debug: '' };
});
expect(result.found, `Header not found: ${result.debug}`).toBe(true);
expect(result.value, colorMismatch('brandedHeaderHeight', THEME.brandedHeaderHeight, result.value)).toBe(THEME.brandedHeaderHeight);
});
});
// ============================================================================
// SIDEBAR - Actual Rendered Values
// ============================================================================
test.describe('Sidebar - Rendered', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForSelector('[id="root"]');
await page.waitForTimeout(2000);
});
test('sidebarBackgroundColor: drawer uses configured background', async ({ page }) => {
const result = await page.evaluate(() => {
const el = document.querySelector('.MuiDrawer-paper');
if (!el) return { found: false, value: '', debug: 'No .MuiDrawer-paper found' };
return { found: true, value: getComputedStyle(el).backgroundColor, debug: '' };
});
expect(result.found, `Sidebar drawer not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.sidebarBackgroundColor), colorMismatch('sidebarBackgroundColor', THEME.sidebarBackgroundColor, result.value)).toBe(true);
});
test('sidebarTextColor: nav item text uses configured color', async ({ page }) => {
const result = await page.evaluate(() => {
const el = document.querySelector('.MuiDrawer-paper .MuiListItemText-primary, .MuiDrawer-paper .MuiListItemText-root span');
if (!el) return { found: false, value: '', debug: 'No sidebar text found' };
return { found: true, value: getComputedStyle(el).color, debug: el.className };
});
expect(result.found, `Sidebar text not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.sidebarTextColor), colorMismatch('sidebarTextColor', THEME.sidebarTextColor, result.value)).toBe(true);
});
test('sidebarIconColor: nav item icons use configured color', async ({ page }) => {
const result = await page.evaluate(() => {
const el = document.querySelector('.MuiDrawer-paper .MuiSvgIcon-root, .MuiDrawer-paper svg, .MuiDrawer-paper .MuiIcon-root');
if (!el) return { found: false, value: '', debug: 'No sidebar icon found' };
return { found: true, value: getComputedStyle(el).color, debug: el.className };
});
expect(result.found, `Sidebar icon not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.sidebarIconColor), colorMismatch('sidebarIconColor', THEME.sidebarIconColor, result.value)).toBe(true);
});
test('sidebarSelectedBackgroundColor: selected nav item uses configured background', async ({ page }) => {
// Click on a nav item to select it
await page.click('.MuiDrawer-paper .MuiListItem-root');
await page.waitForTimeout(500);
const result = await page.evaluate(() => {
const el = document.querySelector('.MuiDrawer-paper .qqq-sidebar-active');
if (!el) return { found: false, value: '', debug: 'No .qqq-sidebar-active found' };
return { found: true, value: getComputedStyle(el).backgroundColor, debug: el.className };
});
expect(result.found, `Selected sidebar item not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.sidebarSelectedBackgroundColor), colorMismatch('sidebarSelectedBackgroundColor', THEME.sidebarSelectedBackgroundColor, result.value)).toBe(true);
});
test('sidebarSelectedTextColor: selected nav item text uses configured color', async ({ page }) => {
await page.click('.MuiDrawer-paper .MuiListItem-root');
await page.waitForTimeout(500);
const result = await page.evaluate(() => {
const selected = document.querySelector('.MuiDrawer-paper .qqq-sidebar-active');
if (!selected) return { found: false, value: '', debug: 'No .qqq-sidebar-active found' };
const text = selected.querySelector('.MuiTypography-root, span') || selected;
return { found: true, value: getComputedStyle(text).color, debug: (text as Element).className };
});
expect(result.found, `Selected sidebar text not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.sidebarSelectedTextColor), colorMismatch('sidebarSelectedTextColor', THEME.sidebarSelectedTextColor, result.value)).toBe(true);
});
test('sidebarDividerColor: sidebar dividers use configured color', async ({ page }) => {
const result = await page.evaluate(() => {
const el = document.querySelector('.MuiDrawer-paper .MuiDivider-root, .MuiDrawer-paper hr');
if (!el) return { found: false, value: '', debug: 'No sidebar divider found' };
const styles = getComputedStyle(el);
return { found: true, value: styles.borderColor || styles.backgroundColor, debug: el.className };
});
expect(result.found, `Sidebar divider not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.sidebarDividerColor), colorMismatch('sidebarDividerColor', THEME.sidebarDividerColor, result.value)).toBe(true);
});
});
// ============================================================================
// TABLES (MUI DataGrid) - Actual Rendered Values
// ============================================================================
test.describe('Tables - Rendered', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/testApp/person');
await page.waitForSelector('[id="root"]');
await page.waitForTimeout(2500);
});
test('tableHeaderBackgroundColor: DataGrid header cells use configured background', async ({ page }) => {
const result = await page.evaluate(() => {
const el = document.querySelector('.MuiDataGrid-columnHeader');
if (!el) return { found: false, value: '', debug: 'No .MuiDataGrid-columnHeader found' };
return { found: true, value: getComputedStyle(el).backgroundColor, debug: '' };
});
expect(result.found, `DataGrid column header not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.tableHeaderBackgroundColor), colorMismatch('tableHeaderBackgroundColor', THEME.tableHeaderBackgroundColor, result.value)).toBe(true);
});
test('tableHeaderTextColor: DataGrid header text uses configured color', async ({ page }) => {
const result = await page.evaluate(() => {
const el = document.querySelector('.MuiDataGrid-columnHeaderTitle');
if (!el) return { found: false, value: '', debug: 'No .MuiDataGrid-columnHeaderTitle found' };
return { found: true, value: getComputedStyle(el).color, debug: '' };
});
expect(result.found, `DataGrid header text not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.tableHeaderTextColor), colorMismatch('tableHeaderTextColor', THEME.tableHeaderTextColor, result.value)).toBe(true);
});
test('tableBorderColor: DataGrid cells use configured border color', async ({ page }) => {
const result = await page.evaluate(() => {
const cell = document.querySelector('.MuiDataGrid-cell');
if (!cell) return { found: false, value: '', debug: 'No .MuiDataGrid-cell found' };
return { found: true, value: getComputedStyle(cell).borderColor, debug: '' };
});
expect(result.found, `DataGrid cell not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.tableBorderColor), colorMismatch('tableBorderColor', THEME.tableBorderColor, result.value)).toBe(true);
});
});
// ============================================================================
// TYPOGRAPHY - Actual Rendered Values
// ============================================================================
test.describe('Typography - Rendered', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForSelector('[id="root"]');
await page.waitForTimeout(2000);
});
test('fontFamily: body uses configured font family', async ({ page }) => {
const result = await page.evaluate(() => {
return { value: getComputedStyle(document.body).fontFamily };
});
expect(result.value, 'fontFamily should contain Inter').toMatch(/Inter/i);
});
test('fontSizeBase: body uses configured font size', async ({ page }) => {
const result = await page.evaluate(() => {
return { value: getComputedStyle(document.body).fontSize };
});
expect(result.value, colorMismatch('fontSizeBase', THEME.fontSizeBase, result.value)).toBe(THEME.fontSizeBase);
});
test('fontWeightRegular: body text uses configured weight', async ({ page }) => {
const result = await page.evaluate(() => {
const el = document.querySelector('.MuiTypography-body1, p, span');
if (!el) return { found: false, value: '', debug: 'No body text found' };
return { found: true, value: getComputedStyle(el).fontWeight, debug: el.className };
});
expect(result.found, `Body text not found: ${result.debug}`).toBe(true);
expect(result.value, colorMismatch('fontWeightRegular', String(THEME.fontWeightRegular), result.value)).toBe(String(THEME.fontWeightRegular));
});
test('typographyButtonFontWeight: buttons use configured weight', async ({ page }) => {
await page.goto('/testApp/person');
await page.waitForTimeout(2500);
const result = await page.evaluate(() => {
const btn = document.querySelector('.MuiButton-root');
if (!btn) return { found: false, value: '', debug: 'No button found' };
return { found: true, value: getComputedStyle(btn).fontWeight, debug: btn.className };
});
expect(result.found, `Button not found: ${result.debug}`).toBe(true);
// Buttons use typographyButtonFontWeight, NOT fontWeightMedium
expect(result.value, colorMismatch('typographyButtonFontWeight', String(THEME.typographyButtonFontWeight), result.value)).toBe(String(THEME.typographyButtonFontWeight));
});
});
// ============================================================================
// UI ELEMENTS - Actual Rendered Values
// ============================================================================
test.describe('UI Elements - Rendered', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/testApp/person');
await page.waitForSelector('[id="root"]');
await page.waitForTimeout(2500);
});
test('borderRadius: buttons use configured border radius', async ({ page }) => {
const result = await page.evaluate(() => {
const btn = document.querySelector('.MuiButton-root');
if (!btn) return { found: false, value: '', debug: 'No button found' };
return { found: true, value: getComputedStyle(btn).borderRadius, debug: btn.className };
});
expect(result.found, `Button not found: ${result.debug}`).toBe(true);
expect(result.value, colorMismatch('borderRadius', THEME.borderRadius, result.value)).toBe(THEME.borderRadius);
});
test('borderColor: inputs use configured border color', async ({ page }) => {
const result = await page.evaluate(() => {
const el = document.querySelector('.MuiOutlinedInput-notchedOutline');
if (!el) return { found: false, value: '', debug: 'No .MuiOutlinedInput-notchedOutline found' };
return { found: true, value: getComputedStyle(el).borderColor, debug: '' };
});
expect(result.found, `Input border not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.borderColor), colorMismatch('borderColor', THEME.borderColor, result.value)).toBe(true);
});
test('dividerColor: dividers use configured color', async ({ page }) => {
const result = await page.evaluate(() => {
// Find a divider NOT in the drawer (sidebar has its own divider color)
const selectors = [
'main .MuiDivider-root',
'.MuiDivider-root:not(.MuiDrawer-root *):not(.MuiDrawer-paper *)'
];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) {
const styles = getComputedStyle(el);
return { found: true, value: styles.borderColor || styles.backgroundColor, debug: el.className };
}
}
// If no divider element found, check if the CSS variable is properly set
const cssVar = getComputedStyle(document.documentElement).getPropertyValue('--qqq-divider-color').trim();
if (cssVar) {
return { found: true, value: cssVar, debug: 'CSS variable --qqq-divider-color' };
}
return { found: false, value: '', debug: 'No divider found outside drawer' };
});
expect(result.found, `Divider not found: ${result.debug}`).toBe(true);
expect(colorsMatch(result.value, THEME.dividerColor), colorMismatch('dividerColor', THEME.dividerColor, result.value)).toBe(true);
});
});
// ============================================================================
// DIAGNOSTIC DUMP - outputs actual values for debugging
// ============================================================================
test.describe('Diagnostic Dump', () => {
test('output all rendered theme values', async ({ page }) => {
await page.goto('/testApp/person');
await page.waitForSelector('[id="root"]');
await page.waitForTimeout(2500);
const diagnostics = await page.evaluate(() => {
const getColor = (selector: string, prop: string) => {
const el = document.querySelector(selector);
if (!el) return { found: false, value: 'NOT FOUND' };
return { found: true, value: getComputedStyle(el)[prop as any] };
};
return {
primaryButton: getColor('.MuiButton-containedPrimary', 'backgroundColor'),
link: getColor('a.MuiLink-root, main a', 'color'),
body: getColor('body', 'backgroundColor'),
card: getColor('.MuiCard-root, .MuiPaper-root', 'backgroundColor'),
heading: getColor('h1, h2, h3, h4', 'color'),
body2: getColor('.MuiTypography-body2', 'color'),
brandedHeader: getColor('.qqq-branded-header-bar', 'backgroundColor'),
brandedHeaderText: getColor('.qqq-branded-header-bar span', 'color'),
sidebarBg: getColor('.MuiDrawer-paper', 'backgroundColor'),
sidebarText: getColor('.MuiDrawer-paper .MuiListItemText-primary', 'color'),
sidebarIcon: getColor('.MuiDrawer-paper .MuiSvgIcon-root', 'color'),
sidebarActive: getColor('.qqq-sidebar-active', 'backgroundColor'),
sidebarActiveText: getColor('.qqq-sidebar-active .MuiTypography-root', 'color'),
tableHeader: getColor('.MuiDataGrid-columnHeader', 'backgroundColor'),
tableHeaderText: getColor('.MuiDataGrid-columnHeaderTitle', 'color'),
tableCell: getColor('.MuiDataGrid-cell', 'borderColor'),
inputBorder: getColor('.MuiOutlinedInput-notchedOutline', 'borderColor'),
buttonRadius: getColor('.MuiButton-root', 'borderRadius'),
buttonWeight: getColor('.MuiButton-root', 'fontWeight'),
bodyWeight: getColor('.MuiTypography-body1', 'fontWeight'),
};
});
console.log('=== DIAGNOSTIC OUTPUT ===');
console.log(JSON.stringify(diagnostics, null, 2));
console.log('=========================');
// This test always passes - it's just for diagnostics
expect(true).toBe(true);
});
});
+421
View File
@@ -0,0 +1,421 @@
import { test, expect } from '@playwright/test';
import {colorsMatch, hexToRgb, isTransparentOrNearlyTransparent, parseRgb} from "../utils/color-helpers";
/**
* STRICT Visual Regression Tests for UNTHEMED Apps (Issue #128)
*
* These tests verify that apps WITHOUT a backend theme look correct.
* Run with: THEME_FIXTURE=index npm run e2e:unthemed
*
* IMPORTANT: These tests require the 'index' fixture (no theme configured).
* They will be SKIPPED if running with a themed fixture (e.g., withFullCustomTheme).
*
* To run these tests:
* npm run e2e:unthemed (uses THEME_FIXTURE=index)
*
* To run all tests properly:
* npm run e2e:all (runs themed then unthemed with correct fixtures)
*
* Tests verify:
* 1. Sidebar has correct DEFAULT_THEME colors (dark background, white text)
* 2. Navbar/AppBar is NOT white (transparent, not affected by surfaceColor)
* 3. CSS variables are injected with DEFAULT_THEME values
* 4. Cards/Paper get surfaceColor but AppBar does not
*/
// Helper function to check if themed fixture is loaded and skip if so
async function skipIfThemedFixture(page: import('@playwright/test').Page) {
const hasTheme = await page.evaluate(() => {
const primaryColor = getComputedStyle(document.documentElement)
.getPropertyValue('--qqq-primary-color').trim();
return primaryColor !== '';
});
if (hasTheme) {
test.skip(true, 'Skipping unthemed test: themed fixture detected. Run with: npm run e2e:unthemed');
}
}
// DEFAULT_THEME values from src/qqq/utils/themeUtils.ts
// These must match the values in themeUtils.ts to restore pre-PR-125 appearance
const DEFAULT_THEME = {
// Core colors
primaryColor: '#0062FF',
backgroundColor: '#f0f2f5',
surfaceColor: '#ffffff',
// Sidebar (dark gray matching original gradients.dark.main)
sidebarBackgroundColor: '#42424a',
sidebarTextColor: '#ffffff',
sidebarIconColor: '#ffffff',
sidebarSelectedBackgroundColor: 'rgba(255, 255, 255, 0.2)',
sidebarSelectedTextColor: '#ffffff',
sidebarHoverBackgroundColor: 'rgba(255, 255, 255, 0.2)',
// Typography (matching original typography.ts)
fontFamily: '"SF Pro Display", "Roboto", "Helvetica", "Arial", sans-serif',
};
// Helper to check if color is NOT white (surfaceColor)
function isNotWhite(color: string): boolean {
// Check if transparent
if (isTransparentOrNearlyTransparent(color)) {
return true;
}
// Check if NOT white (#ffffff or rgb(255,255,255))
const actualRgb = parseRgb(color);
if (!actualRgb) {
if (color.startsWith('#')) {
const hex = hexToRgb(color);
if (!hex) {
return true;
}
return !(hex.r > 250 && hex.g > 250 && hex.b > 250);
}
return true;
}
// Allow some tolerance - if it's close to white, it's a problem
return !(actualRgb.r > 250 && actualRgb.g > 250 && actualRgb.b > 250);
}
// Helper to check if a background (which may be a gradient) contains a color
function backgroundContainsColor(background: string, expectedColor: string, tolerance = 5): boolean {
// Extract all rgb/rgba values from the background string
const rgbMatches = background.matchAll(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/g);
const expectedRgb = expectedColor.startsWith('#') ? hexToRgb(expectedColor) : parseRgb(expectedColor);
if (!expectedRgb) return false;
for (const match of rgbMatches) {
const r = parseInt(match[1]);
const g = parseInt(match[2]);
const b = parseInt(match[3]);
if (
Math.abs(r - expectedRgb.r) <= tolerance &&
Math.abs(g - expectedRgb.g) <= tolerance &&
Math.abs(b - expectedRgb.b) <= tolerance
) {
return true;
}
}
return false;
}
// ============================================================================
// CSS VARIABLES - Verify they are NOT injected for unthemed apps
// CSS variables are 100% optional - CSS uses fallback values
// ============================================================================
test.describe('Unthemed App - CSS Variables Are Optional', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForSelector('[id="root"]');
await page.waitForTimeout(1000);
await skipIfThemedFixture(page);
});
test('no --qqq-* CSS variables are injected when no theme is configured', async ({ page }) => {
const result = await page.evaluate(() => {
const root = document.documentElement;
const style = getComputedStyle(root);
return {
sidebarBg: style.getPropertyValue('--qqq-sidebar-background-color').trim(),
sidebarText: style.getPropertyValue('--qqq-sidebar-text-color').trim(),
surfaceColor: style.getPropertyValue('--qqq-surface-color').trim(),
primaryColor: style.getPropertyValue('--qqq-primary-color').trim(),
};
});
// All CSS variables should be empty (not injected) for unthemed apps
expect(result.sidebarBg, 'CSS variable should not be injected for unthemed app').toBe('');
expect(result.sidebarText, 'CSS variable should not be injected for unthemed app').toBe('');
expect(result.surfaceColor, 'CSS variable should not be injected for unthemed app').toBe('');
expect(result.primaryColor, 'CSS variable should not be injected for unthemed app').toBe('');
});
test('body does NOT have .qqq-themed class when no theme is configured', async ({ page }) => {
const hasClass = await page.evaluate(() => {
return document.body.classList.contains('qqq-themed');
});
expect(hasClass, 'Body should not have .qqq-themed class for unthemed app').toBe(false);
});
});
// ============================================================================
// SIDEBAR - Verify dark theme colors via CSS fallbacks
// ============================================================================
test.describe('Unthemed App - Sidebar Colors', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForSelector('[id="root"]');
await page.waitForTimeout(1000);
await skipIfThemedFixture(page);
});
test('sidebar has dark background via CSS fallback', async ({ page }) => {
const result = await page.evaluate(() => {
const el = document.querySelector('.MuiDrawer-paper');
if (!el) return { found: false, value: '', debug: 'No .MuiDrawer-paper found' };
// Use 'background' not 'backgroundColor' because gradients set background-image
return { found: true, value: getComputedStyle(el).background, debug: '' };
});
expect(result.found, `Sidebar not found: ${result.debug}`).toBe(true);
// Check that the background contains the expected dark color (gradient includes #42424a)
expect(
backgroundContainsColor(result.value, DEFAULT_THEME.sidebarBackgroundColor),
`Sidebar background should contain ${DEFAULT_THEME.sidebarBackgroundColor}, got ${result.value}`
).toBe(true);
});
test('sidebar text is white from DEFAULT_THEME', async ({ page }) => {
const result = await page.evaluate(() => {
const el = document.querySelector('.MuiDrawer-paper .MuiListItemText-primary, .MuiDrawer-paper .MuiTypography-root');
if (!el) return { found: false, value: '', debug: 'No sidebar text found' };
return { found: true, value: getComputedStyle(el).color, debug: el.className };
});
expect(result.found, `Sidebar text not found: ${result.debug}`).toBe(true);
expect(
colorsMatch(result.value, DEFAULT_THEME.sidebarTextColor, 5),
`Sidebar text color: expected ${DEFAULT_THEME.sidebarTextColor}, got ${result.value}`
).toBe(true);
});
test('sidebar icons are white from DEFAULT_THEME', async ({ page }) => {
const result = await page.evaluate(() => {
const el = document.querySelector('.MuiDrawer-paper .MuiIcon-root, .MuiDrawer-paper .material-icons');
if (!el) return { found: false, value: '', debug: 'No sidebar icon found' };
return { found: true, value: getComputedStyle(el).color, debug: el.className };
});
expect(result.found, `Sidebar icon not found: ${result.debug}`).toBe(true);
expect(
colorsMatch(result.value, DEFAULT_THEME.sidebarIconColor, 5),
`Sidebar icon color: expected ${DEFAULT_THEME.sidebarIconColor}, got ${result.value}`
).toBe(true);
});
});
// ============================================================================
// NAVBAR/APPBAR - Verify NOT white (Issue #128 regression)
// ============================================================================
test.describe('Unthemed App - Navbar NOT White (Issue #128)', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForSelector('[id="root"]');
await page.waitForTimeout(1000);
await skipIfThemedFixture(page);
});
test('CRITICAL: AppBar background is NOT white/surfaceColor', async ({ page }) => {
const result = await page.evaluate(() => {
const appBar = document.querySelector('.MuiAppBar-root');
if (!appBar) return { found: false, value: '', debug: 'No .MuiAppBar-root found' };
return { found: true, value: getComputedStyle(appBar).backgroundColor, debug: '' };
});
expect(result.found, `AppBar not found: ${result.debug}`).toBe(true);
expect(
isNotWhite(result.value),
`REGRESSION: AppBar background is white (${result.value}). Should be transparent or non-white.`
).toBe(true);
});
test('CRITICAL: Breadcrumb area is NOT white', async ({ page }) => {
const result = await page.evaluate(() => {
// Find the breadcrumb container or navbar area
const selectors = [
'.MuiBreadcrumbs-root',
'.MuiToolbar-root',
'.MuiAppBar-root .MuiBox-root'
];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) {
const bg = getComputedStyle(el).backgroundColor;
return { found: true, value: bg, selector: sel };
}
}
return { found: false, value: '', selector: '' };
});
expect(result.found, 'Breadcrumb/toolbar area not found').toBe(true);
expect(
isNotWhite(result.value),
`REGRESSION: Breadcrumb area (${result.selector}) is white (${result.value}). Should not be white.`
).toBe(true);
});
test('AppBar does not have --qqq-surface-color applied', async ({ page }) => {
const result = await page.evaluate(() => {
const appBar = document.querySelector('.MuiAppBar-root');
if (!appBar) return { found: false, isSurfaceColor: false, value: '' };
const bg = getComputedStyle(appBar).backgroundColor;
// Check if it matches surfaceColor (#ffffff)
const isSurfaceColor = bg === 'rgb(255, 255, 255)' || bg === '#ffffff';
return { found: true, isSurfaceColor, value: bg };
});
expect(result.found, 'AppBar not found').toBe(true);
expect(
result.isSurfaceColor,
`REGRESSION: AppBar has surfaceColor (#ffffff) applied. Value: ${result.value}`
).toBe(false);
});
});
// ============================================================================
// CARDS/PAPER - Verify surfaceColor IS applied (but not to AppBar)
// ============================================================================
test.describe('Unthemed App - Cards Use Surface Color', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/testApp/person');
await page.waitForSelector('[id="root"]');
await page.waitForTimeout(2500);
await skipIfThemedFixture(page);
});
test('Cards in main content use surfaceColor', async ({ page }) => {
const result = await page.evaluate(() => {
// Find a card in main content, NOT in drawer/sidebar
const el = document.querySelector('main .MuiCard-root, .MuiCard-root:not(.MuiDrawer-paper *)');
if (!el) return { found: false, value: '', debug: 'No card found in main content' };
return { found: true, value: getComputedStyle(el).backgroundColor, debug: el.className };
});
expect(result.found, `Card not found: ${result.debug}`).toBe(true);
expect(
colorsMatch(result.value, DEFAULT_THEME.surfaceColor, 5),
`Card background: expected ${DEFAULT_THEME.surfaceColor}, got ${result.value}`
).toBe(true);
});
});
// ============================================================================
// BRANDED HEADER - Verify NOT shown when disabled
// ============================================================================
test.describe('Unthemed App - Branded Header Disabled', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForSelector('[id="root"]');
await page.waitForTimeout(2000);
await skipIfThemedFixture(page);
});
test('branded header is not visible when not configured', async ({ page }) => {
const result = await page.evaluate(() => {
const header = document.querySelector('.qqq-branded-header-bar');
if (!header) return { exists: false, height: '0' };
const height = getComputedStyle(header).height;
const display = getComputedStyle(header).display;
return { exists: true, height, display };
});
// Either the element doesn't exist, or it has 0 height, or it's hidden
const isHidden = !result.exists ||
result.height === '0px' ||
result.height === '0' ||
result.display === 'none';
expect(isHidden, `Branded header should not be visible. Height: ${result.height}`).toBe(true);
});
test('--qqq-branded-header-height is not set (CSS fallback to 0px)', async ({ page }) => {
const result = await page.evaluate(() => {
const value = getComputedStyle(document.documentElement)
.getPropertyValue('--qqq-branded-header-height').trim();
return { value };
});
// CSS variable should be empty (not injected) - CSS fallback handles it
expect(
result.value === '' || result.value === '0px' || result.value === '0',
`--qqq-branded-header-height should be empty or 0px, got ${result.value}`
).toBe(true);
});
test('sidebar does not overhang content area', async ({ page }) => {
const result = await page.evaluate(() => {
const sidebar = document.querySelector('.MuiDrawer-paper');
// Try multiple selectors for main content area
const mainContent = document.querySelector('main') ||
document.querySelector('[class*="content"]') ||
document.querySelector('.MuiBox-root > .MuiBox-root');
if (!sidebar) {
return { found: false, sidebarTop: 0, contentTop: 0, debug: 'No sidebar found' };
}
const sidebarRect = sidebar.getBoundingClientRect();
// If we can't find main content, just verify sidebar starts at top (no overhang)
if (!mainContent) {
// Sidebar should start at or near the top of viewport
return {
found: true,
sidebarTop: sidebarRect.top,
contentTop: 0,
isAligned: sidebarRect.top <= 100, // Should be near top
debug: 'Using viewport top as reference'
};
}
const contentRect = mainContent.getBoundingClientRect();
return {
found: true,
sidebarTop: sidebarRect.top,
contentTop: contentRect.top,
// Sidebar should start at same level or below content
isAligned: sidebarRect.top <= contentRect.top + 50,
debug: 'Found main content'
};
});
expect(result.found, `Sidebar not found: ${result.debug}`).toBe(true);
expect(
result.isAligned,
`Sidebar overhang: sidebar top (${result.sidebarTop}) should align with content top (${result.contentTop}). Debug: ${result.debug}`
).toBe(true);
});
});
// ============================================================================
// DIAGNOSTIC - Output actual values for debugging
// ============================================================================
test.describe('Unthemed App - Diagnostic Dump', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForSelector('[id="root"]');
await page.waitForTimeout(2000);
await skipIfThemedFixture(page);
});
test('output all CSS variables and rendered values', async ({ page }) => {
const diagnostics = await page.evaluate(() => {
const root = document.documentElement;
const getVar = (name: string) => getComputedStyle(root).getPropertyValue(name).trim();
const getBg = (selector: string) => {
const el = document.querySelector(selector);
return el ? getComputedStyle(el).backgroundColor : 'NOT FOUND';
};
const getColor = (selector: string) => {
const el = document.querySelector(selector);
return el ? getComputedStyle(el).color : 'NOT FOUND';
};
return {
cssVariables: {
'--qqq-sidebar-background-color': getVar('--qqq-sidebar-background-color'),
'--qqq-sidebar-text-color': getVar('--qqq-sidebar-text-color'),
'--qqq-surface-color': getVar('--qqq-surface-color'),
'--qqq-primary-color': getVar('--qqq-primary-color'),
'--qqq-branded-header-height': getVar('--qqq-branded-header-height'),
'--qqq-branded-header-enabled': getVar('--qqq-branded-header-enabled'),
},
renderedValues: {
sidebarBackground: getBg('.MuiDrawer-paper'),
sidebarText: getColor('.MuiDrawer-paper .MuiTypography-root'),
appBarBackground: getBg('.MuiAppBar-root'),
toolbarBackground: getBg('.MuiToolbar-root'),
cardBackground: getBg('.MuiCard-root'),
bodyBackground: getBg('body'),
}
};
});
console.log('=== UNTHEMED APP DIAGNOSTIC ===');
console.log(JSON.stringify(diagnostics, null, 2));
console.log('===============================');
// This test always passes - diagnostic only
expect(true).toBe(true);
});
});
+110
View File
@@ -0,0 +1,110 @@
export type RgbColor = { r: number; g: number; b: number; a?: number };
/**
* Utility functions to help with assertions in theme tests
*/
export function hexToRgb(hex: string): { r: number; g: number; b: number } | null
{
const normalized = hex.trim().replace(/^#/, "");
if (!/^[a-f\d]+$/i.test(normalized))
{
return null;
}
let rHex: string;
let gHex: string;
let bHex: string;
if (normalized.length === 3)
{
rHex = normalized[0] + normalized[0];
gHex = normalized[1] + normalized[1];
bHex = normalized[2] + normalized[2];
}
else if (normalized.length === 6 || normalized.length === 8)
{
// Ignore alpha channel for 8-digit hex values (#RRGGBBAA).
rHex = normalized.slice(0, 2);
gHex = normalized.slice(2, 4);
bHex = normalized.slice(4, 6);
}
else
{
return null;
}
return {
r: parseInt(rHex, 16),
g: parseInt(gHex, 16),
b: parseInt(bHex, 16),
};
}
export function parseRgb(rgbString: string): RgbColor | null
{
const rgbMatch = rgbString.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
if (rgbMatch)
{
return {
r: parseInt(rgbMatch[1]),
g: parseInt(rgbMatch[2]),
b: parseInt(rgbMatch[3]),
a: rgbMatch[4] ? parseFloat(rgbMatch[4]) : undefined,
};
}
return null;
}
function parseColor(color: string): RgbColor | null
{
if (color.startsWith("rgb"))
{
return parseRgb(color);
}
if (color.startsWith("#"))
{
const rgb = hexToRgb(color);
return rgb ? {...rgb, a: undefined} : null;
}
return null;
}
export function colorsMatch(actual: string, expected: string, tolerance = 2): boolean
{
if (actual.startsWith("#") && expected.startsWith("#"))
{
return actual.toLowerCase() === expected.toLowerCase();
}
const actualRgb = parseColor(actual);
const expectedRgb = parseColor(expected);
if (!actualRgb || !expectedRgb)
{
return false;
}
const rgbMatch = (
Math.abs(actualRgb.r - expectedRgb.r) <= tolerance &&
Math.abs(actualRgb.g - expectedRgb.g) <= tolerance &&
Math.abs(actualRgb.b - expectedRgb.b) <= tolerance
);
if (actualRgb.a !== undefined && expectedRgb.a !== undefined)
{
return rgbMatch && Math.abs(actualRgb.a - expectedRgb.a) <= 0.05;
}
return rgbMatch;
}
export function isTransparentOrNearlyTransparent(color: string): boolean
{
if (color === "transparent" || color === "rgba(0, 0, 0, 0)")
{
return true;
}
const rgb = parseRgb(color);
return !!(rgb && rgb.a !== undefined && rgb.a < 0.1);
}
+1808 -1297
View File
File diff suppressed because it is too large. Load diff
+28 -9
View File
@@ -1,6 +1,7 @@
{
"name": "qqq-frontend-material-dashboard",
"version": "0.40.0-SNAPSHOT",
"license": "Apache-2.0",
"version": "0.41.0-SNAPSHOT",
"homepage": ".",
"description": "QQQ Default Dashboard",
"dependencies": {
@@ -15,7 +16,7 @@
"@mui/x-data-grid-pro": "5.17.23",
"@mui/x-date-pickers": "7.1.1",
"@mui/x-license-pro": "5.12.3",
"@qrunio/qqq-frontend-core": "0.40.4-SNAPSHOT",
"@qrunio/qqq-frontend-core": "0.40.20-SNAPSHOT",
"@react-jvectormap/core": "1.0.1",
"@react-jvectormap/unitedstates": "1.0.1",
"@react-oauth/google": "0.2.8",
@@ -31,13 +32,13 @@
"dayjs": "1.11.10",
"downshift": "3.2.10",
"faker": "5.5.3",
"form-data": "4.0.4",
"form-data": "4.0.6",
"formik": "2.2.9",
"html-react-parser": "1.4.8",
"html-to-text": "^9.0.5",
"http-proxy-middleware": "2.0.9",
"jwt-decode": "3.1.2",
"lodash": "4.17.21",
"lodash": "4.18.1",
"oidc-client-ts": "2.4.1",
"rapidoc": "9.3.4",
"react": "18.0.0",
@@ -62,7 +63,7 @@
},
"scripts": {
"build": "react-scripts build",
"build-and-publish": "react-scripts build && rm -rvf src/main/resources/material-dashboard; mkdir -p src/main/resources/material-dashboard; cp -rv build/* src/main/resources/material-dashboard",
"build-and-publish": "react-scripts build && rm -rvf src/main/resources/material-dashboard && mkdir -p src/main/resources/material-dashboard && cp -rv build/* src/main/resources/material-dashboard",
"clean": "rm -rf node_modules package-lock.json lib",
"eject": "react-scripts eject",
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps && npm dedupe --force",
@@ -75,7 +76,11 @@
"test:e2e:ui": "npx playwright test --ui",
"test:e2e:headed": "npx playwright test --headed",
"fixture-server": "node e2e/fixture-server.js",
"e2e": "concurrently --kill-others \"npm run fixture-server\" \"npx playwright test\""
"fixture-server:unthemed": "THEME_FIXTURE=index node e2e/fixture-server.js",
"e2e": "npx playwright test",
"e2e:themed": "THEME_FIXTURE=withFullCustomTheme npx playwright test e2e/tests/theme.spec.ts",
"e2e:unthemed": "THEME_FIXTURE=index npx playwright test e2e/tests/unthemed-regression.spec.ts",
"e2e:all": "npm run e2e:themed && npm run e2e:unthemed"
},
"eslintConfig": {
"extends": [
@@ -101,13 +106,13 @@
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
"@playwright/test": "^1.57.0",
"concurrently": "^8.2.0",
"@svgr/webpack": "6.5.1",
"@types/chroma-js": "2.1.3",
"@types/faker": "5.5.9",
"@types/react-table": "7.7.9",
"@typescript-eslint/eslint-plugin": "5.10.2",
"@typescript-eslint/parser": "5.10.2",
"concurrently": "^8.2.0",
"eslint": "8.8.0",
"eslint-import-resolver-typescript": "2.5.0",
"eslint-plugin-import": "2.25.4",
@@ -116,12 +121,26 @@
"typescript": "4.9.4"
},
"overrides": {
"@svgr/webpack": "$@svgr/webpack"
"@svgr/webpack": "$@svgr/webpack",
"axios@<1.0.0": "0.34.0",
"form-data@^4.0.0": "$form-data",
"resolve-url-loader": "5.0.0",
"serialize-javascript": "7.0.3",
"underscore": "1.13.8",
"webpack-dev-middleware": "7.4.6"
},
"main": "qqq-frontend-material-dashboard.js",
"module": "lib/qqq-frontend-material-dashboard.js",
"files": [
"lib",
"package.json"
]
],
"jest": {
"transformIgnorePatterns": [
"node_modules/(?!(@qrunio/qqq-frontend-core|@mui|@babel/runtime)/)"
]
},
"engines": {
"node": ">=20"
}
}
+16 -7
View File
@@ -1,5 +1,8 @@
import { defineConfig, devices } from '@playwright/test';
const fixturePort = process.env.FIXTURE_PORT || '8001';
const dashboardPort = process.env.DASHBOARD_PORT || '3001';
export default defineConfig({
testDir: './e2e/tests',
fullyParallel: false,
@@ -20,30 +23,36 @@ export default defineConfig({
},
},
use: {
baseURL: 'https://localhost:3001',
baseURL: `https://localhost:${dashboardPort}`,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
ignoreHTTPSErrors: true,
channel: process.env.PLAYWRIGHT_CHANNEL,
// Consistent viewport for visual regression
viewport: { width: 1280, height: 720 },
},
projects: [
{
name: 'chromium',
name: 'themed',
testMatch: /.*(?<!unthemed-regression)\.spec\.ts$/,
use: { ...devices['Desktop Chrome'] },
},
{
name: 'unthemed',
testMatch: /unthemed-regression\.spec\.ts$/,
use: { ...devices['Desktop Chrome'] },
},
],
webServer: [
{
// Pass THEME_FIXTURE through to fixture server for fixture selection
command: `THEME_FIXTURE=${process.env.THEME_FIXTURE || 'index'} node e2e/fixture-server.js`,
url: 'http://localhost:8001/metaData',
command: `THEME_FIXTURE=${process.env.THEME_FIXTURE || 'withFullCustomTheme'} node e2e/fixture-server.js`,
url: `http://localhost:${fixturePort}/metaData`,
reuseExistingServer: true,
timeout: 60000, // 1 minute - Docker startup can be slow
},
{
command: 'HTTPS=true PORT=3001 REACT_APP_PROXY_LOCALHOST_PORT=8001 npm start',
url: 'https://localhost:3001',
command: `HTTPS=true PORT=${dashboardPort} REACT_APP_PROXY_LOCALHOST_PORT=${fixturePort} npm start`,
url: `https://localhost:${dashboardPort}`,
reuseExistingServer: true,
timeout: 300000, // 5 minutes - React compilation in Docker takes time
ignoreHTTPSErrors: true,
+67 -46
View File
@@ -6,18 +6,17 @@
~ contact@kingsrook.com
~ https://github.com/Kingsrook/
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as
~ published by the Free Software Foundation, either version 3 of the
~ License, or (at your option) any later version.
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
@@ -36,8 +35,8 @@
<licenses>
<license>
<name>GNU Affero General Public License v3.0</name>
<url>https://www.gnu.org/licenses/agpl-3.0.html</url>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
<distribution>repo</distribution>
</license>
</licenses>
@@ -58,8 +57,8 @@
</developers>
<properties>
<revision>0.40.0-SNAPSHOT</revision>
<qqq.version>0.40.0-quick-views-16791785-20260202.235216-1</qqq.version>
<revision>0.41.0-SNAPSHOT</revision>
<qqq.version>4.0.0</qqq.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>21</maven.compiler.source>
@@ -70,6 +69,13 @@
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.21.7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
@@ -146,13 +152,13 @@
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.23.1</version>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>6.7.0</version>
<version>7.2.3</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -164,7 +170,7 @@
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.17.1</version>
<version>2.25.3</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -249,7 +255,51 @@
</profiles>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
</resource>
</resources>
<testResources>
<testResource>
<directory>src/test/resources</directory>
</testResource>
<testResource>
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
</testResource>
</testResources>
<plugins>
<!-- Source and Javadoc JARs also include Maven's shared archive resources. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.4.0</version>
<executions>
<execution>
<id>package-root-notices</id>
<phase>generate-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/maven-shared-archive-resources/META-INF</outputDirectory>
<resources>
<resource>
<directory>${project.basedir}</directory>
<filtering>false</filtering>
<includes>
<include>LICENSE</include>
<include>NOTICE</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
@@ -298,35 +348,6 @@
</executions>
</plugin>
<!-- GPG signing for Maven Central -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Sonatype Central publishing (replaces deploy to distributionManagement) -->
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.8.0</version>
<extensions>true</extensions>
<configuration>
<!-- Server credentials with this ID must be provided in settings.xml -->
<publishingServerId>central</publishingServerId>
<autoPublish>true</autoPublish>
</configuration>
</plugin>
</plugins>
</build>
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Check all four published JARs against this checkout's root license files."""
import argparse
from pathlib import Path
import sys
from zipfile import BadZipFile, ZipFile
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("archive_prefix", type=Path,
help="archive path without classifier or .jar suffix")
args = parser.parse_args()
root = Path(__file__).resolve().parents[1]
expected = {name: (root / name).read_bytes() for name in ("LICENSE", "NOTICE")}
failures = []
for classifier in ("", "-sources", "-tests", "-javadoc"):
archive = Path(f"{args.archive_prefix}{classifier}.jar")
try:
with ZipFile(archive) as jar:
for name, content in expected.items():
entry = f"META-INF/{name}"
if jar.namelist().count(entry) != 1:
failures.append(f"{archive}: expected exactly one {entry}")
elif jar.read(entry) != content:
failures.append(f"{archive}: {entry} differs from root {name}")
except (OSError, BadZipFile) as error:
failures.append(f"{archive}: {error}")
if failures:
print("\n".join(failures), file=sys.stderr)
return 1
print("PASS: main, sources, tests and javadoc JARs contain exact root LICENSE/NOTICE")
return 0
if __name__ == "__main__":
sys.exit(main())
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
############################################################################
## ci-prebuild.sh
## CI prebuild for the shared ci-workflows maven.yaml (runs before Maven):
## Jest unit tests, the React build that is packaged into the jar, and the
## HTTPS dev server on :3001 that the Selenium ITs drive (as the old
## qqq-orb frontend-material pipeline did). Maven then runs the ITs headless.
############################################################################
set -euo pipefail
npm ci --legacy-peer-deps
npm test -- --watchAll=false
npm run build-and-publish
HTTPS=true PORT=3001 REACT_APP_PROXY_LOCALHOST_PORT=8001 \
nohup setsid npm run start > /tmp/react-dev-server.log 2>&1 < /dev/null &
for _ in $(seq 1 150); do
if curl -ksf -o /dev/null https://localhost:3001/; then
echo "React dev server is up on https://localhost:3001"
exit 0
fi
sleep 2
done
echo "React dev server did not come up on :3001"
cat /tmp/react-dev-server.log
exit 1
+75 -22
View File
@@ -5,25 +5,24 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import CssBaseline from "@mui/material/CssBaseline";
import Icon from "@mui/material/Icon";
import {ThemeProvider} from "@mui/material/styles";
import {Theme, ThemeProvider} from "@mui/material/styles";
import {LicenseInfo} from "@mui/x-license-pro";
import {QException} from "@qrunio/qqq-frontend-core/lib/exceptions/QException";
import {QAppNodeType} from "@qrunio/qqq-frontend-core/lib/model/metaData/QAppNodeType";
@@ -33,16 +32,16 @@ import {QBrandingMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QB
import {QInstance} from "@qrunio/qqq-frontend-core/lib/model/metaData/QInstance";
import {QProcessMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QTableMetaData";
// eslint-disable-next-line import/no-unresolved
import CommandMenu from "CommandMenu";
import QContext from "QContext";
import useAnonymousAuthenticationModule from "qqq/authorization/anonymous/useAnonymousAuthenticationModule";
import useAuth0AuthenticationModule from "qqq/authorization/auth0/useAuth0AuthenticationModule";
import useOAuth2AuthenticationModule from "qqq/authorization/oauth2/useOAuth2AuthenticationModule";
import BrandedHeaderBar from "qqq/components/horseshoe/BrandedHeaderBar";
import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
import theme from "qqq/components/legacy/Theme";
import {getBanner, getBannerClassName, getBannerStyles, makeBannerContent} from "qqq/components/misc/Banners";
import {setMiniSidenav, useMaterialUIController} from "qqq/context";
import {MaterialDashboardThemeMetaData} from "qqq/models/metadata/MaterialDashboardThemeMetaData";
import AppHome from "qqq/pages/apps/Home";
import NoApps from "qqq/pages/apps/NoApps";
import ProcessRun from "qqq/pages/processes/ProcessRun";
@@ -54,11 +53,13 @@ import RecordQuery from "qqq/pages/records/query/RecordQuery";
import RecordDeveloperView from "qqq/pages/records/view/RecordDeveloperView";
import RecordView from "qqq/pages/records/view/RecordView";
import RecordViewByUniqueKey from "qqq/pages/records/view/RecordViewByUniqueKey";
import GoogleAnalyticsUtils, {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
import {createDynamicTheme} from "qqq/utils/createDynamicTheme";
import AnalyticsUtils, {AnalyticsModel} from "qqq/utils/analytics/AnalyticsUtils";
import {injectIslandVariables} from "qqq/utils/injectIslandVariables";
import {detectBasePath, resolveAssetUrl} from "qqq/utils/PathUtils";
import Client from "qqq/utils/qqq/Client";
import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react";
import React, {JSXElementConstructor, Key, ReactElement, ReactNode, useEffect, useMemo, useState,} from "react";
import {useCookies} from "react-cookie";
import {Navigate, Route, Routes, useLocation, useParams, useSearchParams,} from "react-router-dom";
import {Md5} from "ts-md5/dist/md5";
@@ -66,6 +67,7 @@ import {Md5} from "ts-md5/dist/md5";
const qController = Client.getInstance();
export const SESSION_UUID_COOKIE_NAME = "sessionUUID";
const MATERIAL_DASHBOARD_THEME_META_DATA_NAME = "com.kingsrook.qqq.frontend.materialdashboard.model.metadata.MaterialDashboardThemeMetaData";
interface Props
{
@@ -79,6 +81,7 @@ export default function App({authenticationMetaData}: Props)
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
const [profileRoutes, setProfileRoutes] = useState({});
const [branding, setBranding] = useState({} as QBrandingMetaData);
const [themeMetaData, setThemeMetaData] = useState(null as MaterialDashboardThemeMetaData | null);
const [metaData, setMetaData] = useState({} as QInstance);
const [needLicenseKey, setNeedLicenseKey] = useState(true);
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
@@ -88,6 +91,7 @@ export default function App({authenticationMetaData}: Props)
const {setupSession: auth0SetupSession, logout: auth0Logout} = useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
const {setupSession: oauth2SetupSession, logout: oauth2Logout} = useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth, inOAuthContext: authenticationMetaData.type === "OAUTH2"});
const {setupSession: anonymousSetupSession, logout: anonymousLogout} = useAnonymousAuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
const [analyticsUtils] = useState(new AnalyticsUtils());
/////////////////////////////////////////////////////////
// tell the client how to do a logout if it sees a 401 //
@@ -143,6 +147,8 @@ export default function App({authenticationMetaData}: Props)
***************************************************************************/
function doLogout()
{
analyticsUtils.reset();
if (authenticationMetaData?.type === "AUTH_0")
{
auth0Logout();
@@ -200,7 +206,7 @@ export default function App({authenticationMetaData}: Props)
}
const childList: any[] = [];
if (app.children)
if (app.children && !app.hideChildrenFromNavigation)
{
app.children.forEach((child: QAppTreeNode) =>
{
@@ -245,6 +251,7 @@ export default function App({authenticationMetaData}: Props)
dropdown: true,
icon: <Icon fontSize="medium">{app.iconName}</Icon>,
collapse: childList,
["data-qqq-sidenav-item-type"]: "top-level-parent-app"
});
}
}
@@ -489,17 +496,35 @@ export default function App({authenticationMetaData}: Props)
{
setAccentColor(metaData.branding.accentColor);
}
if (metaData.branding.accentColorLight)
{
setAccentColorLight(metaData.branding.accentColorLight);
}
}
////////////////////////////////////////////
// Set theme metadata for dynamic theming //
////////////////////////////////////////////
const themeMetaDataObject = metaData.supplementalInstanceMetaData?.get(MATERIAL_DASHBOARD_THEME_META_DATA_NAME);
if (themeMetaDataObject)
{
setThemeMetaData(new MaterialDashboardThemeMetaData(themeMetaDataObject));
}
const gravatarBase = "https://www.gravatar.com/avatar/";
const hash = Md5.hashStr(loggedInUser?.email || "user");
const profilePicture = `${gravatarBase}${hash}`;
let profilePicture = `${gravatarBase}${hash}`;
if(metaData?.branding?.gravatarDefault)
{
profilePicture += `?d=${metaData?.branding?.gravatarDefault}`;
}
const profileRoutes = {
type: "collapse",
name: loggedInUser?.name ?? "Anonymous",
key: "username",
noCollapse: true,
icon: <Avatar src={profilePicture} alt="{loggedInUser?.name}" />,
["data-qqq-sidenav-item-type"]: "user-profile"
};
setProfileRoutes(profileRoutes);
@@ -649,7 +674,26 @@ export default function App({authenticationMetaData}: Props)
},
);
/////////////////////////////////////////////////////////////////////////////
// Create dynamic MUI theme from themeMetaData //
// Theme is rebuilt when themeMetaData changes (from backend) //
/////////////////////////////////////////////////////////////////////////////
const dynamicTheme: Theme = useMemo(() =>
{
return createDynamicTheme(themeMetaData || undefined);
}, [themeMetaData]);
/////////////////////////////////////////////////////////////////////////////
// Inject CSS variables for island components (sidebar, header, DataGrid) //
// These components can't use MUI palette, so they need CSS variables //
/////////////////////////////////////////////////////////////////////////////
useEffect(() =>
{
injectIslandVariables(themeMetaData || undefined);
}, [themeMetaData]);
const [pageHeader, setPageHeader] = useState("" as string | JSX.Element);
const [pageHeaderRightContent, setPageHeaderRightContent] = useState(null as ReactNode);
const [accentColor, setAccentColor] = useState("#0062FF");
const [accentColorLight, setAccentColorLight] = useState("#C0D6F7");
const [tableMetaData, setTableMetaData] = useState(null);
@@ -666,14 +710,20 @@ export default function App({authenticationMetaData}: Props)
}, [loggedInUser]);
const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils());
useEffect(() =>
{
if (isFullyAuthenticated)
{
analyticsUtils.initialize();
}
}, [isFullyAuthenticated, analyticsUtils]);
/*******************************************************************************
**
*******************************************************************************/
function recordAnalytics(model: AnalyticsModel)
{
googleAnalyticsUtils.recordAnalytics(model);
analyticsUtils.recordAnalytics(model);
}
///////////////////////////////////////////////////////////////////
@@ -709,7 +759,7 @@ export default function App({authenticationMetaData}: Props)
***************************************************************************/
function pushModalOnStack(modalIdentifier: string): void
{
if(modalStack.length > 0 && modalStack[modalStack.length] == modalIdentifier)
if (modalStack.length > 0 && modalStack[modalStack.length] == modalIdentifier)
{
console.warn(`Pushing a new modal on the QContext modalStack that is a duplicate of the current top-of-stack [${modalIdentifier}]`)
}
@@ -723,7 +773,7 @@ export default function App({authenticationMetaData}: Props)
***************************************************************************/
function popModalOffStack(modalIdentifier: string): void
{
if(modalStack.length > 0 && modalStack[modalStack.length - 1] != modalIdentifier)
if (modalStack.length > 0 && modalStack[modalStack.length - 1] != modalIdentifier)
{
console.warn(`Request to pop a modal [${modalIdentifier}] off the QContext modalStack that is not currently on top [${modalStack[modalStack.length - 1]}]`)
return;
@@ -760,6 +810,7 @@ export default function App({authenticationMetaData}: Props)
appRoutes && (
<QContext.Provider value={{
pageHeader: pageHeader,
pageHeaderRightContent: pageHeaderRightContent,
accentColor: accentColor,
accentColorLight: accentColorLight,
tableMetaData: tableMetaData,
@@ -770,6 +821,7 @@ export default function App({authenticationMetaData}: Props)
helpHelpActive: helpHelpActive,
userId: userId,
setPageHeader: (header: string | JSX.Element) => setPageHeader(header),
setPageHeaderRightContent: (content: ReactNode) => setPageHeaderRightContent(content),
setAccentColor: (accentColor: string) => setAccentColor(accentColor),
setAccentColorLight: (accentColorLight: string) => setAccentColorLight(accentColorLight),
setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData),
@@ -783,10 +835,11 @@ export default function App({authenticationMetaData}: Props)
pathToLabelMap: pathToLabelMap,
branding: branding
}}>
<ThemeProvider theme={theme}>
<ThemeProvider theme={dynamicTheme}>
<CssBaseline />
<CommandMenu metaData={metaData} />
{banner()}
<BrandedHeaderBar theme={themeMetaData} />
<Sidenav
color={sidenavColor}
icon={resolveAssetUrl(branding.icon)}
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Capability} from "@qrunio/qqq-frontend-core/lib/model/metaData/Capability";
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {useAuth0} from "@auth0/auth0-react";
+14 -12
View File
@@ -5,30 +5,31 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {QBrandingMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
import {QProcessMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
import {createContext} from "react";
import {AnalyticsModel} from "qqq/utils/analytics/AnalyticsUtils";
import {createContext, ReactNode} from "react";
interface QContext
{
pageHeader: string | JSX.Element;
setPageHeader?: (header: string | JSX.Element) => void;
pageHeaderRightContent?: ReactNode;
setPageHeaderRightContent?: (content: ReactNode) => void;
accentColor: string;
setAccentColor?: (color: string) => void;
@@ -69,6 +70,7 @@ interface QContext
const defaultState = {
pageHeader: "",
pageHeaderRightContent: null as ReactNode,
accentColor: "#0062FF",
accentColorLight: "#C0D6F7",
dotMenuOpen: false,
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {QAuthenticationMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster;
@@ -31,8 +30,8 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
import com.kingsrook.qqq.backend.javalin.QJavalinMetaData;
import com.kingsrook.qqq.frontend.materialdashboard.model.metadata.MaterialDashboardFieldMetaData;
import com.kingsrook.qqq.middleware.javalin.QJavalinMetaData;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
@@ -29,7 +28,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaDa
/*******************************************************************************
** table-level meta-data for this module (handled as QSupplementalTableMetaData)
** instance-level meta-data for this module (handled as QSupplementalTableMetaData)
*******************************************************************************/
public class MaterialDashboardInstanceMetaData implements QSupplementalInstanceMetaData
{
@@ -39,6 +38,10 @@ public class MaterialDashboardInstanceMetaData implements QSupplementalInstanceM
private Integer queryScreenCopyFullQueryColumnValuesLimit = 100_000;
private MaterialDashboardTableMetaData.RecordViewActionsPlacement recordViewActionsPlacement = MaterialDashboardTableMetaData.RecordViewActionsPlacement.IN_IDENTITY_SECTION;
private WeekdayCriteriaSettings weekdayCriteriaSettings = new WeekdayCriteriaSettings();
/*******************************************************************************
@@ -155,4 +158,71 @@ public class MaterialDashboardInstanceMetaData implements QSupplementalInstanceM
}
/*******************************************************************************
** Getter for recordViewActionsPlacement
*******************************************************************************/
public MaterialDashboardTableMetaData.RecordViewActionsPlacement getRecordViewActionsPlacement()
{
return (this.recordViewActionsPlacement);
}
/*******************************************************************************
** Setter for recordViewActionsPlacement
*******************************************************************************/
public void setRecordViewActionsPlacement(MaterialDashboardTableMetaData.RecordViewActionsPlacement recordViewActionsPlacement)
{
this.recordViewActionsPlacement = recordViewActionsPlacement;
}
/*******************************************************************************
** Fluent setter for recordViewActionsPlacement
*******************************************************************************/
public MaterialDashboardInstanceMetaData withRecordViewActionsPlacement(MaterialDashboardTableMetaData.RecordViewActionsPlacement recordViewActionsPlacement)
{
this.recordViewActionsPlacement = recordViewActionsPlacement;
return (this);
}
/*******************************************************************************
* Getter for weekdayCriteriaSettings
* @see #withWeekdayCriteriaSettings(WeekdayCriteriaSettings)
*******************************************************************************/
public WeekdayCriteriaSettings getWeekdayCriteriaSettings()
{
return (this.weekdayCriteriaSettings);
}
/*******************************************************************************
* Setter for weekdayCriteriaSettings
* @see #withWeekdayCriteriaSettings(WeekdayCriteriaSettings)
*******************************************************************************/
public void setWeekdayCriteriaSettings(WeekdayCriteriaSettings weekdayCriteriaSettings)
{
this.weekdayCriteriaSettings = weekdayCriteriaSettings;
}
/*******************************************************************************
* Fluent setter for weekdayCriteriaSettings
*
* @param weekdayCriteriaSettings
* @return this
*******************************************************************************/
public MaterialDashboardInstanceMetaData withWeekdayCriteriaSettings(WeekdayCriteriaSettings weekdayCriteriaSettings)
{
this.weekdayCriteriaSettings = weekdayCriteriaSettings;
return (this);
}
}
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
@@ -45,6 +44,12 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
*******************************************************************************/
public class MaterialDashboardTableMetaData extends QSupplementalTableMetaData
{
public enum RecordViewActionsPlacement
{
IN_IDENTITY_SECTION,
INLINE_WITH_PAGE_TITLE
}
private static final QLogger LOG = QLogger.getLogger(MaterialDashboardTableMetaData.class);
public static final String TYPE = "materialDashboard";
@@ -52,6 +57,8 @@ public class MaterialDashboardTableMetaData extends QSupplementalTableMetaData
private List<List<String>> gotoFieldNames;
private List<String> defaultQuickFilterFieldNames;
private List<FieldRule> fieldRules;
private Boolean showRecordSidebar = true;
private RecordViewActionsPlacement recordViewActionsPlacement = RecordViewActionsPlacement.IN_IDENTITY_SECTION;
private QCodeReference onLoadFormAdjuster = null;
@@ -262,6 +269,68 @@ public class MaterialDashboardTableMetaData extends QSupplementalTableMetaData
/*******************************************************************************
** Getter for showRecordSidebar
*******************************************************************************/
public Boolean getShowRecordSidebar()
{
return (this.showRecordSidebar);
}
/*******************************************************************************
** Setter for showRecordSidebar
*******************************************************************************/
public void setShowRecordSidebar(Boolean showRecordSidebar)
{
this.showRecordSidebar = showRecordSidebar;
}
/*******************************************************************************
** Fluent setter for showRecordSidebar
*******************************************************************************/
public MaterialDashboardTableMetaData withShowRecordSidebar(Boolean showRecordSidebar)
{
this.showRecordSidebar = showRecordSidebar;
return (this);
}
/*******************************************************************************
** Getter for recordViewActionsPlacement
*******************************************************************************/
public RecordViewActionsPlacement getRecordViewActionsPlacement()
{
return (this.recordViewActionsPlacement);
}
/*******************************************************************************
** Setter for recordViewActionsPlacement
*******************************************************************************/
public void setRecordViewActionsPlacement(RecordViewActionsPlacement recordViewActionsPlacement)
{
this.recordViewActionsPlacement = recordViewActionsPlacement;
}
/*******************************************************************************
** Fluent setter for recordViewActionsPlacement
*******************************************************************************/
public MaterialDashboardTableMetaData withRecordViewActionsPlacement(RecordViewActionsPlacement recordViewActionsPlacement)
{
this.recordViewActionsPlacement = recordViewActionsPlacement;
return (this);
}
/*******************************************************************************
** Getter for fieldRules
*******************************************************************************/
@@ -384,6 +453,9 @@ public class MaterialDashboardTableMetaData extends QSupplementalTableMetaData
clone.onLoadFormAdjuster = onLoadFormAdjuster.clone();
}
clone.showRecordSidebar = showRecordSidebar;
clone.recordViewActionsPlacement = recordViewActionsPlacement;
return abstractClone;
}
@@ -0,0 +1,156 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2026. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/*******************************************************************************
* Material dashboard instance metadata settings - to control weekday criteria
* on date & date-time fields. e.g., day-is-any-of or day-is-non-eof.
*
* <p>These criteria are on by default - but this object can turn them off with
* the {@code enabled} setting.</p>
*
* <p>Also - since how time zones should apply can be a hotly contested issue,
* values for the parameters supported by QQQ's {@code WeekdayOfDateTimeFunction}
* can be set as {@code dateTimeFieldFunctionArguments}</p>
*
* <p>e.g., to always use US/Central time:
* <pre>
new WeekdayCriteriaSettings().withDateTimeFieldFunctionArguments(
Map.of(WeekdayOfDateTimeFunction.PARAM_TIME_ZONE_ID, "US/Central")));</pre></p>
*
* <p>or to use user/session time zone:
* <pre>
new WeekdayCriteriaSettings().withDateTimeFieldFunctionArguments(
Map.of(WeekdayOfDateTimeFunction.PARAM_USE_SESSION_ZONE_ID, true)));</pre></p>
*******************************************************************************/
public class WeekdayCriteriaSettings implements Serializable, Cloneable
{
private Boolean enabled = true;
private Map<String, Serializable> dateTimeFieldFunctionArguments = new HashMap<>();
/*******************************************************************************
* Getter for enabled
* @see #withEnabled(Boolean)
*******************************************************************************/
public Boolean getEnabled()
{
return (this.enabled);
}
/*******************************************************************************
* Setter for enabled
* @see #withEnabled(Boolean)
*******************************************************************************/
public void setEnabled(Boolean enabled)
{
this.enabled = enabled;
}
/*******************************************************************************
* Fluent setter for enabled
*
* @param enabled
* TODO document this property
*
* @return this
*******************************************************************************/
public WeekdayCriteriaSettings withEnabled(Boolean enabled)
{
this.enabled = enabled;
return (this);
}
/*******************************************************************************
* Getter for dateTimeFieldFunctionArguments
* @see #withDateTimeFieldFunctionArguments(Map)
*******************************************************************************/
public Map<String, Serializable> getDateTimeFieldFunctionArguments()
{
return (this.dateTimeFieldFunctionArguments);
}
/*******************************************************************************
* Setter for dateTimeFieldFunctionArguments
* @see #withDateTimeFieldFunctionArguments(Map)
*******************************************************************************/
public void setDateTimeFieldFunctionArguments(Map<String, Serializable> dateTimeFieldFunctionArguments)
{
this.dateTimeFieldFunctionArguments = dateTimeFieldFunctionArguments;
}
/*******************************************************************************
* Fluent setter for dateTimeFieldFunctionArguments
*
* @param dateTimeFieldFunctionArguments
* TODO document this property
*
* @return this
*******************************************************************************/
public WeekdayCriteriaSettings withDateTimeFieldFunctionArguments(Map<String, Serializable> dateTimeFieldFunctionArguments)
{
this.dateTimeFieldFunctionArguments = dateTimeFieldFunctionArguments;
return (this);
}
/***************************************************************************
*
***************************************************************************/
@Override
public WeekdayCriteriaSettings clone()
{
try
{
WeekdayCriteriaSettings clone = (WeekdayCriteriaSettings) super.clone();
if(dateTimeFieldFunctionArguments != null)
{
clone.dateTimeFieldFunctionArguments = new HashMap<>(dateTimeFieldFunctionArguments);
}
return clone;
}
catch(CloneNotSupportedException e)
{
throw new AssertionError();
}
}
}
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata.fieldrules;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata.fieldrules;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata.fieldrules;
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qqq.frontend.materialdashboard.savedreports;
@@ -0,0 +1,98 @@
/***************************************************************************
* The original version of this app had the "primary" color set to a bright
* red/fuchsia/pink tone (#E91E63). So, whenever we would try to add something
* that might have made sense to use "primary" color, we'd see that tone that
* we didn't like, and we'd use "info" instead - a nice blue.
*
* But now, with the backend-provided themes, apps might set their primary
* color to something like, let's say teal, and the expectation is to make a
* lot of things like the new & save buttons be that primary color.
*
* But, we don't want to make "info" that teal tone - it should be blue
* (e.g,. for an info alert or icon - blue for info is the expectation).
*
* So the mistake we made all along I think was using "info" instead of
* "primary" in lots of UI elements that we added.
*
* To preserve backward compatibility as much as possible, while also changing
* some elements from "info" to "primary", then here's what I've come up with
* as an approach:
*
* 1. Define a CSS variable on `:root`called `--qqq-prefer-info-color-to-primary-color`.
* (which flows from MaterialDashboardThemeMetaData.preferInfoColorToPrimaryColor)
* - If this variable is 1, then places that /should/ have used "primary"
* all along would instead still use "info". We'd set that to 1 to
* maintain backward-compatibility (e.g., so the create-new button would
* still be info-blue, not primary-fuchsia).
* - But then, an app can override that value to `0`, to say "no no, don't
* prefer info - instead, use primary where it would have made sense
* (but you just didn't want that fuchsia color)". So an app could set
* MaterialDashboardThemeMetaData.preferInfoColorToPrimaryColor to false,
* or set `--qqq-prefer-info-color-to-primary-color: 0` in their custom CSS
* so its create new button would be primary (e.g., teal as mentioned above),
* not info-blue.
*
* 2. Define a family of functions (e.g., this file) that, based on that css
* variable, either output `"info"` or `"primary"`; or `var(--qqq-info-color)`
* or `var(--qqq-primary-color)`, or the hard-coded fallbacks for info & primary
*
* 3. Update components that are using `"info"` (or `--qqq-info-color`
* (or `var(--qqq-info-color, "#0062FF")`)) to call the appropriate
* `preferredInfoOrPrimaryXXX` function. I don't think this is a
* "change them all" scenario (because info should still be used where it's
* intention is truly 'info'), but, i'm not sure entirely which ones
* it should be... my current list looks like:
* A. Create-New and Save buttons in DefaultButtons.tsx
* B. icon backgrounds on app home screens (Home.tsx)
* C. progress bar background on processes (ProcessRun.tsx usage of Stepper)
* D. the little pagination buttons on the bottoms of like the TableData widget
***************************************************************************/
/***************************************************************************
* private function used by other functions in here to decide if the info
* color is preferred over the primary color, based on the css variable
* --qqq-prefer-info-color-to-primary-color being equal to 1 or not.
*
* 1 = prefer info color; anything else = prefer primary color
*
* Legacy behavior was to always prefer info color; Apps setting their own
* primary color may want to use their primary color for more UI elements,
* and set that css var to 0.
***************************************************************************/
function preferInfoColorToPrimaryColor(): boolean {
return getComputedStyle(document.body).getPropertyValue('--qqq-prefer-info-color-to-primary-color').trim() === "1";
}
/***************************************************************************
* private function that, based on the value of preferInfoColorToPrimaryColor(),
* returns the hex code for either the default info color (a blue), or the
* default primary color (a red/fuchsia).
***************************************************************************/
function preferredDefaultColorInfoOrPrimary(): string {
return preferInfoColorToPrimaryColor() ? "#0062FF" : "#E91E63";
}
/***************************************************************************
* public function that returns either "info" or "primary" based on the
* value of preferInfoColorToPrimaryColor().
*
* So elements that should probably be displayed in the primary color (e.g.,
* primary action buttons on screens, or some UI decorations), they can use
* this function to get their color name - but - if the primary is undesirable,
* then the info color (default blue) can be used instead.
***************************************************************************/
export function preferredColorNameInfoOrPrimary(): "info" | "primary" {
return preferInfoColorToPrimaryColor() ? "info" : "primary";
}
/***************************************************************************
* public function that returns a css var(--qqq...color, #default) expression,
* based on the value of preferInfoColorToPrimaryColor().
*
* For use-cases similar to preferredColorNameInfoOrPrimary(), but where we
* need a CSS value, not a color name for MUI to interpret.
***************************************************************************/
export function preferredInfoOrPrimaryColorVarExpression(): string {
return `var(--qqq-${preferredColorNameInfoOrPrimary()}-color, ${preferredDefaultColorInfoOrPrimary()})`;
}
+40
View File
@@ -0,0 +1,40 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {hasScope} from "./ScopeUtils";
describe("OAuth scopes", () =>
{
const driveScope = "https://www.googleapis.com/auth/drive";
it("accepts the exact scope among granted scopes", () =>
{
expect(hasScope(driveScope, driveScope)).toBe(true);
expect(hasScope(`openid profile ${driveScope} email`, driveScope)).toBe(true);
});
it("rejects missing scopes and substring lookalikes", () =>
{
for (const scope of ["", "openid profile", `${driveScope}.readonly`, `https://example.com/${driveScope}`, `${driveScope}.example.com`])
{
expect(hasScope(scope, driveScope)).toBe(false);
}
});
});
+24
View File
@@ -0,0 +1,24 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function hasScope(grantedScopes: string, requiredScope: string): boolean
{
return grantedScopes.split(/\s+/).includes(requiredScope);
}
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {QAuthenticationMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
@@ -54,6 +53,7 @@ export default function useAnonymousAuthenticationModule({setIsFullyAuthenticate
try
{
const {values} = await qController.manageSession(null, null, null);
localStorage.setItem("sessionValues", JSON.stringify(values ?? {}));
if (values?.user)
{
setLoggedInUser(values.user);
@@ -74,6 +74,7 @@ export default function useAnonymousAuthenticationModule({setIsFullyAuthenticate
const logout = () =>
{
qController.clearAuthenticationMetaDataLocalStorage();
localStorage.removeItem("sessionValues");
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
};
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react";
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react";
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {useAuth0} from "@auth0/auth0-react";
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {withAuthenticationRequired} from "@auth0/auth0-react";
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Auth0Provider, useAuth0} from "@auth0/auth0-react";
import {QAuthenticationMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
@@ -25,6 +24,7 @@ import HandleAuthorizationError from "HandleAuthorizationError";
import jwt_decode from "jwt-decode";
import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute";
import {MaterialUIControllerProvider} from "qqq/context";
import AnalyticsUtils from "qqq/utils/analytics/AnalyticsUtils";
import Client from "qqq/utils/qqq/Client";
import {useCookies} from "react-cookie";
import {useNavigate, useSearchParams} from "react-router-dom";
@@ -106,6 +106,40 @@ export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, s
};
/***************************************************************************
**
***************************************************************************/
const readStoredSessionValues = (): {[key: string]: any} | null =>
{
try
{
const rawSessionValues = localStorage.getItem("sessionValues");
return (rawSessionValues ? JSON.parse(rawSessionValues) : null);
}
catch (e)
{
console.log("Error parsing sessionValues from localStorage: " + e);
return (null);
}
};
/***************************************************************************
**
***************************************************************************/
const hasAnalyticsIdentityValues = (sessionValues: {[key: string]: any} | null): boolean =>
{
const analyticsIdentityValues = AnalyticsUtils.getAnalyticsIdentityValues(sessionValues);
return Boolean(
analyticsIdentityValues["user_email"]
|| sessionValues?.user?.email
|| sessionValues?.user?.idReference
|| sessionValues?.user?.id
|| sessionValues?.user?.userId
);
};
/***************************************************************************
**
***************************************************************************/
@@ -115,26 +149,41 @@ export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, s
{
console.log("Loading token from auth0...");
const accessToken = await auth0GetAccessTokenSilently();
let loggedInUserForApp = auth0User;
const lsAccessToken = localStorage.getItem("accessToken");
if (shouldStoreNewToken(accessToken, lsAccessToken))
{
console.log("Sending accessToken to backend, requesting a sessionUUID...");
const {uuid: values} = await qController.manageSession(accessToken, null);
const {values} = await qController.manageSession(accessToken, null);
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("sessionValues", JSON.stringify(values));
localStorage.setItem("sessionValues", JSON.stringify(values ?? {}));
loggedInUserForApp = values?.user ?? auth0User;
console.log("Got new sessionUUID from backend, and stored new accessToken");
}
else
{
const storedSessionValues = readStoredSessionValues();
if (!hasAnalyticsIdentityValues(storedSessionValues))
{
console.log("Refreshing session values from existing sessionUUID...");
const {values} = await qController.manageSession(null, cookies[SESSION_UUID_COOKIE_NAME]);
localStorage.setItem("sessionValues", JSON.stringify(values ?? {}));
loggedInUserForApp = values?.user ?? auth0User;
}
else if (storedSessionValues?.user)
{
loggedInUserForApp = storedSessionValues.user;
}
console.log("Using existing sessionUUID cookie");
}
setIsFullyAuthenticated(true);
Client.setGotAuthenticationInAllControllers();
setLoggedInUser(auth0User);
setLoggedInUser(loggedInUserForApp);
console.log("Token load complete.");
}
catch (e)
@@ -142,6 +191,7 @@ export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, s
console.log(`Error loading token: ${JSON.stringify(e)}`);
qController.clearAuthenticationMetaDataLocalStorage();
localStorage.removeItem("accessToken");
localStorage.removeItem("sessionValues");
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
useAuth0Logout();
return;
@@ -155,6 +205,7 @@ export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, s
const logout = () =>
{
localStorage.removeItem("accessToken");
localStorage.removeItem("sessionValues");
useAuth0Logout({returnTo: window.location.origin});
};
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {QAuthenticationMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
@@ -186,6 +185,7 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
// ** Exchange authorization code for session UUID via backend API * //
///////////////////////////////////////////////////////////////////////
const {uuid: newSessionUuid, values} = await qController.manageSession(null, null, manageSessionRequestBody);
localStorage.setItem("sessionValues", JSON.stringify(values ?? {}));
/////////////////////////////////////////////////////////
// ** Update parent component's authentication state * //
@@ -265,6 +265,7 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
// ** Validate the session UUID with backend and retrieve user data * //
////////////////////////////////////////////////////////////////////////
const {values} = await qController.manageSession(null, sessionUuid, null);
localStorage.setItem("sessionValues", JSON.stringify(values ?? {}));
/////////////////////////////////////////////////////////
// ** Update parent component's authentication state * //
@@ -371,6 +372,7 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
// ** Clear authentication metadata stored in local storage by the QQQ client * //
//////////////////////////////////////////////////////////////////////////////////
qController.clearAuthenticationMetaDataLocalStorage();
localStorage.removeItem("sessionValues");
/////////////////////////////////////////////////////////////////////////////////////////////////////
// ** Remove session cookies from the browser * //
@@ -429,7 +431,7 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
////////////////////////////////////////////////////////////////////////////////////
// ** Extract the OAuth2 identity provider's base URL (authority) from metadata * //
////////////////////////////////////////////////////////////////////////////////////
const authority: string = authenticationMetaData.data.baseUrl;
const authority: string = authenticationMetaData.data.externalBaseUrl || authenticationMetaData.data.baseUrl;
//////////////////////////////////////////////////////////////////////////
// ** Extract the OAuth2 client ID for this application from metadata * //
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {QException} from "@qrunio/qqq-frontend-core/lib/exceptions/QException";
+30 -22
View File
@@ -5,22 +5,22 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon";
import {preferredColorNameInfoOrPrimary} from "qqq/assets/theme/functions/preferInfoColorToPrimaryColor";
import {QIcon} from "@qrunio/qqq-frontend-core/lib/model/metaData/QIcon";
import React from "react";
import {Link} from "react-router-dom";
@@ -43,7 +43,7 @@ export function QCreateNewButton({tablePath}: QCreateNewButtonProps): JSX.Elemen
return (
<Box display="inline-block" ml={standardML} mr={0} width={standardWidth}>
<Link to={`${tablePath}/create`}>
<MDButton variant="gradient" color="info" fullWidth startIcon={<Icon>add</Icon>}>
<MDButton qqqId="create-new" variant="gradient" color={preferredColorNameInfoOrPrimary()} fullWidth startIcon={<Icon>add</Icon>}>
Create New
</MDButton>
</Link>
@@ -68,7 +68,7 @@ export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveBu
{
return (
<Box ml={standardML} width={standardWidth}>
<MDButton type="submit" variant="gradient" color="info" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
<MDButton qqqId="save" type="submit" variant="gradient" color={preferredColorNameInfoOrPrimary()} size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
{label}
</MDButton>
</Box>
@@ -89,7 +89,7 @@ export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): J
{
return (
<Box ml={standardML} width={standardWidth}>
<MDButton variant="gradient" color="primary" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>delete</Icon>} disabled={disabled}>
<MDButton qqqId="delete" variant="contained" color="error" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>delete</Icon>} disabled={disabled}>
Delete
</MDButton>
</Box>
@@ -122,7 +122,8 @@ export function QActionsMenuButton({isOpen, onClickHandler, label, qIcon}: QActi
return (
<Box width={standardWidth} ml={1}>
<MDButton
variant={isOpen ? "contained" : "outlined"}
qqqId="actions-menu"
variant={"outlined"}
color="dark"
onClick={onClickHandler}
startIcon={<Icon>{qIcon?.name ?? "games"}</Icon>}
@@ -141,15 +142,16 @@ interface QCancelButtonProps
disabled: boolean;
label?: string;
iconName?: string;
qqqId?: string;
}
export function QCancelButton({
onClickHandler, disabled, label, iconName,
onClickHandler, disabled, label, iconName, qqqId
}: QCancelButtonProps): JSX.Element
{
return (
<Box ml={standardML} width={standardWidth}>
<MDButton type="button" variant="outlined" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} onClick={onClickHandler} disabled={disabled}>
<MDButton type="button" variant="outlined" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} onClick={onClickHandler} disabled={disabled} qqqId={qqqId}>
{label}
</MDButton>
</Box>
@@ -159,6 +161,7 @@ export function QCancelButton({
QCancelButton.defaultProps = {
label: "cancel",
iconName: "cancel",
qqqId: "cancel"
};
interface QSubmitButtonProps
@@ -166,13 +169,14 @@ interface QSubmitButtonProps
label?: string;
iconName?: string;
disabled: boolean;
qqqId?: string;
}
export function QSubmitButton({label, iconName, disabled}: QSubmitButtonProps): JSX.Element
export function QSubmitButton({label, iconName, disabled, qqqId}: QSubmitButtonProps): JSX.Element
{
return (
<Box ml={standardML} width={standardWidth}>
<MDButton type="submit" variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
<MDButton type="submit" variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled} qqqId={qqqId}>
{label}
</MDButton>
</Box>
@@ -182,6 +186,7 @@ export function QSubmitButton({label, iconName, disabled}: QSubmitButtonProps):
QSubmitButton.defaultProps = {
label: "Submit",
iconName: "check",
qqqId: "submit"
};
interface QAlternateButtonProps
@@ -189,18 +194,21 @@ interface QAlternateButtonProps
label: string,
iconName?: string,
disabled: boolean,
onClick?: () => void
onClick?: () => void,
qqqId?: string;
}
export function QAlternateButton({label, iconName, disabled, onClick}: QAlternateButtonProps): JSX.Element
export function QAlternateButton({label, iconName, disabled, onClick, qqqId}: QAlternateButtonProps): JSX.Element
{
return (
<Box ml={standardML} width={standardWidth}>
<MDButton type="button" variant="gradient" color="secondary" size="small" fullWidth startIcon={iconName && <Icon>{iconName}</Icon>} onClick={onClick} disabled={disabled}>
<MDButton type="button" variant="gradient" color="secondary" size="small" fullWidth startIcon={iconName && <Icon>{iconName}</Icon>} onClick={onClick} disabled={disabled} qqqId={qqqId}>
{label}
</MDButton>
</Box>
);
}
QAlternateButton.defaultProps = {};
QAlternateButton.defaultProps = {
qqqId: "alternate"
};
+12 -11
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ClickAwayListener, Grow, MenuList, Paper, Popper} from "@mui/material";
@@ -24,6 +23,7 @@ import Button from "@mui/material/Button/Button";
import Icon from "@mui/material/Icon";
import MenuItem from "@mui/material/MenuItem";
import React, {useEffect, useRef, useState} from "react";
import {generateButtonId, generateMenuItemId} from "qqq/utils/qqqIdUtils";
interface Props
@@ -95,7 +95,7 @@ function MenuButton({label, iconName, options, disabled, callback}: Props)
const menuItems: JSX.Element[] = []
options.map((option, index) =>
{
menuItems.push(<MenuItem key={index} onClick={e => menuItemClicked(e, index)}>
menuItems.push(<MenuItem key={index} data-qqq-id={generateMenuItemId(undefined, option)} onClick={e => menuItemClicked(e, index)}>
{option}
</MenuItem>);
})
@@ -105,6 +105,7 @@ function MenuButton({label, iconName, options, disabled, callback}: Props)
<Button
ref={anchorRef}
id="composition-button"
data-qqq-id={generateButtonId(undefined, label)}
aria-controls={open ? "composition-menu" : undefined}
aria-expanded={open ? "true" : undefined}
aria-haspopup="true"
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {QJobError} from "@qrunio/qqq-frontend-core/lib/model/processes/QJobError";
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Box, InputLabel} from "@mui/material";
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Chip} from "@mui/material";
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Box from "@mui/material/Box";
+12 -11
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {InputAdornment, InputLabel} from "@mui/material";
@@ -35,6 +34,7 @@ import MDTypography from "qqq/components/legacy/MDTypography";
import React, {useMemo, useState} from "react";
import AceEditor from "react-ace";
import {flushSync} from "react-dom";
import {generateInputId} from "qqq/utils/qqqIdUtils";
// Declaring props types for FormField
interface Props
@@ -281,9 +281,10 @@ function QDynamicFormField({
}
else
{
const dataQqqId = generateInputId(undefined, name, label);
field = (
<>
<Field {...rest} {...onChange} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="outlined" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
<Field {...rest} {...onChange} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="outlined" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled} data-qqq-id={dataQqqId}
onKeyPress={(e: any) =>
{
if (e.key === "Enter")
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Alert, Skeleton} from "@mui/material";
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {AdornmentType} from "@qrunio/qqq-frontend-core/lib/model/metaData/AdornmentType";
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Checkbox, Chip, CircularProgress, FilterOptionsState, Icon} from "@mui/material";
+19 -17
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Alert} from "@mui/material";
@@ -53,6 +52,7 @@ import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc
import RowBuilderWidget from "qqq/components/widgets/misc/RowBuilderWidget";
import {FieldRule, FieldRuleAction, FieldRuleTrigger} from "qqq/models/fields/FieldRules";
import HtmlUtils from "qqq/utils/HtmlUtils";
import {sanitizeId} from "qqq/utils/qqqIdUtils";
import Client from "qqq/utils/qqq/Client";
import TableUtils from "qqq/utils/qqq/TableUtils";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
@@ -463,8 +463,8 @@ function EntityForm(props: Props): JSX.Element
return form
}
return <Card id={section.name} sx={{overflow: "visible", scrollMarginTop: "100px"}} elevation={cardElevation}>
<MDTypography variant="h6" p={3} pb={1}>
return <Card id={section.name} sx={{overflow: "visible", scrollMarginTop: "100px"}} elevation={cardElevation} data-qqq-id={`form-section-${sanitizeId(section.name)}`}>
<MDTypography variant="h6" p={3} pb={1} data-qqq-id={`form-section-header-${sanitizeId(section.name)}`}>
{section.label}
</MDTypography>
{getSectionHelp(section)}
@@ -1587,6 +1587,8 @@ function EntityForm(props: Props): JSX.Element
}
const formId = props.id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`;
const tableNameForId = tableMetaData ? sanitizeId(tableMetaData.name) : "";
const formMode = props.isCopy ? "copy" : (props.id != null ? "edit" : "create");
let body;
@@ -1644,7 +1646,7 @@ function EntityForm(props: Props): JSX.Element
else
{
body = (
<Box mb={3} className="entityForm">
<Box mb={3} className="entityForm" data-qqq-id={`record-${formMode}-${tableNameForId}`}>
{
(alertContent || warningContent) &&
<Grid container spacing={3}>
@@ -1750,9 +1752,9 @@ function EntityForm(props: Props): JSX.Element
<ScrollToFirstError />
<Box pb={3} pt={0}>
<Card id={`${t1sectionName}`} sx={{overflow: "visible", pb: 2, scrollMarginTop: "100px"}} elevation={cardElevation}>
<Card id={`${t1sectionName}`} sx={{overflow: "visible", pb: 2, scrollMarginTop: "100px"}} elevation={cardElevation} data-qqq-id={`record-${formMode}-header-${tableNameForId}`}>
<Box display="flex" p={3} pb={1}>
<Box mr={1.5}>
<Box mr={1.5} data-qqq-id={`record-${formMode}-avatar-${tableNameForId}`}>
<Avatar sx={{bgcolor: accentColor}}>
<Icon>
{tableMetaData?.iconName}
@@ -1760,7 +1762,7 @@ function EntityForm(props: Props): JSX.Element
</Avatar>
</Box>
<Box display="flex" alignItems="center">
<MDTypography variant="h5">{formTitle}</MDTypography>
<MDTypography variant="h5" data-qqq-id={`record-${formMode}-title-${tableNameForId}`}>{formTitle}</MDTypography>
</Box>
</Box>
{t1section && getSectionHelp(t1section)}
@@ -1780,7 +1782,7 @@ function EntityForm(props: Props): JSX.Element
)) : null}
{formFieldsBySection &&
<Box component="div" p={3} className={props.isModal ? "modalBottomButtonBar" : "stickyBottomButtonBar"}>
<Box component="div" p={3} className={props.isModal ? "modalBottomButtonBar" : "stickyBottomButtonBar"} data-qqq-id={`record-${formMode}-button-bar-${tableNameForId}`}>
<Grid container justifyContent="flex-end" spacing={3}>
<QCancelButton onClickHandler={props.isModal ? props.closeModalHandler : handleCancelClicked} disabled={isSubmitting} />
<Tooltip title={saveDisabledTooltip}>
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@@ -0,0 +1,94 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import {MaterialDashboardThemeMetaData} from "qqq/models/metadata/MaterialDashboardThemeMetaData";
import React from "react";
interface Props
{
theme?: MaterialDashboardThemeMetaData | null;
}
/*******************************************************************************
** BrandedHeaderBar - renders a branded header bar when enabled via theme.
** Uses CSS variables injected from MaterialDashboardThemeMetaData for styling.
*******************************************************************************/
export default function BrandedHeaderBar({theme}: Props): JSX.Element | null
{
/////////////////////////////////////////////////////////////////////////////
// Check if branded header is enabled via theme prop //
/////////////////////////////////////////////////////////////////////////////
if (!theme?.brandedHeaderEnabled)
{
return null;
}
return (
<Box
component="header"
data-branded-header="true"
className="qqq-branded-header-bar"
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "0 1rem",
backgroundColor: "var(--qqq-branded-header-background-color, #1a2035)",
color: "var(--qqq-branded-header-text-color, #ffffff)",
height: "var(--qqq-branded-header-height, 48px)",
position: "sticky",
top: 0,
zIndex: 1200,
}}
>
<Box sx={{display: "flex", alignItems: "center", gap: 2}}>
{theme.brandedHeaderLogoPath && (
<Box
component="img"
src={theme.brandedHeaderLogoPath}
alt={theme.brandedHeaderLogoAltText || "Logo"}
sx={{
height: "calc(var(--qqq-branded-header-height, 48px) - 8px)",
maxWidth: "200px",
objectFit: "contain",
}}
onError={(e: React.SyntheticEvent<HTMLImageElement>) =>
{
e.currentTarget.style.display = "none";
}}
/>
)}
{theme.brandedHeaderTagline && (
<Typography
variant="body1"
sx={{
color: "var(--qqq-branded-header-text-color, #ffffff)",
fontWeight: 500,
}}
>
{theme.brandedHeaderTagline}
</Typography>
)}
</Box>
</Box>
);
}
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Breadcrumbs as MuiBreadcrumbs} from "@mui/material";
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Box from "@mui/material/Box";
+12 -12
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Popper, InputAdornment, Box} from "@mui/material";
@@ -65,7 +64,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
const route = useLocation().pathname.split("/").slice(1);
const navigate = useNavigate();
const {pageHeader, setDotMenuOpen} = useContext(QContext);
const {pageHeader, pageHeaderRightContent, setDotMenuOpen} = useContext(QContext);
useEffect(() =>
{
@@ -269,10 +268,11 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
</Toolbar>
{
pageHeader &&
<Box display="flex" justifyContent="space-between">
<Box display="flex" justifyContent="space-between" alignItems="center" flexWrap={{xs: "wrap", md: "nowrap"}} columnGap={2} rowGap={1}>
<MDTypography pb="0.5rem" variant="h3" color={light ? "white" : "dark"}>
{pageHeader}
</MDTypography>
{pageHeaderRightContent && <Box display="flex">{pageHeaderRightContent}</Box>}
</Box>
}
</AppBar>
+9 -10
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Theme} from "@mui/material/styles";
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {QBrandingMetaData} from "@qrunio/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
@@ -34,6 +33,7 @@ import sidenavLogoLabel from "qqq/components/horseshoe/sidenav/styles/SideNav";
import MDTypography from "qqq/components/legacy/MDTypography";
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
import {setMiniSidenav, setTransparentSidenav, setWhiteSidenav, useMaterialUIController,} from "qqq/context";
import {sanitizeId} from "qqq/utils/qqqIdUtils";
import {ReactNode, useEffect, useReducer, useState} from "react";
import {NavLink, useLocation} from "react-router-dom";
@@ -209,7 +209,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}
// Render all the routes from the routes.js (All the visible items on the Sidenav)
const renderRoutes = routes.map(
({type, name, icon, title, collapse, noCollapse, key, href, route}: any) =>
({type, name, icon, title, collapse, noCollapse, key, href, route, ...routeRest}: any) =>
{
let returnValue;
@@ -230,6 +230,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}
icon={icon}
active={key === collapseName}
noCollapse={noCollapse}
{...routeRest}
/>
</Link>
);
@@ -260,6 +261,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}
open={openCollapse === key}
noCollapse={noCollapse}
onClick={() => (!noCollapse ? (openCollapse === key ? setOpenCollapse(false) : setOpenCollapse(key)) : null)}
{...routeRest}
>
{collapse ? renderCollapse(collapse) : null}
</SideNavCollapse>
@@ -331,8 +333,9 @@ function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}
{...rest}
variant="permanent"
ownerState={{transparentSidenav, whiteSidenav, miniSidenav, darkMode}}
data-qqq-id="sidenav-root"
>
<Box pt={3} mr={1} pb={0} px={4} textAlign="center">
<Box pt={3} mr={1} pb={0} px={4} textAlign="center" data-qqq-id="sidenav-logo-area">
<Box
display={{xs: "block", xl: "none"}}
position="absolute"
@@ -370,14 +373,14 @@ function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}
(darkMode && !transparentSidenav && whiteSidenav)
}
/>
<List>{renderRoutes}</List>
<List data-qqq-id="sidenav-menu-list">{renderRoutes}</List>
<Divider
light={
(!darkMode && !whiteSidenav && !transparentSidenav) ||
(darkMode && !transparentSidenav && whiteSidenav)
}
/>
<Button onClick={logout}>Log Out</Button>
<Button onClick={logout} data-qqq-id="sidenav-logout-button">Log Out</Button>
</SidenavRoot>
);
}
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Box from "@mui/material/Box";
@@ -28,6 +27,7 @@ import ListItemText from "@mui/material/ListItemText";
import {ReactNode} from "react";
import {collapseArrow, collapseIcon, collapseIconBox, collapseItem, collapseText,} from "qqq/components/horseshoe/sidenav/styles/SideNavCollapse";
import {useMaterialUIController} from "qqq/context";
import {generateNavItemId} from "qqq/utils/qqqIdUtils";
// Declaring props types for SideNavCollapse
interface Props {
@@ -53,11 +53,14 @@ function SideNavCollapse({
const [controller] = useMaterialUIController();
const {miniSidenav, transparentSidenav, whiteSidenav, darkMode} = controller;
const dataQqqId = generateNavItemId(undefined, name);
return (
<>
<ListItem component="li">
<ListItem component="li" data-qqq-id={dataQqqId}>
<Box
{...rest}
className={active ? "qqq-sidebar-active" : ""}
sx={(theme: any) =>
collapseItem(theme, {active, transparentSidenav, whiteSidenav, darkMode})
}
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Box from "@mui/material/Box";
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import List from "@mui/material/List";
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Drawer from "@mui/material/Drawer";
@@ -96,8 +95,9 @@ export default styled(Drawer)(({theme, ownerState}: { theme?: Theme | any; owner
border: "none",
margin: "0",
borderRadius: "0",
height: "100%",
top: "unset",
// Account for branded header height when present
top: "var(--qqq-branded-header-height, 0px)",
height: "calc(100vh - var(--qqq-branded-header-height, 0px))",
...(miniSidenav ? drawerCloseStyles() : drawerOpenStyles()),
},
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @mui material components
@@ -5,23 +5,22 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Theme} from "@mui/material/styles";
function collapseItem(theme: Theme, ownerState: any)
function collapseItem(theme: Theme, ownerState: any)
{
const {palette, transitions, breakpoints, boxShadows, borders, functions} = theme;
const {active, transparentSidenav, whiteSidenav, darkMode} = ownerState;
@@ -32,30 +31,30 @@ function collapseItem(theme: Theme, ownerState: any)
const {pxToRem, rgba} = functions;
return {
background: () =>
background: () =>
{
let backgroundValue;
if (transparentSidenav && darkMode)
if (transparentSidenav && darkMode)
{
backgroundValue = active ? rgba(white.main, 0.2) : transparent.main;
}
else if (transparentSidenav && !darkMode)
else if (transparentSidenav && !darkMode)
{
backgroundValue = active ? grey[300] : transparent.main;
}
else if (whiteSidenav)
else if (whiteSidenav)
{
backgroundValue = active ? grey[200] : transparent.main;
}
else
else
{
backgroundValue = active ? rgba(white.main, 0.2) : transparent.main;
}
return backgroundValue;
},
color: (transparentSidenav && !darkMode) || whiteSidenav ? dark.main : white.main,
color: "var(--qqq-sidebar-text-color, #ffffff)",
display: "flex",
alignItems: "center",
width: "100%",
@@ -79,15 +78,12 @@ function collapseItem(theme: Theme, ownerState: any)
},
"&:hover, &:focus": {
backgroundColor:
transparentSidenav && !darkMode
? grey[300]
: rgba(whiteSidenav ? grey[400] : white.main, 0.2),
backgroundColor: "var(--qqq-sidebar-hover-background-color, rgba(255, 255, 255, 0.1))",
},
};
}
function collapseIconBox(theme: Theme, ownerState: any)
function collapseIconBox(theme: Theme, ownerState: any)
{
const {palette, transitions, borders, functions} = theme;
const {transparentSidenav, whiteSidenav, darkMode} = ownerState;
@@ -99,7 +95,7 @@ function collapseIconBox(theme: Theme, ownerState: any)
return {
minWidth: pxToRem(32),
minHeight: pxToRem(32),
color: (transparentSidenav && !darkMode) || whiteSidenav ? dark.main : white.main,
color: "var(--qqq-sidebar-icon-color, #ffffff)",
borderRadius: borderRadius.md,
display: "grid",
placeItems: "center",
@@ -109,13 +105,13 @@ function collapseIconBox(theme: Theme, ownerState: any)
}),
"& svg, svg g": {
color: transparentSidenav || whiteSidenav ? dark.main : white.main,
color: "var(--qqq-sidebar-icon-color, #ffffff)",
},
};
}
const collapseIcon = ({palette: {white, gradients}}: Theme, {active}: any) => ({
color: active ? white.main : gradients.dark.state,
const collapseIcon = (_theme: Theme, {active}: any) => ({
color: active ? "var(--qqq-sidebar-selected-text-color, #ffffff)" : "var(--qqq-sidebar-icon-color, #ffffff)",
});
function collapseText(theme: any, ownerState: any)
@@ -147,7 +143,7 @@ function collapseText(theme: any, ownerState: any)
};
}
function collapseArrow(theme: Theme, ownerState: any)
function collapseArrow(theme: Theme, ownerState: any)
{
const {palette, typography, transitions, breakpoints, functions} = theme;
const {noCollapse, transparentSidenav, whiteSidenav, miniSidenav, open, active, darkMode} =
@@ -162,25 +158,8 @@ function collapseArrow(theme: Theme, ownerState: any)
fontWeight: 700,
marginBottom: pxToRem(-1),
transform: open ? "rotate(0)" : "rotate(-180deg)",
color: () =>
{
let colorValue;
if (transparentSidenav && darkMode)
{
colorValue = open || active ? white.main : rgba(white.main, 0.25);
}
else if (transparentSidenav || whiteSidenav)
{
colorValue = open || active ? dark.main : rgba(dark.main, 0.25);
}
else
{
colorValue = open || active ? white.main : rgba(white.main, 0.5);
}
return colorValue;
},
color: open || active ? "var(--qqq-sidebar-selected-text-color, #ffffff)" : "var(--qqq-sidebar-icon-color, #ffffff)",
opacity: open || active ? 1 : 0.5,
transition: transitions.create(["color", "transform", "opacity"], {
easing: transitions.easing.easeInOut,
duration: transitions.duration.shorter,
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Theme} from "@mui/material/styles";
@@ -50,11 +49,7 @@ function item(theme: Theme | any, ownerState: any)
}
else if (active === "isParent" && transparentSidenav)
{
backgroundValue = grey[300];
}
else if (active === "isParent" && whiteSidenav)
{
backgroundValue = grey[200];
return "var(--qqq-sidebar-selected-background-color, rgba(255, 255, 255, 0.2))";
}
else if (active)
{
@@ -69,9 +64,7 @@ function item(theme: Theme | any, ownerState: any)
}),
"&:hover, &:focus": {
backgroundColor:
!active &&
rgba((transparentSidenav && !darkMode) || whiteSidenav ? grey[400] : white.main, 0.2),
backgroundColor: !active ? "var(--qqq-sidebar-hover-background-color, rgba(255, 255, 255, 0.1))" : undefined,
},
};
}
@@ -96,10 +89,7 @@ function itemContent(theme: Theme, ownerState: any)
position: "relative",
"& span": {
color:
((transparentSidenav && !darkMode) || whiteSidenav) && (active === "isParent" || !active)
? dark.main
: white.main,
color: active ? "var(--qqq-sidebar-selected-text-color, #ffffff)" : "var(--qqq-sidebar-text-color, #ffffff)",
fontWeight: active ? fontWeightRegular : fontWeightLight,
fontSize: size.sm,
opacity: miniSidenav ? 0 : 1,
@@ -110,10 +100,7 @@ function itemContent(theme: Theme, ownerState: any)
},
"&::before": {
color:
((transparentSidenav && !darkMode) || whiteSidenav) && (active === "isParent" || !active)
? dark.main
: white.main,
color: active ? "var(--qqq-sidebar-selected-text-color, #ffffff)" : "var(--qqq-sidebar-text-color, #ffffff)",
fontWeight: fontWeightRegular,
display: "flex",
alignItems: "center",
@@ -143,25 +130,8 @@ function itemArrow(theme: Theme, ownerState: any)
fontWeight: 700,
marginBottom: pxToRem(-1),
transform: open ? "rotate(0)" : "rotate(-180deg)",
color: () =>
{
let colorValue;
if (transparentSidenav && darkMode)
{
colorValue = open || active ? white.main : rgba(white.main, 0.25);
}
else if (transparentSidenav || whiteSidenav)
{
colorValue = open || active ? dark.main : rgba(dark.main, 0.25);
}
else
{
colorValue = open || active ? white.main : rgba(white.main, 0.5);
}
return colorValue;
},
color: open || active ? "var(--qqq-sidebar-selected-text-color, #ffffff)" : "var(--qqq-sidebar-icon-color, #ffffff)",
opacity: open || active ? 1 : 0.5,
transition: transitions.create(["color", "transform", "opacity"], {
easing: transitions.easing.easeInOut,
duration: transitions.duration.shorter,
+9 -10
View File
@@ -4,18 +4,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Box from "@mui/material/Box";
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Button from "@mui/material/Button";
@@ -167,7 +166,7 @@ export default styled(Button)(({theme, ownerState}: { theme?: Theme | any; owner
// styles for the button with variant="gradient"
const gradientStyles = () =>
{
// background value
// background value - always use gradients for consistent appearance
const backgroundValue =
color === "white" || !gradients[color]
? white.main
+12 -11
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ButtonProps} from "@mui/material";
@@ -46,7 +45,7 @@ export interface Props extends Omit<ButtonProps, "color" | "variant"> {
}
const MDButton: FC<Props> = forwardRef(
({color, variant, size, circular, iconOnly, children, ...rest}, ref) =>
({color, variant, size, circular, iconOnly, children, qqqId, ...rest}, ref) =>
{
const [controller] = useMaterialUIController();
const {darkMode} = controller;
@@ -55,6 +54,8 @@ const MDButton: FC<Props> = forwardRef(
<MDButtonRoot
{...rest}
ref={ref}
data-qqq-id={qqqId}
data-button-variant={variant}
ownerState={{color, variant, size, circular, iconOnly, darkMode}}
>
{children}
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {styled, Theme} from "@mui/material/styles";
@@ -0,0 +1,59 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {expect, it} from "@jest/globals";
import {ThemeProvider} from "@mui/material/styles";
import React from "react";
import {createRoot} from "react-dom/client";
import {act} from "react-dom/test-utils";
import theme from "qqq/components/legacy/Theme";
import MDInput from "./index";
/*******************************************************************************
** A disabled field must receive native input semantics as well as styling.
*******************************************************************************/
it("updates native disabled state when the form metadata changes", () =>
{
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
const host = document.createElement("div");
document.body.appendChild(host);
const root = createRoot(host);
const render = (disabled: boolean) => act(() => root.render(<ThemeProvider theme={theme}>
<MDInput name="createDate" type="datetime-local" disabled={disabled} />
</ThemeProvider>));
try
{
render(true);
expect(host.querySelector("input").disabled).toBe(true);
host.querySelector("input").focus();
expect(document.activeElement).not.toBe(host.querySelector("input"));
render(false);
expect(host.querySelector("input").disabled).toBe(false);
host.querySelector("input").focus();
expect(document.activeElement).toBe(host.querySelector("input"));
render(true);
expect(host.querySelector("input").disabled).toBe(true);
}
finally
{
act(() => root.unmount());
host.remove();
}
});
+10 -11
View File
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {OutlinedTextFieldProps, StandardTextFieldProps} from "@mui/material";
@@ -31,7 +30,7 @@ interface Props extends Omit<OutlinedTextFieldProps | StandardTextFieldProps, "v
}
const MDInput: FC<Props | any> = forwardRef(({error, success, disabled, ...rest}, ref) => (
<MDInputRoot {...rest} ref={ref} ownerState={{error, success, disabled}} />
<MDInputRoot {...rest} ref={ref} disabled={disabled} ownerState={{error, success, disabled}} />
));
// Declaring default props for MDInput
@@ -5,18 +5,17 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {styled, Theme} from "@mui/material/styles";
@@ -5,21 +5,21 @@
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* https://www.apache.org/licenses/LICENSE-2.0
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Box from "@mui/material/Box";
import {preferredColorNameInfoOrPrimary} from "qqq/assets/theme/functions/preferInfoColorToPrimaryColor";
import {createContext, FC, forwardRef, ReactNode, useContext, useMemo} from "react";
import MDPaginationItemRoot from "qqq/components/legacy/MDPagination/MDPaginationItemRoot";
@@ -54,6 +54,11 @@ const MDPagination: FC<Props | any> = forwardRef(
const context: any = useContext(Context);
const paginationSize = context ? context.size : undefined;
if(!color)
{
color = preferredColorNameInfoOrPrimary();
}
const providerValue = useMemo(
() => ({
variant,
@@ -96,7 +101,6 @@ const MDPagination: FC<Props | any> = forwardRef(
MDPagination.defaultProps = {
item: false,
variant: "gradient",
color: "info",
size: "medium",
active: false,
};
Loaded 100 of 350 files, more files were not shown because too many files have changed in this diff. Show more