A Power Pages front-end enhancement for BC's Environmental Farm Plan workbook system. Built for the BC Ministry of Agriculture and Food.
This repo contains powerpod – a JavaScript library that extends Microsoft Power Pages with custom UI components, form logic, and Dataverse integrations. It powers the EFP workbook experience where producers complete environmental assessments with their Planning Advisors.
The core workflow: producers log in → fill out workbook chapters → PAs review and sign off → workbooks get submitted.
cd powerpod
npm install
npm run dev # Build with watch mode
npm run build # Production build
npm test # Run testsYou don't want to upload a new Web File every time you make a change. Instead, serve the bundle locally and redirect the browser to load it from your machine.
cd powerpod
npm run dev # Builds and watches for changes
npm run serve # Serves dist/ on http://localhost:8080Or run both in separate terminals. The serve command uses http-server to host files from dist/ with caching disabled and CORS enabled.
Use a browser extension like Resource Override (Chrome) or Requestly to intercept the request for the production bundle and redirect it to your local build.
Example redirect rule:
| From | To |
|---|---|
https://af-efp-dev.powerappsportals.com/powerpod-*.min.js |
http://localhost:8080/powerpod.js |
Now when you visit the DEV portal, it loads your local powerpod.js instead of the deployed version. Make a change, save, and refresh – no deploy needed.
- Use the non-minified build – Point to
powerpod.js(not.min.js) for readable stack traces and console logs - Watch mode auto-rebuilds –
npm run devrecompiles on file save - Storybook for components – Run
npm run storybookto develop UI components in isolation
Instead of using a browser extension, you can create a dedicated test page in Power Pages that loads directly from localhost:
- In Portal Management, create a new Web Page (e.g.,
/efpworkbook-dev/) - In its content snippet or template, use a loader that points to your local server:
(function () { const src = 'http://127.0.0.1:8080/powerpod.js'; const script = document.createElement('script'); script.setAttribute('async', ''); script.src = src; document.head.appendChild(script); })();
Now you have a dedicated URL for testing that always loads your local build. No browser extensions needed – just navigate to /efpworkbook-dev/ instead of /efpworkbook/.
Chrome DevTools has a built-in "Local Overrides" feature:
- Open DevTools → Sources → Overrides
- Select a local folder to store overrides
- Find
powerpod.min.jsin the Network tab, right-click → "Save for overrides" - Replace the file content with your local build
This works but you have to manually copy the file each time.
Developer Guide – Everything below is a quick-reference for developers joining the project. Last updated: January 2026
We build a JavaScript bundle (powerpod) that gets injected into Microsoft Power Pages to extend its capabilities beyond what's available out of the box.
efp/
├── assets/ # Static assets for Power Pages portal
│ ├── Content Snippets/ # Global HTML/CSS snippets (injected via Portal Management)
│ │ ├── headbottom.html # Global styles, fonts, Shoelace includes
│ │ └── EFP_Gov_Style_CSS.html # BC Gov theme CSS tokens
│ ├── Templates/ # Liquid templates for portal pages
│ │ ├── efp-home-page-template.html
│ │ └── efp-workbook-template.html
│ ├── PowerPagesStyling/ # Portal theme CSS files
│ └── Web Pages/ # Page-specific content
│
├── powerpod/ # Main JavaScript application
│ ├── src/
│ │ ├── js/
│ │ │ ├── app.js # Entry point - bootstraps everything
│ │ │ ├── powerpod.js # Core jQuery plugin + route detection
│ │ │ ├── jquery-adapter.js # jQuery initialization wrapper
│ │ │ ├── common/ # Shared utilities
│ │ │ │ ├── constants.js # Environment hosts, form types, status codes
│ │ │ │ ├── env.ts # Environment detection (dev/test/prod)
│ │ │ │ ├── fetch.js # All Dataverse API calls live here
│ │ │ │ ├── logger.js # Logging utility
│ │ │ │ ├── options.js # Runtime configuration
│ │ │ │ ├── utils.js # General helpers
│ │ │ │ └── ... # Other utilities
│ │ │ ├── components/ # Lit-based web components
│ │ │ │ ├── EFPEntryForm.ts # Main workbook form component
│ │ │ │ ├── NavigationSidebar.ts
│ │ │ │ ├── ActionPlanTable.ts
│ │ │ │ ├── efp/ # EFP-specific component utils
│ │ │ │ └── ...
│ │ │ ├── services/ # Business logic services
│ │ │ │ ├── ChapterManagementService.ts
│ │ │ │ ├── WorkbookResponseService.ts
│ │ │ │ └── WorkbookValidationService.ts
│ │ │ ├── state/ # State management
│ │ │ ├── store/ # Vuex-like store pattern
│ │ │ ├── pages/ # Page-specific initialization
│ │ │ └── workbook/ # Workbook feature logic
│ │ ├── assets/ # CSS, fonts, icons bundled with JS
│ │ └── test/ # Jest test files
│ ├── dist/ # Build output
│ │ ├── powerpod.js # Development build (readable)
│ │ └── powerpod.min.js # Production build (minified)
│ ├── releases/ # Versioned release artifacts
│ │ └── powerpod-X.Y.Z.min.js
│ ├── rollup.config.js # Rollup bundler configuration
│ ├── package.json # NPM dependencies & scripts
│ └── jest.config.cjs # Test configuration
│
└── scripts/
├── version-bump.sh # Bumps version across all files, copies release
└── pipeline.sh # CI validation script
Runtime: Node.js (check .nvmrc or use latest LTS)
Package Manager: NPM (package-lock.json present)
Bundler: Rollup with Babel (transpiles to ES5 for IE11 compat)
Rollup produces two files:
dist/powerpod.js– UMD bundle, readable (for debugging)dist/powerpod.min.js– UMD bundle, minified (drops console logs)
This is the part that confuses everyone at first. Here's the flow:
-
Content Snippets – In Portal Management, there's a global "Head/Bottom" snippet that loads the powerpod script. See
assets/Content Snippets/headbottom.html. -
CDN Loading – The script is loaded from jsDelivr CDN, which serves files directly from this GitHub repo. The loader in the content snippet looks like:
(function () { const src = 'https://cdn.jsdelivr.net/gh/bcgov/nf-af-efp-digitalwb@dev/powerpod/releases/powerpod-4.9.1.min.js'; const script = document.createElement('script'); script.setAttribute('async', ''); script.src = src; document.head.appendChild(script); })();
The URL format is:
cdn.jsdelivr.net/gh/{org}/{repo}@{branch}/{path}To deploy a new version, just push to the branch referenced in the URL (e.g.,
devormain). jsDelivr will serve the updated file within minutes. -
Page Templates – Liquid templates (like
efp-workbook-template.html) define the page structure. They include containers that powerpod components render into (e.g.,.efpEntryFormContainer). -
Auto-initialization – When the page loads, powerpod detects the current URL path, determines which form/page type it is (Workbook, Application, Claim, etc.), and initializes the appropriate functionality.
- Run
scripts/version-bump.shto increment version and copy build toreleases/ - Commit and push to the branch referenced in the CDN URL
- Update the version number in the Content Snippet if using a versioned filename
For production, you may want to use a specific version tag instead of a branch to avoid unexpected updates.
The app auto-detects which environment it's running in based on the hostname:
| Environment | Hosts |
|---|---|
| DEV | af-pods-dev.powerappsportals.com, af-efp-dev.powerappsportals.com |
| TEST | af-pods-test.powerappsportals.com, af-efp-test.powerappsportals.com |
| PROD | af-pods.powerappsportals.com |
See powerpod/src/js/common/constants.js for the full host list.
Log levels vary by environment:
- DEV: Everything logged
- TEST: Warnings and errors only
- PROD: Errors only
The app fetches environment variables from Dataverse at runtime via the /_api/environmentvariabledefinitions endpoint. These are used for configuration that varies between environments.
All custom env vars are prefixed with quartech_ in the schema name.
Power Pages uses Site Settings for configuration. Key ones to know:
- Authentication settings (BCSC integration)
- Content snippet references
- Custom entity permissions
All Dataverse API calls are centralized in powerpod/src/js/common/fetch.js. The ENDPOINT_URL object defines every endpoint used:
ENDPOINT_URL = {
get_env_vars_data: "/_api/environmentvariabledefinitions?...",
get_workbook_data_by_id: (id) => `/_api/quartech_workbooks(${id})`,
get_chapters_data: "/_api/quartech_chapters?$filter=statecode eq 0",
// ... etc
}If you're adding a new feature that needs data from Dataverse, add the endpoint here and create a corresponding fetch function.
We keep it simple:
| Branch | Purpose |
|---|---|
main |
Production-ready code. Deployments to PROD come from here. |
dev |
Active development. This is the default branch for PRs. |
release/* |
Release branches for stabilization before merging to main. |
feature/* |
Individual feature work. Branch off dev, merge back to dev. |
- Create
feature/my-thingfromdev - Do your work, push commits
- Open PR against
dev - After review + merge, the updated
devgets deployed to DEV environment - When ready for release, create
release/X.Y.Zfromdev - Test in TEST environment, fix any issues directly on release branch
- Merge to
mainfor production deploy
We use semver-ish versioning. The scripts/version-bump.sh script increments the patch version and updates:
powerpod/package.jsonpowerpod/rollup.config.js(license banner)powerpod/src/js/powerpod.js(runtime version)
Then it copies the minified build to the releases folder.
Quick reference for common tasks:
| If you need to... | Look here |
|---|---|
| Add a new Dataverse API call | powerpod/src/js/common/fetch.js |
| Change environment detection | powerpod/src/js/common/constants.js |
| Modify the workbook form UI | powerpod/src/js/components/EFPEntryForm.ts |
| Update navigation sidebar | powerpod/src/js/components/NavigationSidebar.ts |
| Add/modify workbook questions | powerpod/src/js/components/QuestionRenderer.ts |
| Change chapter navigation logic | powerpod/src/js/services/ChapterManagementService.ts |
| Update response saving behavior | powerpod/src/js/services/WorkbookResponseService.ts |
| Add validation rules | powerpod/src/js/services/WorkbookValidationService.ts |
| Modify state management | powerpod/src/js/store/ directory |
| Update global portal styles | assets/Content Snippets/headbottom.html |
| Change page templates | assets/Templates/ directory |
| Configure allowed paths/hosts | powerpod/src/js/common/options.js |
| Add/update program configuration | See powerpod/README.md for JSON config format |
| Write tests | powerpod/src/test/ directory |
| Debug in Storybook | powerpod/src/js/components/*.stories.ts |
| Review UI customizations | docs/CUSTOMIZATION_INVENTORY.md |
| Portal Path | Feature | Entry Point |
|---|---|---|
/ or /home-dev/ |
Home page | Auto-detected, loads home content |
/efpworkbook/ |
Workbook form | initWorkbook() in workbook/workbook.js |
/my-efp-workbooks/ |
Workbook list | initMyEfpWorkbooks() in pages/myEfpWorkbooks.js |
/application/ |
Grant application | Application form logic (legacy) |
/claim/ |
Claim submission | Claim form logic (legacy) |
Path detection happens in powerpod/src/js/powerpod.js and uses the path arrays defined in constants.js.
- Console logging: Use the
Loggerutility (import { Logger } from './common/logger.js'). It respects environment log levels. - jQuery is available: Power Pages includes jQuery. Powerpod piggybacks on it via
$.fn.powerpod(). - Shoelace components: We use Shoelace for UI components (dialogs, buttons, etc.). It's loaded via CDN in the head snippet.
- Lit for web components: Custom components use Lit. Check existing components for patterns.
-
CORS issues on localhost: The fetch module automatically prefixes requests with the DEV portal URL when running locally, but you still need proper browser setup (or use the actual portal).
-
Caching: API responses are cached by default. Use
skipCache: truein fetch calls if you need fresh data. -
Request Verification Token: POST/PATCH/DELETE requests need the
__RequestVerificationTokenheader. The fetch module handles this when you setaddRequestVerificationToken: true. -
Path matching: The app only runs on allowed paths. If your new page isn't working, check
options.jsfor theallowedPathsarray. -
Version mismatch: After deploying, users might have cached old versions. The version is in the license banner at the top of the bundle.
Questions? Check the powerpod README.md for field configuration docs, or ping the team.