@cdmbase/wiki-browser 12.0.18-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/lib/components/Logo.d.ts +4 -0
- package/lib/components/Logo.d.ts.map +1 -0
- package/lib/components/Logo.js +16 -0
- package/lib/components/Logo.js.map +1 -0
- package/lib/components/help/SidebarSearch.d.ts +8 -0
- package/lib/components/help/SidebarSearch.d.ts.map +1 -0
- package/lib/components/help/SidebarSearch.js +111 -0
- package/lib/components/help/SidebarSearch.js.map +1 -0
- package/lib/components/help/index.d.ts +2 -0
- package/lib/components/help/index.d.ts.map +1 -0
- package/lib/components/landing/FeatureCard.d.ts +13 -0
- package/lib/components/landing/FeatureCard.d.ts.map +1 -0
- package/lib/components/landing/FeatureCard.js +85 -0
- package/lib/components/landing/FeatureCard.js.map +1 -0
- package/lib/components/landing/QuickLinkCard.d.ts +8 -0
- package/lib/components/landing/QuickLinkCard.d.ts.map +1 -0
- package/lib/components/landing/QuickLinkCard.js +26 -0
- package/lib/components/landing/QuickLinkCard.js.map +1 -0
- package/lib/components/landing/SearchInput.d.ts +10 -0
- package/lib/components/landing/SearchInput.d.ts.map +1 -0
- package/lib/components/landing/SearchInput.js +223 -0
- package/lib/components/landing/SearchInput.js.map +1 -0
- package/lib/components/landing/index.d.ts +4 -0
- package/lib/components/landing/index.d.ts.map +1 -0
- package/lib/components/welcome.d.ts +3 -0
- package/lib/components/welcome.d.ts.map +1 -0
- package/lib/compute.d.ts +4 -0
- package/lib/compute.d.ts.map +1 -0
- package/lib/compute.js +96 -0
- package/lib/compute.js.map +1 -0
- package/lib/config/env-config.d.ts +4 -0
- package/lib/config/env-config.d.ts.map +1 -0
- package/lib/config/env-config.js +7 -0
- package/lib/config/env-config.js.map +1 -0
- package/lib/docs.config.d.ts +48 -0
- package/lib/docs.config.d.ts.map +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -0
- package/lib/loaders/search.d.ts +1 -0
- package/lib/loaders/search.d.ts.map +1 -0
- package/lib/module.d.ts +4 -0
- package/lib/module.d.ts.map +1 -0
- package/lib/module.js +11 -0
- package/lib/module.js.map +1 -0
- package/lib/pages/ArticlePage/ArticlePage.d.ts +4 -0
- package/lib/pages/ArticlePage/ArticlePage.d.ts.map +1 -0
- package/lib/pages/ArticlePage/ArticlePage.js +222 -0
- package/lib/pages/ArticlePage/ArticlePage.js.map +1 -0
- package/lib/pages/ArticlePage/index.d.ts +3 -0
- package/lib/pages/ArticlePage/index.d.ts.map +1 -0
- package/lib/pages/ArticlePage/index.js +3 -0
- package/lib/pages/ArticlePage/index.js.map +1 -0
- package/lib/pages/CategoryCollection/CategoryCollection.d.ts +4 -0
- package/lib/pages/CategoryCollection/CategoryCollection.d.ts.map +1 -0
- package/lib/pages/CategoryCollection/CategoryCollection.js +103 -0
- package/lib/pages/CategoryCollection/CategoryCollection.js.map +1 -0
- package/lib/pages/CategoryCollection/index.d.ts +3 -0
- package/lib/pages/CategoryCollection/index.d.ts.map +1 -0
- package/lib/pages/CategoryCollection/index.js +3 -0
- package/lib/pages/CategoryCollection/index.js.map +1 -0
- package/lib/pages/Help/HelpIndex.d.ts +4 -0
- package/lib/pages/Help/HelpIndex.d.ts.map +1 -0
- package/lib/pages/Help/HelpIndex.js +44 -0
- package/lib/pages/Help/HelpIndex.js.map +1 -0
- package/lib/pages/Help/index.d.ts +4 -0
- package/lib/pages/Help/index.d.ts.map +1 -0
- package/lib/pages/Help/index.js +226 -0
- package/lib/pages/Help/index.js.map +1 -0
- package/lib/pages/Landing/index.d.ts +3 -0
- package/lib/pages/Landing/index.d.ts.map +1 -0
- package/lib/pages/Landing/index.js +281 -0
- package/lib/pages/Landing/index.js.map +1 -0
- package/lib/routes.json +2533 -0
- package/lib/seo.d.ts +22 -0
- package/lib/seo.d.ts.map +1 -0
- package/lib/slot-fill/FooterFill.d.ts +3 -0
- package/lib/slot-fill/FooterFill.d.ts.map +1 -0
- package/lib/slot-fill/FooterFill.js +18 -0
- package/lib/slot-fill/FooterFill.js.map +1 -0
- package/lib/slot-fill/LogoFill.d.ts +5 -0
- package/lib/slot-fill/LogoFill.d.ts.map +1 -0
- package/lib/slot-fill/LogoFill.js +74 -0
- package/lib/slot-fill/LogoFill.js.map +1 -0
- package/lib/slot-fill/consts.d.ts +5 -0
- package/lib/slot-fill/consts.d.ts.map +1 -0
- package/lib/slot-fill/consts.js +1 -0
- package/lib/slot-fill/consts.js.map +1 -0
- package/lib/slot-fill/index.d.ts +4 -0
- package/lib/slot-fill/index.d.ts.map +1 -0
- package/lib/templates/assets/images/add-link-frontend.png +0 -0
- package/lib/templates/assets/images/add-package-backend.png +0 -0
- package/lib/templates/assets/images/add-to-backend-module.png +0 -0
- package/lib/templates/assets/images/add-upload-client-frontend.png +0 -0
- package/lib/templates/assets/images/additional-parameters.png +0 -0
- package/lib/templates/assets/images/aeh-implementation.png +0 -0
- package/lib/templates/assets/images/aeh-usage.png +0 -0
- package/lib/templates/assets/images/apollo-client/recommendation_cache_mgmt.png +0 -0
- package/lib/templates/assets/images/app-deploy-new-version/jenkins1.PNG +0 -0
- package/lib/templates/assets/images/app-deploy-new-version/jenkins2.PNG +0 -0
- package/lib/templates/assets/images/auth-wrapper-code.png +0 -0
- package/lib/templates/assets/images/cdebase.png +0 -0
- package/lib/templates/assets/images/cdm-locales-directory.png +0 -0
- package/lib/templates/assets/images/client-settings.png +0 -0
- package/lib/templates/assets/images/codegen_file_update.png +0 -0
- package/lib/templates/assets/images/configuration.png +0 -0
- package/lib/templates/assets/images/copy-plugin.png +0 -0
- package/lib/templates/assets/images/docusaurus.png +0 -0
- package/lib/templates/assets/images/error-link.png +0 -0
- package/lib/templates/assets/images/error-sample.png +0 -0
- package/lib/templates/assets/images/extension copy.png +0 -0
- package/lib/templates/assets/images/extension.png +0 -0
- package/lib/templates/assets/images/graphql/graphql-folder-backend.png +0 -0
- package/lib/templates/assets/images/graphql/graphql-folder-with-gql.png +0 -0
- package/lib/templates/assets/images/i18n-config.png +0 -0
- package/lib/templates/assets/images/image.png +0 -0
- package/lib/templates/assets/images/logo.svg +10 -0
- package/lib/templates/assets/images/logo1.svg +1 -0
- package/lib/templates/assets/images/modify-upload-false-server.png +0 -0
- package/lib/templates/assets/images/navigation-auth-enabled.png +0 -0
- package/lib/templates/assets/images/org-dashboard-navigation.png +0 -0
- package/lib/templates/assets/images/org-navigation.png +0 -0
- package/lib/templates/assets/images/preferences_graphql_type.png +0 -0
- package/lib/templates/assets/images/provider.png +0 -0
- package/lib/templates/assets/images/route-config.png +0 -0
- package/lib/templates/assets/images/service-accounts.png +0 -0
- package/lib/templates/assets/images/source-code/source-code-environments.png +0 -0
- package/lib/templates/assets/images/source-code/source-code-organization.png +0 -0
- package/lib/templates/assets/images/spin-clone-develop-deployment/jenkins-changes.png +0 -0
- package/lib/templates/assets/images/spin-clone-develop-deployment/lerna-changes.png +0 -0
- package/lib/templates/assets/images/spin-clone-develop-deployment/root-package-json-changes.png +0 -0
- package/lib/templates/assets/images/spin-clone-develop-deployment/values-dev-changes.png +0 -0
- package/lib/templates/assets/images/sso-mappers.png +0 -0
- package/lib/templates/assets/images/sso-picture-mapper.png +0 -0
- package/lib/templates/assets/images/sso-settings.png +0 -0
- package/lib/templates/assets/images/timesheet_apollo_cache.png +0 -0
- package/lib/templates/assets/images/timesheet_query.png +0 -0
- package/lib/templates/assets/images/tutorial/docsVersionDropdown.png +0 -0
- package/lib/templates/assets/images/tutorial/localeDropdown.png +0 -0
- package/lib/templates/assets/images/unauthenticated.png +0 -0
- package/lib/templates/assets/images/undraw_docusaurus_mountain.svg +170 -0
- package/lib/templates/assets/images/undraw_docusaurus_react.svg +169 -0
- package/lib/templates/assets/images/undraw_docusaurus_tree.svg +1 -0
- package/lib/templates/assets/images/vite-plugin-config.png +0 -0
- package/lib/templates/content/docs/Generators/Project/generate-fullproject.md +12 -0
- package/lib/templates/content/docs/LLM/Logger.llm.md +194 -0
- package/lib/templates/content/docs/LLM/backend-proxies-services-llm.md +2687 -0
- package/lib/templates/content/docs/LLM/backend-service-llm.md +3384 -0
- package/lib/templates/content/docs/LLM/db_migration_llm.md +954 -0
- package/lib/templates/content/docs/LLM/frontend/REMIX-15.3-upgrade-llm.md +1245 -0
- package/lib/templates/content/docs/LLM/inngest/INNGEST_FUNCTION_DEVELOPMENT_GUIDE_LLM.md +1241 -0
- package/lib/templates/content/docs/LLM/inngest/INNGEST_NAMESPACE_LLM.md +384 -0
- package/lib/templates/content/docs/LLM/llm_workflow_namespace.md +384 -0
- package/lib/templates/content/docs/LLM/organization-components-form-llm.md +1395 -0
- package/lib/templates/content/docs/LLM/page-component-llm.md +173 -0
- package/lib/templates/content/docs/LLM/preferences-settings-llm.md +2781 -0
- package/lib/templates/content/docs/LLM/tailwind-css-llm.md +502 -0
- package/lib/templates/content/docs/UI/SchemaBasedUI.md +334 -0
- package/lib/templates/content/docs/UI/SlotFillComponent.md +334 -0
- package/lib/templates/content/docs/adminide-modules/account/auth0-login.md +31 -0
- package/lib/templates/content/docs/adminide-modules/account/index.md +14 -0
- package/lib/templates/content/docs/adminide-modules/account/keycloak-remix-setup.md +86 -0
- package/lib/templates/content/docs/adminide-modules/account/remix-auth-setup.md +79 -0
- package/lib/templates/content/docs/adminide-modules/account/various-auth-qatest.md +157 -0
- package/lib/templates/content/docs/adminide-modules/api-builders/graphql.md +906 -0
- package/lib/templates/content/docs/adminide-modules/billing/payments/index.md +14 -0
- package/lib/templates/content/docs/adminide-modules/billing/payments/stripe/index.md +14 -0
- package/lib/templates/content/docs/adminide-modules/billing/payments/stripe/settingup-stripe-locally.md +25 -0
- package/lib/templates/content/docs/adminide-modules/billing/tier-config.md +293 -0
- package/lib/templates/content/docs/adminide-modules/connectors/Connector.md +207 -0
- package/lib/templates/content/docs/adminide-modules/file-upload/index.md +16 -0
- package/lib/templates/content/docs/adminide-modules/file-upload/setup.md +435 -0
- package/lib/templates/content/docs/adminide-modules/file-upload/upload-file-using-signed-url.md +161 -0
- package/lib/templates/content/docs/adminide-modules/preferences/AddAdditionalPermissions.md +151 -0
- package/lib/templates/content/docs/adminide-modules/preferences/Configuration.md +241 -0
- package/lib/templates/content/docs/adminide-modules/preferences/Policy-Configuration.md +61 -0
- package/lib/templates/content/docs/adminide-modules/preferences/UI-components/ResourceSettingsLoader.md +319 -0
- package/lib/templates/content/docs/adminide-modules/preferences/contribute_scope_target.md +280 -0
- package/lib/templates/content/docs/adminide-modules/preferences/generate-urii.md +94 -0
- package/lib/templates/content/docs/adminide-modules/preferences/index.md +28 -0
- package/lib/templates/content/docs/adminide-modules/preferences/machine-configuration.md +157 -0
- package/lib/templates/content/docs/adminide-modules/preferences/pageSettings/generateCdecodeUri.md +1289 -0
- package/lib/templates/content/docs/adminide-modules/preferences/pageSettings/migratingFromUseSettings.md +215 -0
- package/lib/templates/content/docs/adminide-modules/preferences/permissions/Roles-Permissions.md +72 -0
- package/lib/templates/content/docs/adminide-modules/preferences/permissions/settingUserPermissions.md +139 -0
- package/lib/templates/content/docs/adminide-modules/preferences/preference-dependency.md +138 -0
- package/lib/templates/content/docs/adminide-modules/preferences/route-based-configuration.md +41 -0
- package/lib/templates/content/docs/adminide-modules/preferences/schema-configuration.md +71 -0
- package/lib/templates/content/docs/adminide-modules/preferences/supported.md +24 -0
- package/lib/templates/content/docs/adminide-modules/preferences/useSettingsLoader.md +248 -0
- package/lib/templates/content/docs/adminide-modules/project-tools/auth-providers.md +1317 -0
- package/lib/templates/content/docs/adminide-modules/project-tools/keycloak-guide.md +543 -0
- package/lib/templates/content/docs/adminide-modules/project-tools/tenant-management/tenant-based-authentication.md +846 -0
- package/lib/templates/content/docs/adminide-modules/project-tools/tenant-management/tenant-management.md +708 -0
- package/lib/templates/content/docs/adminide-modules/project-tools/tenant-management/tenants.md +1117 -0
- package/lib/templates/content/docs/chrome-extension/index.md +14 -0
- package/lib/templates/content/docs/chrome-extension/setup.md +30 -0
- package/lib/templates/content/docs/contributing/adding-package.md +23 -0
- package/lib/templates/content/docs/contributing/adding_new_modules.md +99 -0
- package/lib/templates/content/docs/contributing/architecture-updates.md +19 -0
- package/lib/templates/content/docs/contributing/avoid-using-promises-ui.md +116 -0
- package/lib/templates/content/docs/contributing/coding-guidelines.md +111 -0
- package/lib/templates/content/docs/contributing/do-and-dont.md +42 -0
- package/lib/templates/content/docs/contributing/faq.md +22 -0
- package/lib/templates/content/docs/contributing/folder-setup/browser.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/config.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/containers-server.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/core.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/graphql.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/index.md +30 -0
- package/lib/templates/content/docs/contributing/folder-setup/module.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/server.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/services.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/store.md +12 -0
- package/lib/templates/content/docs/contributing/frontend-coding.md +30 -0
- package/lib/templates/content/docs/contributing/git-subtree-sharing.md +73 -0
- package/lib/templates/content/docs/contributing/graphql-subscriptions.md +69 -0
- package/lib/templates/content/docs/contributing/how-to-contribute.md +30 -0
- package/lib/templates/content/docs/contributing/how_to_check_pure_esm.md +29 -0
- package/lib/templates/content/docs/contributing/index.md +60 -0
- package/lib/templates/content/docs/contributing/installation-issues.md +23 -0
- package/lib/templates/content/docs/contributing/keyboard-shortcut.md +131 -0
- package/lib/templates/content/docs/contributing/language/locale-support.md +12 -0
- package/lib/templates/content/docs/contributing/lerna-build-tools.md +516 -0
- package/lib/templates/content/docs/contributing/lerna-yarn-workspaces.md +95 -0
- package/lib/templates/content/docs/contributing/lint-and-formatter.md +20 -0
- package/lib/templates/content/docs/contributing/mobile-setup.md +16 -0
- package/lib/templates/content/docs/contributing/project-setup.md +233 -0
- package/lib/templates/content/docs/contributing/react/index.md +14 -0
- package/lib/templates/content/docs/contributing/react/lazy-component.md +70 -0
- package/lib/templates/content/docs/contributing/run-various-options.md +124 -0
- package/lib/templates/content/docs/contributing/schema-first-graphql-types.md +37 -0
- package/lib/templates/content/docs/contributing/source-code-organization.md +57 -0
- package/lib/templates/content/docs/contributing/staging-docker.md +88 -0
- package/lib/templates/content/docs/contributing/third-party/apollo-client-v3-tutorials.md +28 -0
- package/lib/templates/content/docs/contributing/third-party/index.md +18 -0
- package/lib/templates/content/docs/contributing/typescript-contribution.md +16 -0
- package/lib/templates/content/docs/devops/app-deploy-new-version.md +30 -0
- package/lib/templates/content/docs/devops/index.md +14 -0
- package/lib/templates/content/docs/devops/mobile-jenkins-build.md +40 -0
- package/lib/templates/content/docs/devops/versioning-the-project.md +128 -0
- package/lib/templates/content/docs/error-handler/application-error-handler.md +40 -0
- package/lib/templates/content/docs/error-handler/error-handling.md +26 -0
- package/lib/templates/content/docs/error-handler/index.md +16 -0
- package/lib/templates/content/docs/error-handler/logging-errors.md +14 -0
- package/lib/templates/content/docs/feature-api/copy-operation.md +427 -0
- package/lib/templates/content/docs/feature-api/feature-browser/assets.md +46 -0
- package/lib/templates/content/docs/feature-api/feature-browser/auth-permissions.md +12 -0
- package/lib/templates/content/docs/feature-api/feature-browser/feature.md +131 -0
- package/lib/templates/content/docs/feature-api/feature-browser/index.md +22 -0
- package/lib/templates/content/docs/feature-api/feature-browser/routes-menu.md +110 -0
- package/lib/templates/content/docs/feature-api/feature-browser/routing-convention.md +124 -0
- package/lib/templates/content/docs/feature-api/feature-browser/routing.md +338 -0
- package/lib/templates/content/docs/feature-api/feature-mobile/auth-permissions.md +20 -0
- package/lib/templates/content/docs/feature-api/feature-mobile/feature.md +130 -0
- package/lib/templates/content/docs/feature-api/feature-mobile/index.md +18 -0
- package/lib/templates/content/docs/feature-api/feature-mobile/navigation.md +187 -0
- package/lib/templates/content/docs/feature-api/feature-server/Scheduling.md +44 -0
- package/lib/templates/content/docs/feature-api/feature-server/dataloader.md +320 -0
- package/lib/templates/content/docs/feature-api/feature-server/dependency-injection.md +81 -0
- package/lib/templates/content/docs/feature-api/feature-server/feature.md +65 -0
- package/lib/templates/content/docs/feature-api/feature-server/generic-dataloader.md +135 -0
- package/lib/templates/content/docs/feature-api/feature-server/index.md +40 -0
- package/lib/templates/content/docs/feature-api/feature-server/migration.md +127 -0
- package/lib/templates/content/docs/feature-api/feature-server/mongo-model.md +72 -0
- package/lib/templates/content/docs/feature-api/feature-server/permissions.md +12 -0
- package/lib/templates/content/docs/feature-api/feature-server/policies.md +57 -0
- package/lib/templates/content/docs/feature-api/feature-server/preferences.md +57 -0
- package/lib/templates/content/docs/feature-api/feature-server/repositories.md +114 -0
- package/lib/templates/content/docs/feature-api/feature-server/resolvers.md +126 -0
- package/lib/templates/content/docs/feature-api/feature-server/rules.md +132 -0
- package/lib/templates/content/docs/feature-api/feature-server/schema.md +12 -0
- package/lib/templates/content/docs/feature-api/feature-server/services.md +102 -0
- package/lib/templates/content/docs/feature-api/feature-server/setup-resource-crud.md +359 -0
- package/lib/templates/content/docs/feature-api/index.md +18 -0
- package/lib/templates/content/docs/graphql/apolloClient-mutation.md +94 -0
- package/lib/templates/content/docs/graphql/index.md +14 -0
- package/lib/templates/content/docs/graphql/scalars.md +15 -0
- package/lib/templates/content/docs/help/index.md +14 -0
- package/lib/templates/content/docs/help/intro.md +16 -0
- package/lib/templates/content/docs/intl/ant-design-menu-translation.md +74 -0
- package/lib/templates/content/docs/intl/intl-namespace.md +129 -0
- package/lib/templates/content/docs/intl/vite-plugin-intl.md +87 -0
- package/lib/templates/content/docs/intl/webpack-plugin-intl.md +12 -0
- package/lib/templates/content/docs/intro.md +18 -0
- package/lib/templates/content/docs/knowledge/basic-fullstack.md +238 -0
- package/lib/templates/content/docs/mailing/index.md +14 -0
- package/lib/templates/content/docs/mailing/mailing-template.md +148 -0
- package/lib/templates/content/docs/mobile/App-navigation-generator.md +410 -0
- package/lib/templates/content/docs/mobile/MobileTestCases.md +264 -0
- package/lib/templates/content/docs/mobile/eas-profile-build.md +107 -0
- package/lib/templates/content/docs/mobile/expo-push-notification-setup.md +216 -0
- package/lib/templates/content/docs/mobile/index.md +14 -0
- package/lib/templates/content/docs/mobile/routes.md +83 -0
- package/lib/templates/content/docs/organization/adding-account-context.md +116 -0
- package/lib/templates/content/docs/organization/adding-org-mobile-navigation.md +22 -0
- package/lib/templates/content/docs/organization/adding-org-web-navigation.md +12 -0
- package/lib/templates/content/docs/organization/index.md +20 -0
- package/lib/templates/content/docs/organization/initialization.md +20 -0
- package/lib/templates/content/docs/organization/organization-resource-vs-resource.md +112 -0
- package/lib/templates/content/docs/remix/configuration/component-structure-best-practices.md +152 -0
- package/lib/templates/content/docs/remix/configuration/configurations.md +218 -0
- package/lib/templates/content/docs/remix/configuration/css-import-and-stylesheets.md +142 -0
- package/lib/templates/content/docs/remix/configuration/dont-subcomponent-network.md +166 -0
- package/lib/templates/content/docs/remix/configuration/generated-data-loaders.md +122 -0
- package/lib/templates/content/docs/remix/configuration/generated-resource-loaders.md +257 -0
- package/lib/templates/content/docs/remix/configuration/query-params-generator.md +216 -0
- package/lib/templates/content/docs/remix/configuration/routes-extra-icons.md +103 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-advanced.md +86 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-auth.md +113 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-best-practices.md +55 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-fields.md +79 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-graphql.md +79 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-index.md +112 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-loaders.md +165 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-middleware.md +196 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-overview.md +53 -0
- package/lib/templates/content/docs/remix/data-loaders.md +43 -0
- package/lib/templates/content/docs/remix/devtools/remix-devtools.md +58 -0
- package/lib/templates/content/docs/remix/examples/changes-using-servercode.md +79 -0
- package/lib/templates/content/docs/remix/extra-icons.md +62 -0
- package/lib/templates/content/docs/remix/extra-links.md +65 -0
- package/lib/templates/content/docs/remix/generated-data-loaders.md +114 -0
- package/lib/templates/content/docs/remix/queryParamsGenerator.md +89 -0
- package/lib/templates/content/docs/remix/resources.md +16 -0
- package/lib/templates/content/docs/remix/styles.md +132 -0
- package/lib/templates/content/docs/remix/wiki.md +12 -0
- package/lib/templates/content/docs/security/auth-wrapper/auth-wrapper.md +24 -0
- package/lib/templates/content/docs/security/index.md +18 -0
- package/lib/templates/content/docs/security/secure-button-mobilenative.md +88 -0
- package/lib/templates/content/docs/security/secure-button-web.md +89 -0
- package/lib/templates/content/docs/server-side/account-customization.md +82 -0
- package/lib/templates/content/docs/server-side/apollo/caching.md +164 -0
- package/lib/templates/content/docs/server-side/backend-architecture/FINAL-DECISION.md +209 -0
- package/lib/templates/content/docs/server-side/backend-architecture/TRUE-FINAL-ARCHITECTURE.md +603 -0
- package/lib/templates/content/docs/server-side/backend-architecture/index1.md +0 -0
- package/lib/templates/content/docs/server-side/backend-coding.md +839 -0
- package/lib/templates/content/docs/server-side/e2b/manageing-template.md +197 -0
- package/lib/templates/content/docs/server-side/index.md +14 -0
- package/lib/templates/content/docs/server-side/inngest-functions-module.md +309 -0
- package/lib/templates/content/docs/server-side/listen-stripe-events.md +43 -0
- package/lib/templates/content/docs/server-side/slug-service.md +323 -0
- package/lib/templates/content/docs/tests/index.md +18 -0
- package/lib/templates/content/docs/tests/jest-test-debug-vscode.md +40 -0
- package/lib/templates/content/docs/tests/known-errors.md +116 -0
- package/lib/templates/content/docs/tests/service-test-template.md +118 -0
- package/lib/templates/content/docs/tests/test-setup.md +93 -0
- package/lib/templates/content/docs/xstate.md +23 -0
- package/lib/types.d.ts +37 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/utils/docsNavigation.d.ts +9 -0
- package/lib/utils/docsNavigation.d.ts.map +1 -0
- package/lib/utils/docsNavigation.js +37 -0
- package/lib/utils/docsNavigation.js.map +1 -0
- package/lib/utils/helpCenterUtils.d.ts +26 -0
- package/lib/utils/helpCenterUtils.d.ts.map +1 -0
- package/lib/utils/index.d.ts +3 -0
- package/lib/utils/index.d.ts.map +1 -0
- package/lib/utils/index.js +3 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/markdownLoader.d.ts +36 -0
- package/lib/utils/markdownLoader.d.ts.map +1 -0
- package/lib/utils/markdownLoader.js +2242 -0
- package/lib/utils/markdownLoader.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,1395 @@
|
|
|
1
|
+
# ๐จ Frontend: Organization-Scoped Form Creation Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This guide covers creating React forms that work with organization-scoped entities in the AdminIDE stack. The pattern ensures proper organization context is maintained throughout the form lifecycle, from data fetching to mutations.
|
|
6
|
+
|
|
7
|
+
**Use this guide when creating forms for entities that belong to an organization (Tags, Projects, Vaults, etc.)**
|
|
8
|
+
|
|
9
|
+
## ๐ Core Principles
|
|
10
|
+
|
|
11
|
+
1. **Organization from Route Parameters**: Extract `orgName` from URL params
|
|
12
|
+
2. **GraphQL Variables**: Pass `orgName` to all queries requiring organization context
|
|
13
|
+
3. **State Management**: Use XState machines for complex form workflows
|
|
14
|
+
4. **Type Safety**: Leverage generated GraphQL types from `common/graphql`
|
|
15
|
+
5. **Error Handling**: Handle partial data gracefully with error policies
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## ๐ Complete Frontend Implementation Pattern
|
|
20
|
+
|
|
21
|
+
### **Step 1: React Component Structure**
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
// TagsView.tsx - Organization-scoped entity management
|
|
25
|
+
import React, { useState } from 'react';
|
|
26
|
+
import { useMachine } from '@xstate/react';
|
|
27
|
+
import { fromPromise } from 'xstate';
|
|
28
|
+
import { useLoaderData, useParams } from '@remix-run/react';
|
|
29
|
+
import {
|
|
30
|
+
useCreateTagMutation,
|
|
31
|
+
useRemoveTagMutation,
|
|
32
|
+
useGetTagsQuery,
|
|
33
|
+
useUpdateTagMutation,
|
|
34
|
+
useGetTagTypesQuery,
|
|
35
|
+
useGetProjectsQuery,
|
|
36
|
+
} from 'common/graphql';
|
|
37
|
+
import { tagsMachine, Tag } from '../tagsMachine';
|
|
38
|
+
|
|
39
|
+
export default function TagsView() {
|
|
40
|
+
// ๐ CRITICAL: Extract organization from route params
|
|
41
|
+
const { orgName } = useParams<{ orgName: string }>();
|
|
42
|
+
const loaderData: any = useLoaderData();
|
|
43
|
+
const userId = loaderData.dataContext.userId;
|
|
44
|
+
|
|
45
|
+
// ๐ CRITICAL: GraphQL Queries with orgName variable
|
|
46
|
+
const {
|
|
47
|
+
data: tagsData,
|
|
48
|
+
refetch: tagsRefetch,
|
|
49
|
+
loading: tagsLoading,
|
|
50
|
+
error: tagsError,
|
|
51
|
+
} = useGetTagsQuery({
|
|
52
|
+
variables: { orgName }, // Pass organization context
|
|
53
|
+
errorPolicy: 'all', // Allow partial data
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const { data: projectsData, error: projectsError } = useGetProjectsQuery({
|
|
57
|
+
variables: { orgName }, // Organization context required
|
|
58
|
+
fetchPolicy: 'cache-only',
|
|
59
|
+
errorPolicy: 'all',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const { data: tagTypesData, error: tagTypesError } = useGetTagTypesQuery({
|
|
63
|
+
errorPolicy: 'all',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ๐ GraphQL Mutations
|
|
67
|
+
const [createTag] = useCreateTagMutation();
|
|
68
|
+
const [updateTag] = useUpdateTagMutation();
|
|
69
|
+
const [removeTag] = useRemoveTagMutation();
|
|
70
|
+
|
|
71
|
+
// Toast notifications for UX feedback
|
|
72
|
+
const [toast, setToast] = useState({ show: false, message: '', type: '' });
|
|
73
|
+
|
|
74
|
+
// ๐ XState Machine with actor implementations
|
|
75
|
+
const [state, send] = useMachine(
|
|
76
|
+
tagsMachine.provide({
|
|
77
|
+
actors: {
|
|
78
|
+
// Fetch actor - loads tags from query data
|
|
79
|
+
fetchTags: fromPromise(async () => {
|
|
80
|
+
const tags = tagsData?.getTags?.data || [];
|
|
81
|
+
// Filter out null/invalid entries
|
|
82
|
+
return tags.filter((tag) => tag && tag.id);
|
|
83
|
+
}),
|
|
84
|
+
|
|
85
|
+
// Fetch related data
|
|
86
|
+
fetchProjects: fromPromise(async () => {
|
|
87
|
+
const projects = projectsData?.getProjects?.data || [];
|
|
88
|
+
return projects.filter((project) => project && project.id);
|
|
89
|
+
}),
|
|
90
|
+
|
|
91
|
+
fetchTagTypes: fromPromise(async () => {
|
|
92
|
+
const tagTypes = tagTypesData?.getTagTypes || [];
|
|
93
|
+
return tagTypes.filter((tagType) => tagType && tagType.id);
|
|
94
|
+
}),
|
|
95
|
+
|
|
96
|
+
// Submit actor - creates new tag
|
|
97
|
+
submitTag: fromPromise(async ({ input }) => {
|
|
98
|
+
const { tag } = input;
|
|
99
|
+
|
|
100
|
+
// Client-side validation
|
|
101
|
+
if (!tag.project?.id || !tag.tagType?.id) {
|
|
102
|
+
setToast({
|
|
103
|
+
show: true,
|
|
104
|
+
message: 'Please select both a project and a tag type',
|
|
105
|
+
type: 'error',
|
|
106
|
+
});
|
|
107
|
+
setTimeout(() => setToast({ show: false, message: '', type: '' }), 3000);
|
|
108
|
+
throw new Error('Project and tag type must be selected.');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ๐ CRITICAL: Create mutation
|
|
112
|
+
// Note: organization context comes from backend @addAccountContext
|
|
113
|
+
// No need to pass orgName/orgId in mutation variables
|
|
114
|
+
const response = await createTag({
|
|
115
|
+
variables: {
|
|
116
|
+
request: {
|
|
117
|
+
projectId: tag.project.id,
|
|
118
|
+
tagTypeId: tag.tagType.id,
|
|
119
|
+
name: tag.name,
|
|
120
|
+
description: tag.description || '',
|
|
121
|
+
createdBy: userId,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
update: (cache, { data, errors }) => {
|
|
125
|
+
if (data) {
|
|
126
|
+
setToast({
|
|
127
|
+
show: true,
|
|
128
|
+
message: 'Tag has been created!',
|
|
129
|
+
type: 'success',
|
|
130
|
+
});
|
|
131
|
+
setTimeout(() => setToast({ show: false, message: '', type: '' }), 3000);
|
|
132
|
+
} else if (errors) {
|
|
133
|
+
setToast({
|
|
134
|
+
show: true,
|
|
135
|
+
message: `Failed to create tag: ${errors[0]?.message}`,
|
|
136
|
+
type: 'error',
|
|
137
|
+
});
|
|
138
|
+
setTimeout(() => setToast({ show: false, message: '', type: '' }), 4000);
|
|
139
|
+
throw new Error(errors[0].message);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Refresh the list
|
|
145
|
+
tagsRefetch();
|
|
146
|
+
return response.data?.createTag;
|
|
147
|
+
}),
|
|
148
|
+
|
|
149
|
+
// Update actor
|
|
150
|
+
updateTag: fromPromise(async ({ input }) => {
|
|
151
|
+
const { tag } = input;
|
|
152
|
+
const response = await updateTag({
|
|
153
|
+
variables: {
|
|
154
|
+
id: tag.id,
|
|
155
|
+
request: {
|
|
156
|
+
name: tag.name,
|
|
157
|
+
description: tag.description,
|
|
158
|
+
projectId: tag.project?.id,
|
|
159
|
+
tagTypeId: tag.tagType?.id,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
tagsRefetch();
|
|
164
|
+
return response.data?.updateTag;
|
|
165
|
+
}),
|
|
166
|
+
|
|
167
|
+
// Delete actor
|
|
168
|
+
deleteTag: fromPromise(async ({ input }) => {
|
|
169
|
+
const { id } = input;
|
|
170
|
+
await removeTag({
|
|
171
|
+
variables: { id },
|
|
172
|
+
update: (cache, { data, errors }) => {
|
|
173
|
+
if (data) {
|
|
174
|
+
setToast({
|
|
175
|
+
show: true,
|
|
176
|
+
message: 'Tag has been deleted!',
|
|
177
|
+
type: 'success',
|
|
178
|
+
});
|
|
179
|
+
setTimeout(() => setToast({ show: false, message: '', type: '' }), 3000);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
return { id };
|
|
184
|
+
}),
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const { filteredTags, projects, tagTypes, searchQuery, isLoading } = state.context;
|
|
190
|
+
|
|
191
|
+
// Trigger data fetch when GraphQL data is available
|
|
192
|
+
React.useEffect(() => {
|
|
193
|
+
if (tagsData && projectsData && tagTypesData) {
|
|
194
|
+
send({ type: 'FETCH' });
|
|
195
|
+
}
|
|
196
|
+
}, [send, tagsData, projectsData, tagTypesData]);
|
|
197
|
+
|
|
198
|
+
// Component render
|
|
199
|
+
return (
|
|
200
|
+
<div className="min-h-screen bg-background">
|
|
201
|
+
{/* Toast notifications */}
|
|
202
|
+
{toast.show && (
|
|
203
|
+
<div
|
|
204
|
+
className={`fixed top-4 right-4 px-4 py-2 rounded-themed shadow-themed-lg z-50
|
|
205
|
+
${
|
|
206
|
+
toast.type === 'success'
|
|
207
|
+
? 'bg-success text-success-foreground'
|
|
208
|
+
: 'bg-destructive text-destructive-foreground'
|
|
209
|
+
}`}
|
|
210
|
+
>
|
|
211
|
+
<span>{toast.message}</span>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{/* Header */}
|
|
216
|
+
<div className="flex justify-between items-center mb-6">
|
|
217
|
+
<h1 className="text-2xl font-bold text-foreground">Tags</h1>
|
|
218
|
+
<button onClick={() => send({ type: 'OPEN_CREATE_MODAL' })} className="themed-button focus-themed">
|
|
219
|
+
Create Tag
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{/* Search */}
|
|
224
|
+
<input
|
|
225
|
+
type="text"
|
|
226
|
+
placeholder="Search tags..."
|
|
227
|
+
value={searchQuery}
|
|
228
|
+
onChange={(e) => send({ type: 'SEARCH', query: e.target.value })}
|
|
229
|
+
className="themed-input w-full mb-4 focus-themed"
|
|
230
|
+
/>
|
|
231
|
+
|
|
232
|
+
{/* Tags list */}
|
|
233
|
+
{isLoading ? (
|
|
234
|
+
<div>Loading...</div>
|
|
235
|
+
) : (
|
|
236
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
237
|
+
{filteredTags.map((tag) => (
|
|
238
|
+
<div key={tag.id} className="themed-card p-4">
|
|
239
|
+
<h3 className="font-semibold">{tag.name}</h3>
|
|
240
|
+
<p className="text-sm text-muted-foreground">{tag.description}</p>
|
|
241
|
+
</div>
|
|
242
|
+
))}
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{/* Modal Form */}
|
|
247
|
+
<TagModal
|
|
248
|
+
isOpen={state.context.isCreateModalOpen || state.context.isEditModalOpen}
|
|
249
|
+
onClose={() => send({ type: 'CLOSE_CREATE_MODAL' })}
|
|
250
|
+
title={state.context.isEditModalOpen ? 'Edit Tag' : 'Create Tag'}
|
|
251
|
+
draftTag={state.context.draftTag}
|
|
252
|
+
projects={projects}
|
|
253
|
+
tagTypes={tagTypes}
|
|
254
|
+
send={send}
|
|
255
|
+
onSubmit={(e) => {
|
|
256
|
+
e.preventDefault();
|
|
257
|
+
if (state.context.isEditModalOpen) {
|
|
258
|
+
send({ type: 'UPDATE_TAG' });
|
|
259
|
+
} else {
|
|
260
|
+
send({ type: 'SUBMIT_TAG' });
|
|
261
|
+
}
|
|
262
|
+
}}
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### **Step 2: XState Machine for Form State Management**
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
// tagsMachine.ts - State machine with organization context
|
|
275
|
+
import { createMachine, assign } from 'xstate';
|
|
276
|
+
import type { ITagType } from 'common/server';
|
|
277
|
+
|
|
278
|
+
// ๐ Type definition with organization field
|
|
279
|
+
export type Tag = {
|
|
280
|
+
id: string;
|
|
281
|
+
tagId: string;
|
|
282
|
+
name: string;
|
|
283
|
+
description?: string;
|
|
284
|
+
createdAt: string;
|
|
285
|
+
updatedAt: string;
|
|
286
|
+
createdBy: string;
|
|
287
|
+
orgName: string; // Organization context
|
|
288
|
+
tagType: {
|
|
289
|
+
id: string;
|
|
290
|
+
name: string;
|
|
291
|
+
color: string;
|
|
292
|
+
};
|
|
293
|
+
color: string;
|
|
294
|
+
project: {
|
|
295
|
+
id: string;
|
|
296
|
+
name: string;
|
|
297
|
+
};
|
|
298
|
+
organization: {
|
|
299
|
+
id: string;
|
|
300
|
+
name: string;
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
type TagsContext = {
|
|
305
|
+
tags: Tag[];
|
|
306
|
+
projects: Project[];
|
|
307
|
+
tagTypes: ITagType[];
|
|
308
|
+
filteredTags: Tag[];
|
|
309
|
+
searchQuery: string;
|
|
310
|
+
isLoading: boolean;
|
|
311
|
+
error: string | null;
|
|
312
|
+
isCreateModalOpen: boolean;
|
|
313
|
+
isEditModalOpen: boolean;
|
|
314
|
+
draftTag: Partial<Tag> | null;
|
|
315
|
+
validationErrors?: Record<string, string>;
|
|
316
|
+
tagsLoaded: boolean;
|
|
317
|
+
projectsLoaded: boolean;
|
|
318
|
+
tagTypesLoaded: boolean;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
type TagsEvent =
|
|
322
|
+
| { type: 'FETCH' }
|
|
323
|
+
| { type: 'SEARCH'; query: string }
|
|
324
|
+
| { type: 'OPEN_CREATE_MODAL' }
|
|
325
|
+
| { type: 'CLOSE_CREATE_MODAL' }
|
|
326
|
+
| { type: 'OPEN_EDIT_MODAL'; tag: Tag }
|
|
327
|
+
| { type: 'UPDATE_DRAFT'; value: Partial<Tag> }
|
|
328
|
+
| { type: 'SUBMIT_TAG' }
|
|
329
|
+
| { type: 'UPDATE_TAG' }
|
|
330
|
+
| { type: 'DELETE_TAG'; id: string };
|
|
331
|
+
|
|
332
|
+
const defaultDraftTag: Partial<Tag> = {
|
|
333
|
+
name: '',
|
|
334
|
+
description: '',
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Helper function for search filtering
|
|
338
|
+
const filterTags = (tags: Tag[], query: string): Tag[] => {
|
|
339
|
+
if (!query) return tags;
|
|
340
|
+
const lowercaseQuery = query.toLowerCase();
|
|
341
|
+
return tags.filter(
|
|
342
|
+
(tag) =>
|
|
343
|
+
tag.name.toLowerCase().includes(lowercaseQuery) ||
|
|
344
|
+
(tag.description?.toLowerCase() || '').includes(lowercaseQuery),
|
|
345
|
+
);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
export const tagsMachine = createMachine({
|
|
349
|
+
id: 'tags',
|
|
350
|
+
types: {} as {
|
|
351
|
+
context: TagsContext;
|
|
352
|
+
events: TagsEvent;
|
|
353
|
+
},
|
|
354
|
+
initial: 'loading',
|
|
355
|
+
context: {
|
|
356
|
+
tags: [],
|
|
357
|
+
projects: [],
|
|
358
|
+
tagTypes: [],
|
|
359
|
+
filteredTags: [],
|
|
360
|
+
searchQuery: '',
|
|
361
|
+
isLoading: true,
|
|
362
|
+
error: null,
|
|
363
|
+
isCreateModalOpen: false,
|
|
364
|
+
isEditModalOpen: false,
|
|
365
|
+
draftTag: null,
|
|
366
|
+
validationErrors: {},
|
|
367
|
+
tagsLoaded: false,
|
|
368
|
+
projectsLoaded: false,
|
|
369
|
+
tagTypesLoaded: false,
|
|
370
|
+
},
|
|
371
|
+
states: {
|
|
372
|
+
loading: {
|
|
373
|
+
// Parallel invocations for fetching multiple data sources
|
|
374
|
+
invoke: [
|
|
375
|
+
{
|
|
376
|
+
src: 'fetchTags',
|
|
377
|
+
onDone: {
|
|
378
|
+
actions: assign({
|
|
379
|
+
tags: ({ event }) => event.output,
|
|
380
|
+
filteredTags: ({ event }) => event.output,
|
|
381
|
+
tagsLoaded: true,
|
|
382
|
+
}),
|
|
383
|
+
},
|
|
384
|
+
onError: {
|
|
385
|
+
target: 'error',
|
|
386
|
+
actions: assign({
|
|
387
|
+
error: ({ event }) => String(event.error),
|
|
388
|
+
isLoading: false,
|
|
389
|
+
}),
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
src: 'fetchProjects',
|
|
394
|
+
onDone: {
|
|
395
|
+
actions: assign({
|
|
396
|
+
projects: ({ event }) => event.output,
|
|
397
|
+
projectsLoaded: true,
|
|
398
|
+
}),
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
src: 'fetchTagTypes',
|
|
403
|
+
onDone: {
|
|
404
|
+
actions: assign({
|
|
405
|
+
tagTypes: ({ event }) => event.output,
|
|
406
|
+
tagTypesLoaded: true,
|
|
407
|
+
}),
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
// Transition to idle when all data is loaded
|
|
412
|
+
always: {
|
|
413
|
+
target: 'idle',
|
|
414
|
+
guard: ({ context }) => context.tagsLoaded && context.projectsLoaded && context.tagTypesLoaded,
|
|
415
|
+
actions: assign({ isLoading: false }),
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
idle: {
|
|
419
|
+
on: {
|
|
420
|
+
FETCH: {
|
|
421
|
+
target: 'loading',
|
|
422
|
+
actions: assign({
|
|
423
|
+
isLoading: true,
|
|
424
|
+
tagsLoaded: false,
|
|
425
|
+
projectsLoaded: false,
|
|
426
|
+
tagTypesLoaded: false,
|
|
427
|
+
}),
|
|
428
|
+
},
|
|
429
|
+
SEARCH: {
|
|
430
|
+
actions: assign({
|
|
431
|
+
searchQuery: ({ event }) => event.query || '',
|
|
432
|
+
filteredTags: ({ context, event }) => filterTags(context.tags, event.query),
|
|
433
|
+
}),
|
|
434
|
+
},
|
|
435
|
+
OPEN_CREATE_MODAL: {
|
|
436
|
+
actions: assign({
|
|
437
|
+
isCreateModalOpen: true,
|
|
438
|
+
draftTag: () => ({ ...defaultDraftTag }),
|
|
439
|
+
}),
|
|
440
|
+
},
|
|
441
|
+
CLOSE_CREATE_MODAL: {
|
|
442
|
+
actions: assign({
|
|
443
|
+
isCreateModalOpen: false,
|
|
444
|
+
isEditModalOpen: false,
|
|
445
|
+
draftTag: null,
|
|
446
|
+
}),
|
|
447
|
+
},
|
|
448
|
+
OPEN_EDIT_MODAL: {
|
|
449
|
+
actions: assign({
|
|
450
|
+
isEditModalOpen: true,
|
|
451
|
+
draftTag: ({ event }) => ({ ...event.tag }),
|
|
452
|
+
}),
|
|
453
|
+
},
|
|
454
|
+
UPDATE_DRAFT: {
|
|
455
|
+
actions: assign({
|
|
456
|
+
draftTag: ({ context, event }) => ({
|
|
457
|
+
...context.draftTag,
|
|
458
|
+
...event.value,
|
|
459
|
+
}),
|
|
460
|
+
}),
|
|
461
|
+
},
|
|
462
|
+
SUBMIT_TAG: {
|
|
463
|
+
target: 'submitting',
|
|
464
|
+
guard: ({ context }) => Boolean(context.draftTag?.name),
|
|
465
|
+
},
|
|
466
|
+
UPDATE_TAG: {
|
|
467
|
+
target: 'updating',
|
|
468
|
+
},
|
|
469
|
+
DELETE_TAG: {
|
|
470
|
+
target: 'deleting',
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
submitting: {
|
|
475
|
+
invoke: {
|
|
476
|
+
src: 'submitTag',
|
|
477
|
+
input: ({ context }) => ({ tag: context.draftTag! }),
|
|
478
|
+
onDone: {
|
|
479
|
+
target: 'idle',
|
|
480
|
+
actions: assign({
|
|
481
|
+
tags: ({ context, event }) => [...context.tags, event.output],
|
|
482
|
+
filteredTags: ({ context, event }) => {
|
|
483
|
+
const updatedTags = [...context.tags, event.output];
|
|
484
|
+
return filterTags(updatedTags, context.searchQuery);
|
|
485
|
+
},
|
|
486
|
+
isCreateModalOpen: false,
|
|
487
|
+
draftTag: null,
|
|
488
|
+
}),
|
|
489
|
+
},
|
|
490
|
+
onError: {
|
|
491
|
+
target: 'idle',
|
|
492
|
+
actions: assign({
|
|
493
|
+
error: ({ event }) => String(event.error),
|
|
494
|
+
}),
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
updating: {
|
|
499
|
+
invoke: {
|
|
500
|
+
src: 'updateTag',
|
|
501
|
+
input: ({ context }) => ({ tag: context.draftTag! }),
|
|
502
|
+
onDone: {
|
|
503
|
+
target: 'idle',
|
|
504
|
+
actions: assign({
|
|
505
|
+
tags: ({ context, event }) =>
|
|
506
|
+
context.tags.map((tag) => (tag.id === event.output.id ? event.output : tag)),
|
|
507
|
+
filteredTags: ({ context, event }) => {
|
|
508
|
+
const updatedTags = context.tags.map((tag) =>
|
|
509
|
+
tag.id === event.output.id ? event.output : tag,
|
|
510
|
+
);
|
|
511
|
+
return filterTags(updatedTags, context.searchQuery);
|
|
512
|
+
},
|
|
513
|
+
isEditModalOpen: false,
|
|
514
|
+
draftTag: null,
|
|
515
|
+
}),
|
|
516
|
+
},
|
|
517
|
+
onError: {
|
|
518
|
+
target: 'idle',
|
|
519
|
+
actions: assign({
|
|
520
|
+
error: ({ event }) => String(event.error),
|
|
521
|
+
}),
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
deleting: {
|
|
526
|
+
invoke: {
|
|
527
|
+
src: 'deleteTag',
|
|
528
|
+
input: ({ event }) => ({ id: (event as any).id }),
|
|
529
|
+
onDone: {
|
|
530
|
+
target: 'idle',
|
|
531
|
+
actions: assign({
|
|
532
|
+
tags: ({ context, event }) => context.tags.filter((tag) => tag.id !== event.output.id),
|
|
533
|
+
filteredTags: ({ context, event }) => {
|
|
534
|
+
const updatedTags = context.tags.filter((tag) => tag.id !== event.output.id);
|
|
535
|
+
return filterTags(updatedTags, context.searchQuery);
|
|
536
|
+
},
|
|
537
|
+
}),
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
error: {
|
|
542
|
+
on: {
|
|
543
|
+
FETCH: 'loading',
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
### **Step 3: Modal Form Component**
|
|
553
|
+
|
|
554
|
+
```tsx
|
|
555
|
+
// TagModal.tsx - Reusable form modal component
|
|
556
|
+
import React from 'react';
|
|
557
|
+
import type { ITagType } from 'common/server';
|
|
558
|
+
|
|
559
|
+
interface TagModalProps {
|
|
560
|
+
isOpen: boolean;
|
|
561
|
+
onClose: () => void;
|
|
562
|
+
title: string;
|
|
563
|
+
draftTag: any;
|
|
564
|
+
projects: any[];
|
|
565
|
+
tagTypes: ITagType[];
|
|
566
|
+
send: any;
|
|
567
|
+
onSubmit: (e: React.FormEvent) => void;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ๐ Extract modal outside parent to prevent re-renders
|
|
571
|
+
const TagModal: React.FC<TagModalProps> = ({
|
|
572
|
+
isOpen,
|
|
573
|
+
onClose,
|
|
574
|
+
title,
|
|
575
|
+
draftTag,
|
|
576
|
+
projects,
|
|
577
|
+
tagTypes,
|
|
578
|
+
send,
|
|
579
|
+
onSubmit,
|
|
580
|
+
}) => {
|
|
581
|
+
if (!isOpen || !draftTag) return null;
|
|
582
|
+
|
|
583
|
+
return (
|
|
584
|
+
<div className="fixed inset-0 z-10 overflow-y-auto">
|
|
585
|
+
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
586
|
+
{/* Backdrop */}
|
|
587
|
+
<div
|
|
588
|
+
className="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity"
|
|
589
|
+
onClick={onClose}
|
|
590
|
+
aria-hidden="true"
|
|
591
|
+
/>
|
|
592
|
+
|
|
593
|
+
{/* Center modal */}
|
|
594
|
+
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
|
595
|
+
​
|
|
596
|
+
</span>
|
|
597
|
+
|
|
598
|
+
{/* Modal panel */}
|
|
599
|
+
<div className="themed-card inline-block align-bottom rounded-themed-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-themed-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
|
|
600
|
+
<div className="sm:flex sm:items-start">
|
|
601
|
+
<div className="mt-3 text-center sm:mt-0 sm:text-left w-full">
|
|
602
|
+
<h3 className="text-lg leading-6 font-medium text-foreground">{title}</h3>
|
|
603
|
+
|
|
604
|
+
<form onSubmit={onSubmit} className="mt-6 space-y-4">
|
|
605
|
+
{/* Name Field */}
|
|
606
|
+
<div>
|
|
607
|
+
<label htmlFor="name" className="block text-sm font-medium text-foreground">
|
|
608
|
+
Name <span className="text-destructive">*</span>
|
|
609
|
+
</label>
|
|
610
|
+
<input
|
|
611
|
+
type="text"
|
|
612
|
+
id="name"
|
|
613
|
+
value={draftTag.name || ''}
|
|
614
|
+
onChange={(e) =>
|
|
615
|
+
send({
|
|
616
|
+
type: 'UPDATE_DRAFT',
|
|
617
|
+
value: { name: e.target.value },
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
className="themed-input mt-1 block w-full rounded-themed focus-themed"
|
|
621
|
+
required
|
|
622
|
+
autoFocus
|
|
623
|
+
aria-required="true"
|
|
624
|
+
/>
|
|
625
|
+
</div>
|
|
626
|
+
|
|
627
|
+
{/* Project Selection */}
|
|
628
|
+
<div>
|
|
629
|
+
<label htmlFor="project" className="block text-sm font-medium text-foreground">
|
|
630
|
+
Project <span className="text-destructive">*</span>
|
|
631
|
+
</label>
|
|
632
|
+
<select
|
|
633
|
+
id="project"
|
|
634
|
+
value={draftTag.project?.id || ''}
|
|
635
|
+
required
|
|
636
|
+
onChange={(e) => {
|
|
637
|
+
const selectedProject = projects.find((p) => p.id === e.target.value);
|
|
638
|
+
send({
|
|
639
|
+
type: 'UPDATE_DRAFT',
|
|
640
|
+
value: {
|
|
641
|
+
project: selectedProject
|
|
642
|
+
? {
|
|
643
|
+
id: selectedProject.id,
|
|
644
|
+
name: selectedProject.name,
|
|
645
|
+
}
|
|
646
|
+
: null,
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
}}
|
|
650
|
+
className="themed-input mt-1 block w-full rounded-themed focus-themed"
|
|
651
|
+
aria-required="true"
|
|
652
|
+
>
|
|
653
|
+
<option value="">Select a project</option>
|
|
654
|
+
{projects.map((project) => (
|
|
655
|
+
<option key={project.id} value={project.id}>
|
|
656
|
+
{project.name}
|
|
657
|
+
</option>
|
|
658
|
+
))}
|
|
659
|
+
</select>
|
|
660
|
+
</div>
|
|
661
|
+
|
|
662
|
+
{/* Tag Type Selection */}
|
|
663
|
+
<div>
|
|
664
|
+
<label htmlFor="tagType" className="block text-sm font-medium text-foreground">
|
|
665
|
+
Tag Type <span className="text-destructive">*</span>
|
|
666
|
+
</label>
|
|
667
|
+
<select
|
|
668
|
+
id="tagType"
|
|
669
|
+
value={draftTag.tagType?.id || ''}
|
|
670
|
+
onChange={(e) => {
|
|
671
|
+
const selectedTagType = tagTypes.find((tt) => tt.id === e.target.value);
|
|
672
|
+
send({
|
|
673
|
+
type: 'UPDATE_DRAFT',
|
|
674
|
+
value: {
|
|
675
|
+
tagType: selectedTagType
|
|
676
|
+
? {
|
|
677
|
+
id: selectedTagType.id,
|
|
678
|
+
name: selectedTagType.name,
|
|
679
|
+
color: selectedTagType.color,
|
|
680
|
+
}
|
|
681
|
+
: null,
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
}}
|
|
685
|
+
className="themed-input mt-1 block w-full rounded-themed focus-themed"
|
|
686
|
+
required
|
|
687
|
+
aria-required="true"
|
|
688
|
+
>
|
|
689
|
+
<option value="">Select a tag type</option>
|
|
690
|
+
{tagTypes.map((tagType) => (
|
|
691
|
+
<option key={tagType.id} value={tagType.id}>
|
|
692
|
+
{tagType.name}
|
|
693
|
+
</option>
|
|
694
|
+
))}
|
|
695
|
+
</select>
|
|
696
|
+
</div>
|
|
697
|
+
|
|
698
|
+
{/* Description Field */}
|
|
699
|
+
<div>
|
|
700
|
+
<label htmlFor="description" className="block text-sm font-medium text-foreground">
|
|
701
|
+
Description <span className="text-muted-foreground">(Optional)</span>
|
|
702
|
+
</label>
|
|
703
|
+
<textarea
|
|
704
|
+
id="description"
|
|
705
|
+
value={draftTag.description || ''}
|
|
706
|
+
onChange={(e) =>
|
|
707
|
+
send({
|
|
708
|
+
type: 'UPDATE_DRAFT',
|
|
709
|
+
value: { description: e.target.value },
|
|
710
|
+
})
|
|
711
|
+
}
|
|
712
|
+
rows={3}
|
|
713
|
+
className="themed-input mt-1 block w-full rounded-themed focus-themed"
|
|
714
|
+
placeholder="Enter a description for this tag..."
|
|
715
|
+
/>
|
|
716
|
+
</div>
|
|
717
|
+
|
|
718
|
+
{/* Action Buttons */}
|
|
719
|
+
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
|
720
|
+
<button
|
|
721
|
+
type="submit"
|
|
722
|
+
className="themed-button w-full sm:ml-3 sm:w-auto focus-themed"
|
|
723
|
+
>
|
|
724
|
+
{draftTag.id ? 'Update Tag' : 'Create Tag'}
|
|
725
|
+
</button>
|
|
726
|
+
<button
|
|
727
|
+
type="button"
|
|
728
|
+
onClick={onClose}
|
|
729
|
+
className="bg-secondary text-secondary-foreground w-full sm:w-auto mt-3 sm:mt-0 px-4 py-2 rounded-themed hover:bg-secondary/80 transition-colors focus-themed"
|
|
730
|
+
>
|
|
731
|
+
Cancel
|
|
732
|
+
</button>
|
|
733
|
+
</div>
|
|
734
|
+
</form>
|
|
735
|
+
</div>
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
);
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
export default TagModal;
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
---
|
|
747
|
+
|
|
748
|
+
### **Step 4: GraphQL Query Patterns**
|
|
749
|
+
|
|
750
|
+
```typescript
|
|
751
|
+
// Queries with organization context
|
|
752
|
+
const { data, loading, error } = useGetTagsQuery({
|
|
753
|
+
variables: { orgName }, // โ
Pass organization context
|
|
754
|
+
errorPolicy: 'all', // โ
Handle partial data gracefully
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// Mutations - organization context from backend @addAccountContext
|
|
758
|
+
const [createTag] = useCreateTagMutation();
|
|
759
|
+
const response = await createTag({
|
|
760
|
+
variables: {
|
|
761
|
+
request: {
|
|
762
|
+
projectId: tag.project.id,
|
|
763
|
+
tagTypeId: tag.tagType.id,
|
|
764
|
+
name: tag.name,
|
|
765
|
+
description: tag.description,
|
|
766
|
+
},
|
|
767
|
+
// โ
NO orgName/orgId needed - backend gets it from userContext
|
|
768
|
+
},
|
|
769
|
+
// โ
Cache update after mutation
|
|
770
|
+
update: (cache, { data, errors }) => {
|
|
771
|
+
if (data?.createTag) {
|
|
772
|
+
// Optimistically update cache
|
|
773
|
+
cache.modify({
|
|
774
|
+
fields: {
|
|
775
|
+
getTags(existingTags = { data: [] }) {
|
|
776
|
+
return {
|
|
777
|
+
...existingTags,
|
|
778
|
+
data: [...existingTags.data, data.createTag],
|
|
779
|
+
};
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
## ๐ฏ Frontend Implementation Checklist
|
|
791
|
+
|
|
792
|
+
### **React Component Setup**
|
|
793
|
+
|
|
794
|
+
- [ ] Extract `orgName` from `useParams<{ orgName: string }>()`
|
|
795
|
+
- [ ] Pass `orgName` to all GraphQL queries requiring organization context
|
|
796
|
+
- [ ] Use `errorPolicy: 'all'` for partial data handling
|
|
797
|
+
- [ ] Implement loading and error states
|
|
798
|
+
- [ ] Add toast notifications for user feedback
|
|
799
|
+
- [ ] Import hooks from `common/graphql` (not local files)
|
|
800
|
+
|
|
801
|
+
### **XState Machine**
|
|
802
|
+
|
|
803
|
+
- [ ] Define comprehensive context type with all form fields
|
|
804
|
+
- [ ] Include organization-related fields in entity types
|
|
805
|
+
- [ ] Create states: `loading`, `idle`, `submitting`, `updating`, `deleting`, `error`
|
|
806
|
+
- [ ] Implement proper state transitions with guards
|
|
807
|
+
- [ ] Use `fromPromise` for async operations (fetch/submit/update/delete actors)
|
|
808
|
+
- [ ] Handle validation in guards or actors
|
|
809
|
+
- [ ] Use `assign` for context updates
|
|
810
|
+
- [ ] Implement search/filter logic in event handlers
|
|
811
|
+
|
|
812
|
+
### **Form Modal**
|
|
813
|
+
|
|
814
|
+
- [ ] Extract modal component outside parent to prevent re-renders
|
|
815
|
+
- [ ] Implement controlled inputs with XState `send` function
|
|
816
|
+
- [ ] Add proper validation with error messages
|
|
817
|
+
- [ ] Use `send({ type: 'UPDATE_DRAFT', value })` for field updates
|
|
818
|
+
- [ ] Handle form submission with `send({ type: 'SUBMIT_TAG' })`
|
|
819
|
+
- [ ] Add cancel/close functionality
|
|
820
|
+
- [ ] Add accessibility attributes (`aria-required`, `aria-label`)
|
|
821
|
+
- [ ] Auto-focus first input field
|
|
822
|
+
- [ ] Show loading state on submit button
|
|
823
|
+
|
|
824
|
+
### **GraphQL Integration**
|
|
825
|
+
|
|
826
|
+
- [ ] Import generated hooks from `common/graphql`
|
|
827
|
+
- [ ] Pass organization variables (`orgName`) to queries
|
|
828
|
+
- [ ] Handle mutation responses with optimistic updates
|
|
829
|
+
- [ ] Implement cache updates after mutations using `update` callback
|
|
830
|
+
- [ ] Refetch queries after successful mutations if needed
|
|
831
|
+
- [ ] Use `errorPolicy: 'all'` to handle partial data
|
|
832
|
+
- [ ] Filter out null/invalid entries from response data
|
|
833
|
+
|
|
834
|
+
### **Error Handling**
|
|
835
|
+
|
|
836
|
+
- [ ] Use `errorPolicy: 'all'` for queries
|
|
837
|
+
- [ ] Filter null/invalid data entries with `.filter(item => item && item.id)`
|
|
838
|
+
- [ ] Display partial data warnings to users
|
|
839
|
+
- [ ] Show user-friendly error messages in toast notifications
|
|
840
|
+
- [ ] Log detailed errors to console for debugging
|
|
841
|
+
- [ ] Handle network errors gracefully
|
|
842
|
+
- [ ] Validate required fields before submission
|
|
843
|
+
|
|
844
|
+
### **User Experience**
|
|
845
|
+
|
|
846
|
+
- [ ] Add loading spinners during data fetch
|
|
847
|
+
- [ ] Show toast notifications for success/error/info
|
|
848
|
+
- [ ] Auto-hide toasts after 3-4 seconds
|
|
849
|
+
- [ ] Disable system/default entities from editing/deletion
|
|
850
|
+
- [ ] Validate required fields before submission
|
|
851
|
+
- [ ] Show validation errors inline
|
|
852
|
+
- [ ] Add empty state messages when no data
|
|
853
|
+
- [ ] Implement search/filter functionality
|
|
854
|
+
- [ ] Add keyboard shortcuts (Escape to close modal)
|
|
855
|
+
- [ ] Make modals responsive for mobile
|
|
856
|
+
|
|
857
|
+
---
|
|
858
|
+
|
|
859
|
+
## ๐จ Common Frontend Mistakes
|
|
860
|
+
|
|
861
|
+
### โ Mistake 1: Missing Organization Context in Queries
|
|
862
|
+
|
|
863
|
+
```tsx
|
|
864
|
+
// โ WRONG - No organization context
|
|
865
|
+
const { data } = useGetTagsQuery();
|
|
866
|
+
|
|
867
|
+
// Result: Query returns data for wrong organization or fails
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
```tsx
|
|
871
|
+
// โ
CORRECT - Organization from route params
|
|
872
|
+
const { orgName } = useParams<{ orgName: string }>();
|
|
873
|
+
const { data } = useGetTagsQuery({
|
|
874
|
+
variables: { orgName },
|
|
875
|
+
});
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
**Why**: Backend uses `@addAccountContext` directive which requires `orgName` to resolve organization context.
|
|
879
|
+
|
|
880
|
+
---
|
|
881
|
+
|
|
882
|
+
### โ Mistake 2: Not Handling Partial Data
|
|
883
|
+
|
|
884
|
+
```tsx
|
|
885
|
+
// โ WRONG - Will crash on partial errors
|
|
886
|
+
const tags = data.getTags.data;
|
|
887
|
+
tags.map((tag) => <div>{tag.name}</div>);
|
|
888
|
+
|
|
889
|
+
// Result: TypeError: Cannot read property 'data' of undefined
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
```tsx
|
|
893
|
+
// โ
CORRECT - Filter null entries and handle errors
|
|
894
|
+
const tags = (data?.getTags?.data || []).filter((tag) => tag && tag.id);
|
|
895
|
+
tags.map((tag) => <div>{tag.name}</div>);
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
**Why**: GraphQL can return partial data when some fields fail. Always use optional chaining and filter invalid entries.
|
|
899
|
+
|
|
900
|
+
---
|
|
901
|
+
|
|
902
|
+
### โ Mistake 3: Creating Modal Inside Parent Component
|
|
903
|
+
|
|
904
|
+
```tsx
|
|
905
|
+
// โ WRONG - Modal recreated on every render
|
|
906
|
+
export function TagsView() {
|
|
907
|
+
const TagModal = () => {
|
|
908
|
+
return <div>Modal content</div>;
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
return (
|
|
912
|
+
<div>
|
|
913
|
+
<TagModal />
|
|
914
|
+
</div>
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Result: Performance issues, state loss on re-render
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
```tsx
|
|
922
|
+
// โ
CORRECT - Modal outside parent component
|
|
923
|
+
const TagModal = (props) => {
|
|
924
|
+
return <div>Modal content</div>;
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
export function TagsView() {
|
|
928
|
+
return (
|
|
929
|
+
<div>
|
|
930
|
+
<TagModal {...props} />
|
|
931
|
+
</div>
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
**Why**: Defining components inside other components causes them to remount on every render, losing internal state.
|
|
937
|
+
|
|
938
|
+
---
|
|
939
|
+
|
|
940
|
+
### โ Mistake 4: Not Refetching After Mutations
|
|
941
|
+
|
|
942
|
+
```tsx
|
|
943
|
+
// โ WRONG - Stale data after mutation
|
|
944
|
+
await createTag({ variables: { request } });
|
|
945
|
+
// List not updated!
|
|
946
|
+
|
|
947
|
+
// Result: User doesn't see new item until page refresh
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
```tsx
|
|
951
|
+
// โ
CORRECT - Refetch or update cache
|
|
952
|
+
await createTag({
|
|
953
|
+
variables: { request },
|
|
954
|
+
update: (cache, { data }) => {
|
|
955
|
+
// Update Apollo cache
|
|
956
|
+
cache.modify({
|
|
957
|
+
fields: {
|
|
958
|
+
getTags(existing) {
|
|
959
|
+
return {
|
|
960
|
+
...existing,
|
|
961
|
+
data: [...existing.data, data.createTag],
|
|
962
|
+
};
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
},
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
// OR simply refetch
|
|
970
|
+
tagsRefetch();
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
**Why**: Apollo cache doesn't automatically update after mutations. Must manually update or refetch.
|
|
974
|
+
|
|
975
|
+
---
|
|
976
|
+
|
|
977
|
+
### โ Mistake 5: Missing Error Policy
|
|
978
|
+
|
|
979
|
+
```tsx
|
|
980
|
+
// โ WRONG - Entire query fails on any error
|
|
981
|
+
const { data } = useGetTagsQuery({
|
|
982
|
+
variables: { orgName },
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
// Result: No data shown even if some tags loaded successfully
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
```tsx
|
|
989
|
+
// โ
CORRECT - Allow partial data with errors
|
|
990
|
+
const { data, error } = useGetTagsQuery({
|
|
991
|
+
variables: { orgName },
|
|
992
|
+
errorPolicy: 'all', // Return partial data even with errors
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
if (error) {
|
|
996
|
+
console.error('Partial error:', error);
|
|
997
|
+
// Still render available data
|
|
998
|
+
}
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
**Why**: Some queries may fail partially (e.g., organization field resolution fails for one item). `errorPolicy: 'all'` allows rendering valid data.
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
### โ Mistake 6: Not Passing orgId in Mutations
|
|
1006
|
+
|
|
1007
|
+
```tsx
|
|
1008
|
+
// โ WRONG - Passing orgId/orgName in mutation
|
|
1009
|
+
await createTag({
|
|
1010
|
+
variables: {
|
|
1011
|
+
request: {
|
|
1012
|
+
organizationId: orgId, // โ Not needed!
|
|
1013
|
+
name: tag.name,
|
|
1014
|
+
},
|
|
1015
|
+
},
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// Result: Backend ignores this - gets orgId from userContext anyway
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
```tsx
|
|
1022
|
+
// โ
CORRECT - No organization in mutation variables
|
|
1023
|
+
await createTag({
|
|
1024
|
+
variables: {
|
|
1025
|
+
request: {
|
|
1026
|
+
// โ
Backend gets orgId from @addAccountContext directive
|
|
1027
|
+
name: tag.name,
|
|
1028
|
+
projectId: tag.project.id,
|
|
1029
|
+
},
|
|
1030
|
+
},
|
|
1031
|
+
});
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
**Why**: Backend `@addAccountContext` directive automatically injects organization context into `userContext`. Service layer uses `userContext.orgId`.
|
|
1035
|
+
|
|
1036
|
+
---
|
|
1037
|
+
|
|
1038
|
+
### โ Mistake 7: Not Validating Before Submission
|
|
1039
|
+
|
|
1040
|
+
```tsx
|
|
1041
|
+
// โ WRONG - No validation
|
|
1042
|
+
const submitTag = async () => {
|
|
1043
|
+
await createTag({ variables: { request: draftTag } });
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
// Result: Backend errors, poor UX
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
```tsx
|
|
1050
|
+
// โ
CORRECT - Validate in actor or guard
|
|
1051
|
+
submitTag: fromPromise(async ({ input }) => {
|
|
1052
|
+
const { tag } = input;
|
|
1053
|
+
|
|
1054
|
+
// Client-side validation
|
|
1055
|
+
if (!tag.name?.trim()) {
|
|
1056
|
+
throw new Error('Name is required');
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (!tag.project?.id) {
|
|
1060
|
+
throw new Error('Project must be selected');
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
return await createTag({ variables: { request: tag } });
|
|
1064
|
+
}),
|
|
1065
|
+
|
|
1066
|
+
// In machine
|
|
1067
|
+
SUBMIT_TAG: {
|
|
1068
|
+
target: 'submitting',
|
|
1069
|
+
guard: ({ context }) => {
|
|
1070
|
+
// Guard prevents transition if validation fails
|
|
1071
|
+
return Boolean(context.draftTag?.name?.trim());
|
|
1072
|
+
},
|
|
1073
|
+
},
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
**Why**: Client-side validation provides immediate feedback and prevents unnecessary network requests.
|
|
1077
|
+
|
|
1078
|
+
---
|
|
1079
|
+
|
|
1080
|
+
### โ Mistake 8: Importing from Local Instead of common/graphql
|
|
1081
|
+
|
|
1082
|
+
```tsx
|
|
1083
|
+
// โ WRONG - Local import
|
|
1084
|
+
import { useGetTagsQuery } from './queries/generated';
|
|
1085
|
+
|
|
1086
|
+
// Result: May work but defeats code generation purpose
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
```tsx
|
|
1090
|
+
// โ
CORRECT - Import from common/graphql
|
|
1091
|
+
import { useGetTagsQuery } from 'common/graphql';
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
**Why**: Code generation creates centralized types/hooks in `common/graphql` for consistency across all packages.
|
|
1095
|
+
|
|
1096
|
+
---
|
|
1097
|
+
|
|
1098
|
+
## ๐ Complete Data Flow: Frontend to Backend
|
|
1099
|
+
|
|
1100
|
+
```
|
|
1101
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1102
|
+
โ Frontend โ Backend Flow โ
|
|
1103
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1104
|
+
|
|
1105
|
+
1. USER NAVIGATES TO ROUTE
|
|
1106
|
+
URL: /org/acme-corp/projects/tags
|
|
1107
|
+
Route: <Route path="/:orgName/projects/tags" />
|
|
1108
|
+
|
|
1109
|
+
2. REACT COMPONENT MOUNTS
|
|
1110
|
+
const { orgName } = useParams(); // "acme-corp"
|
|
1111
|
+
|
|
1112
|
+
3. GRAPHQL QUERY EXECUTED
|
|
1113
|
+
useGetTagsQuery({
|
|
1114
|
+
variables: { orgName: "acme-corp" }
|
|
1115
|
+
})
|
|
1116
|
+
โ
|
|
1117
|
+
Apollo Client sends to GraphQL server
|
|
1118
|
+
|
|
1119
|
+
4. BACKEND RECEIVES QUERY
|
|
1120
|
+
Query getTags(orgName: "acme-corp")
|
|
1121
|
+
โ
|
|
1122
|
+
@addAccountContext directive intercepts
|
|
1123
|
+
โ
|
|
1124
|
+
Resolves: orgName โ orgId via OrganizationService.getBySlug()
|
|
1125
|
+
โ
|
|
1126
|
+
Injects userContext.orgId = "507f1f77bcf86cd799439011"
|
|
1127
|
+
|
|
1128
|
+
5. RESOLVER LAYER
|
|
1129
|
+
getTags: (_, args, { tagService, userContext }) =>
|
|
1130
|
+
tagService.getTags(userContext.orgId, args.projectId, ...)
|
|
1131
|
+
โ
|
|
1132
|
+
Resolver uses userContext.orgId (NOT args.orgName)
|
|
1133
|
+
|
|
1134
|
+
6. SERVICE LAYER
|
|
1135
|
+
getTags(orgId: string, projectId?: string, ...) {
|
|
1136
|
+
// orgId already resolved - no slug service needed
|
|
1137
|
+
const match = {
|
|
1138
|
+
organization: new mongoose.Types.ObjectId(orgId),
|
|
1139
|
+
...(projectId && { project: new mongoose.Types.ObjectId(projectId) })
|
|
1140
|
+
};
|
|
1141
|
+
return tagRepository.getAll({ criteria: match });
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
7. REPOSITORY LAYER
|
|
1145
|
+
getAll(criteria: { organization: ObjectId }) {
|
|
1146
|
+
return TagModel.find(criteria)
|
|
1147
|
+
.populate('organization')
|
|
1148
|
+
.populate('project')
|
|
1149
|
+
.populate('tagType');
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
8. DATABASE QUERY
|
|
1153
|
+
db.tags.find({
|
|
1154
|
+
organization: ObjectId("507f1f77bcf86cd799439011")
|
|
1155
|
+
})
|
|
1156
|
+
|
|
1157
|
+
9. RESPONSE TO FRONTEND
|
|
1158
|
+
{
|
|
1159
|
+
getTags: {
|
|
1160
|
+
totalCount: 5,
|
|
1161
|
+
data: [
|
|
1162
|
+
{
|
|
1163
|
+
id: "tag1",
|
|
1164
|
+
name: "Bug",
|
|
1165
|
+
organization: {
|
|
1166
|
+
id: "507f1f77bcf86cd799439011",
|
|
1167
|
+
name: "ACME Corp"
|
|
1168
|
+
},
|
|
1169
|
+
project: { id: "proj1", name: "Project A" },
|
|
1170
|
+
tagType: { id: "type1", name: "Bug", color: "#ff0000" }
|
|
1171
|
+
},
|
|
1172
|
+
// ... more tags
|
|
1173
|
+
]
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
10. FRONTEND STATE UPDATE
|
|
1178
|
+
XState machine:
|
|
1179
|
+
- fetchTags actor receives response
|
|
1180
|
+
- Filters null entries: .filter(tag => tag && tag.id)
|
|
1181
|
+
- Updates context: assign({ tags: validTags })
|
|
1182
|
+
โ
|
|
1183
|
+
React re-renders with new data
|
|
1184
|
+
โ
|
|
1185
|
+
UI displays tag list
|
|
1186
|
+
|
|
1187
|
+
11. USER CREATES TAG
|
|
1188
|
+
Form submission โ send({ type: 'SUBMIT_TAG' })
|
|
1189
|
+
โ
|
|
1190
|
+
Machine transitions to 'submitting' state
|
|
1191
|
+
โ
|
|
1192
|
+
submitTag actor invoked:
|
|
1193
|
+
|
|
1194
|
+
useCreateTagMutation({
|
|
1195
|
+
variables: {
|
|
1196
|
+
request: {
|
|
1197
|
+
projectId: "proj1",
|
|
1198
|
+
tagTypeId: "type1",
|
|
1199
|
+
name: "New Bug Tag"
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
})
|
|
1203
|
+
โ
|
|
1204
|
+
Backend @addAccountContext gets orgId from request headers
|
|
1205
|
+
โ
|
|
1206
|
+
Service creates tag with organization ObjectId
|
|
1207
|
+
โ
|
|
1208
|
+
Response with new tag
|
|
1209
|
+
โ
|
|
1210
|
+
Apollo cache updated via 'update' callback
|
|
1211
|
+
โ
|
|
1212
|
+
Query refetched: tagsRefetch()
|
|
1213
|
+
โ
|
|
1214
|
+
Machine transitions to 'idle'
|
|
1215
|
+
โ
|
|
1216
|
+
UI shows success toast
|
|
1217
|
+
โ
|
|
1218
|
+
Modal closes
|
|
1219
|
+
```
|
|
1220
|
+
|
|
1221
|
+
---
|
|
1222
|
+
|
|
1223
|
+
## ๐จ Styling Best Practices
|
|
1224
|
+
|
|
1225
|
+
### **Use Themed Classes**
|
|
1226
|
+
|
|
1227
|
+
The AdminIDE stack uses theme-aware CSS classes that adapt to light/dark modes:
|
|
1228
|
+
|
|
1229
|
+
```tsx
|
|
1230
|
+
// Input fields
|
|
1231
|
+
<input className="themed-input focus-themed" />
|
|
1232
|
+
|
|
1233
|
+
// Buttons
|
|
1234
|
+
<button className="themed-button focus-themed">Submit</button>
|
|
1235
|
+
|
|
1236
|
+
// Cards/Panels
|
|
1237
|
+
<div className="themed-card shadow-themed-lg" />
|
|
1238
|
+
|
|
1239
|
+
// Success/Error states
|
|
1240
|
+
<div className="bg-success text-success-foreground">Success!</div>
|
|
1241
|
+
<div className="bg-destructive text-destructive-foreground">Error!</div>
|
|
1242
|
+
|
|
1243
|
+
// Text colors
|
|
1244
|
+
<p className="text-foreground">Primary text</p>
|
|
1245
|
+
<p className="text-muted-foreground">Secondary text</p>
|
|
1246
|
+
|
|
1247
|
+
// Background colors
|
|
1248
|
+
<div className="bg-background">Main background</div>
|
|
1249
|
+
<div className="bg-card">Card background</div>
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1252
|
+
### **Responsive Design**
|
|
1253
|
+
|
|
1254
|
+
```tsx
|
|
1255
|
+
// Mobile-first approach
|
|
1256
|
+
<div className="flex flex-col sm:flex-row">
|
|
1257
|
+
<button className="w-full sm:w-auto sm:ml-3">Submit</button>
|
|
1258
|
+
</div>
|
|
1259
|
+
|
|
1260
|
+
// Grid layouts
|
|
1261
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
1262
|
+
{items.map(item => <Card key={item.id} />)}
|
|
1263
|
+
</div>
|
|
1264
|
+
|
|
1265
|
+
// Responsive padding
|
|
1266
|
+
<div className="px-4 sm:px-6 lg:px-8">
|
|
1267
|
+
Content
|
|
1268
|
+
</div>
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
### **Accessibility**
|
|
1272
|
+
|
|
1273
|
+
```tsx
|
|
1274
|
+
// Labels and inputs
|
|
1275
|
+
<label htmlFor="name" className="block text-sm font-medium">
|
|
1276
|
+
Name <span className="text-destructive">*</span>
|
|
1277
|
+
</label>
|
|
1278
|
+
<input
|
|
1279
|
+
id="name"
|
|
1280
|
+
aria-required="true"
|
|
1281
|
+
aria-invalid={!!errors.name}
|
|
1282
|
+
aria-describedby={errors.name ? "name-error" : undefined}
|
|
1283
|
+
autoFocus
|
|
1284
|
+
/>
|
|
1285
|
+
{errors.name && (
|
|
1286
|
+
<p id="name-error" className="text-destructive text-sm mt-1">
|
|
1287
|
+
{errors.name}
|
|
1288
|
+
</p>
|
|
1289
|
+
)}
|
|
1290
|
+
|
|
1291
|
+
// Buttons
|
|
1292
|
+
<button
|
|
1293
|
+
aria-label="Close modal"
|
|
1294
|
+
onClick={onClose}
|
|
1295
|
+
>
|
|
1296
|
+
<XIcon aria-hidden="true" />
|
|
1297
|
+
</button>
|
|
1298
|
+
|
|
1299
|
+
// Loading states
|
|
1300
|
+
<button disabled={isLoading} aria-busy={isLoading}>
|
|
1301
|
+
{isLoading ? 'Saving...' : 'Save'}
|
|
1302
|
+
</button>
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
---
|
|
1306
|
+
|
|
1307
|
+
## ๐งช Testing Checklist
|
|
1308
|
+
|
|
1309
|
+
### **Query Testing**
|
|
1310
|
+
|
|
1311
|
+
- [ ] Test with valid organization context (`orgName` in params)
|
|
1312
|
+
- [ ] Test without organization context (should error or redirect)
|
|
1313
|
+
- [ ] Test partial data scenarios (some items fail to load)
|
|
1314
|
+
- [ ] Test network errors (offline, timeout)
|
|
1315
|
+
- [ ] Test empty results (no tags for organization)
|
|
1316
|
+
- [ ] Test filtering/search with various inputs
|
|
1317
|
+
- [ ] Test loading states displayed correctly
|
|
1318
|
+
|
|
1319
|
+
### **Mutation Testing**
|
|
1320
|
+
|
|
1321
|
+
- [ ] Test successful creation with valid data
|
|
1322
|
+
- [ ] Test successful update with changed fields
|
|
1323
|
+
- [ ] Test successful deletion
|
|
1324
|
+
- [ ] Test validation errors (missing required fields)
|
|
1325
|
+
- [ ] Test backend errors (duplicate names, constraints)
|
|
1326
|
+
- [ ] Test network errors during submission
|
|
1327
|
+
- [ ] Test cache updates after mutations
|
|
1328
|
+
- [ ] Test refetch after mutations
|
|
1329
|
+
|
|
1330
|
+
### **State Machine Testing**
|
|
1331
|
+
|
|
1332
|
+
- [ ] Test all state transitions (loading โ idle โ submitting โ idle)
|
|
1333
|
+
- [ ] Test event handlers (FETCH, SEARCH, SUBMIT_TAG, etc.)
|
|
1334
|
+
- [ ] Test guards (SUBMIT_TAG blocked if name empty)
|
|
1335
|
+
- [ ] Test context updates (assign actions work correctly)
|
|
1336
|
+
- [ ] Test error states and recovery
|
|
1337
|
+
- [ ] Test parallel invocations (fetchTags, fetchProjects, fetchTagTypes)
|
|
1338
|
+
|
|
1339
|
+
### **UI/UX Testing**
|
|
1340
|
+
|
|
1341
|
+
- [ ] Test toast notifications appear and disappear
|
|
1342
|
+
- [ ] Test modal open/close animations
|
|
1343
|
+
- [ ] Test form field updates reflect in state
|
|
1344
|
+
- [ ] Test keyboard navigation (Tab, Enter, Escape)
|
|
1345
|
+
- [ ] Test responsive layout on mobile/tablet/desktop
|
|
1346
|
+
- [ ] Test accessibility with screen reader
|
|
1347
|
+
- [ ] Test dark/light theme switching
|
|
1348
|
+
|
|
1349
|
+
### **Integration Testing**
|
|
1350
|
+
|
|
1351
|
+
- [ ] Test complete flow: load โ create โ update โ delete
|
|
1352
|
+
- [ ] Test with multiple browser tabs (concurrent edits)
|
|
1353
|
+
- [ ] Test with slow network (loading states)
|
|
1354
|
+
- [ ] Test with backend errors (500, 404, validation)
|
|
1355
|
+
- [ ] Test authentication errors (token expired)
|
|
1356
|
+
- [ ] Test permission errors (unauthorized actions)
|
|
1357
|
+
|
|
1358
|
+
---
|
|
1359
|
+
|
|
1360
|
+
## ๐ Key Takeaways
|
|
1361
|
+
|
|
1362
|
+
1. โ
**Always extract `orgName` from route params** - Required for organization-scoped queries
|
|
1363
|
+
2. โ
**Pass `orgName` to all organization-scoped queries** - Backend needs it for context resolution
|
|
1364
|
+
3. โ
**Backend handles organization resolution via `@addAccountContext`** - Don't pass orgId in mutations
|
|
1365
|
+
4. โ
**Use XState for complex form workflows** - Better state management than useState
|
|
1366
|
+
5. โ
**Handle partial data with `errorPolicy: 'all'`** - Gracefully handle field resolution errors
|
|
1367
|
+
6. โ
**Update cache or refetch after mutations** - Keep UI in sync with backend
|
|
1368
|
+
7. โ
**Provide user feedback with toast notifications** - Confirm actions and show errors
|
|
1369
|
+
8. โ
**Extract modals to prevent unnecessary re-renders** - Define outside parent component
|
|
1370
|
+
9. โ
**Validate data before submission** - Better UX and fewer backend errors
|
|
1371
|
+
10. โ
**Filter null/invalid entries from GraphQL responses** - Prevent runtime errors
|
|
1372
|
+
|
|
1373
|
+
---
|
|
1374
|
+
|
|
1375
|
+
## ๐ Related Documentation
|
|
1376
|
+
|
|
1377
|
+
- [Backend Service Creation Template](./LLM-Service-Creation-Template.md) - Backend patterns
|
|
1378
|
+
- [XState Documentation](https://xstate.js.org/docs/) - State machine patterns
|
|
1379
|
+
- [Apollo Client Documentation](https://www.apollographql.com/docs/react/) - GraphQL client
|
|
1380
|
+
- [Remix Documentation](https://remix.run/docs) - React framework
|
|
1381
|
+
- [TailwindCSS Documentation](https://tailwindcss.com/docs) - Utility-first CSS
|
|
1382
|
+
|
|
1383
|
+
---
|
|
1384
|
+
|
|
1385
|
+
## ๐ Example: Complete Tag Form Implementation
|
|
1386
|
+
|
|
1387
|
+
See the following files for a complete working example:
|
|
1388
|
+
|
|
1389
|
+
- `packages/client-organization/app/routes/$orgName.projects.tags.tsx` - Main view component
|
|
1390
|
+
- `packages/client-organization/app/machines/tagsMachine.ts` - XState machine
|
|
1391
|
+
- `packages/common/graphql/src/generated.ts` - Generated GraphQL hooks
|
|
1392
|
+
|
|
1393
|
+
---
|
|
1394
|
+
|
|
1395
|
+
**This frontend pattern ensures consistent organization context throughout the application and provides a smooth user experience!** ๐
|