Development
Develop ERPNext TaskView
Set up a bench checkout, build the Vue and frappe-ui bundles, run the Python tests and JavaScript checks, and see what CI and pre-commit enforce.
On this page
This page is for people changing the app. How it works explains the design behind the code, and the changelog says where to find what changed.
Set up
You need a bench with Frappe v16 and ERPNext, plus Node 24 or newer and yarn. Work in the app's folder inside the bench, because the build writes into the bench's sites/assets:
cd apps/erpnext_taskview
yarn install
yarn buildyarn build builds the desk bundles and the portal, then runs scripts/vite-register.mjs, which registers the bundles in the bench's sites/assets/assets.json. It finds the bench from the app folder (apps/erpnext_taskview) or $FRAPPE_BENCH_ROOT, and skips itself outside one. The first bench build of the app installs the dependencies and runs the same command: see Install and upgrade.
Commands
Run these from the app folder.
| Command | What it does |
|---|---|
yarn build | Builds the desk bundles and the portal, then updates sites/assets/assets.json |
yarn dev | Rebuilds the Task View bundle whenever a file changes |
yarn dev:dock | Rebuilds the timer dock bundle whenever a file changes |
yarn dev:portal | Runs the portal on the Vite dev server, proxied to the bench |
yarn build:taskview | Builds the Task View bundle only |
yarn build:timerdock | Builds the timer dock bundle only |
yarn build:portal | Builds the portal only |
yarn typecheck | Type-checks the desk code and the portal code |
yarn lint, yarn lint:fix | Runs oxlint on erpnext_taskview/public/js and portal/src |
yarn format, yarn format:check | Runs oxfmt on the same two folders |
The Python tests are run with bench: see Tests.
yarn dev:portal finds the bench by walking up from the app folder to a folder that holds sites and apps, reads the web server port from sites/common_site_config.json (8000 when there is none), and serves the portal on the matching Vite port: 8080 for web port 8000, 8081 for 8001, and so on. It proxies /api, /app, /assets and the other Frappe routes to the bench. Under the dev server it takes its boot data from erpnext_taskview.www.projects.get_context_for_dev instead of the Jinja page, and that method answers only on a site in developer mode.
Where things live
erpnext_taskview/
hooks.py app hooks: bundles, class overrides, portal routes
install.py creates the Client Portal Notification Type
patches/v1_0/setup_client_portal.py one-time portal switch-over for existing sites
erpnext_taskview/ the app's module
api.py desk endpoints: tree data, saves, timers, pinning
budget.py budget and logged-hours rollup
models.py Pydantic models of the desk API
custom/ custom fields, property settings and class overrides
portal/ portal API
access.py who may open what
api.py endpoints behind /projects
html.py sanitising rich text in and out
notify.py notifying the team
models.py Pydantic models of the portal API
www/projects.py page shell and boot data for the portal
public/js/ desk bundles: TaskView.vue, components/, timer store
tests/ Python tests and their builders
portal/ the portal app (Vue 3, frappe-ui)
vite.config.ts desk bundles
vite.portal.config.ts portal build
scripts/vite-register.mjs registers the Vite bundles in sites/assets/assets.json (managed)How the build fits together
Desk bundles.
vite.config.tsbuilds two IIFE bundles fromerpnext_taskview/public/js:taskview.entry.tsandtimerdock.entry.ts. The sources are not named*.bundle.*, so Frappe's esbuild leaves them to Vite. Frappe is declared external, because the desk provides it as a global. Both write intoerpnext_taskview/public/distone after the other, so neither empties the folder.scripts/vite-register.mjs, which frappe-nix manages, then reads the Vite manifests and writes thetaskview.bundle.js,taskview.bundle.css,timerdock.bundle.jsandtimerdock.bundle.csskeys thathooks.pylists inapp_include_jsandapp_include_cssinto the bench'ssites/assets/assets.json. Outside a bench it skips itself, soyarn buildworks in a bare checkout.Portal.
vite.portal.config.tsbuildsportal/intoerpnext_taskview/public/portal/and copies itsindex.htmltoerpnext_taskview/www/projects.html. Frappe renders that file as a Jinja template with the context fromwww/projects.py, and frappe-ui's build plugin turns every key ofcontext.bootinto awindowglobal. Both outputs are generated and git-ignored. Do not name portal files*.bundle.*: Frappe's esbuild compiles every such file underpublic/on its own.Two TypeScript versions. The app's
typescriptis the native TypeScript 7 build, which has no JavaScript API.vue-tscand Vue's single-file-component compiler need one, sotypescript5is an alias of TypeScript 5.scripts/vue-tsc.cjsrunsvue-tscagainst it, andvite.portal.config.tsregisters it with the SFC compiler, which frappe-ui's components need to resolve their prop types.Frappe's types. The desk code is checked against frappe-types (
tsconfig.jsonextends its strict preset and loads itsfrappeglobal). What the app hangs on frappe's namespaces (frappe.views.TasksView,frappe.views.TaskViewSelect,frappe.ui.TaskViewSortSelector) is declared intypes/erpnext_taskview.augment.d.ts, together with the one frappe member frappe-types does not declare yet.Tailwind 3. frappe-ui is still a Tailwind 3 consumer, so Dependabot is told to ignore Tailwind 4 and later.
Tests
The Python tests are Frappe IntegrationTestCases in erpnext_taskview/tests/, with record builders in fixtures.py:
| File | Covers |
|---|---|
test_api.py | The desk endpoints: filters and sorting, every save_doc route (projects, tasks, drag and drop, timers, manual logs), bulk creation, deleting, assigning and pinned order |
test_hooks.py | The install hook, the hooks the app registers, and the Task extension's permission bypass |
test_budget.py | Rollup of budgets and logged hours, own estimates beating subtask sums, Cancelled Timesheets, open timers, rollup by parent_task, and a parent cycle that must not hang |
test_portal.py | Who can open what, template tasks hidden, project and task reads, status changes and closing assignments, creating tasks, comments, attachments, boot data and the legacy link redirect |
test_pinned.py | Quick entry, placing after an anchor, giving a task a project, and the cases that are refused |
test_timesheet_guard.py | A submitted Timesheet never moves a task backwards |
Frappe only runs tests on a site that allows them. Use a development or test site, never production:
bench --site <TEST_SITE> set-config allow_tests true
bench --site <TEST_SITE> run-tests --app erpnext_taskview
bench --site <TEST_SITE> run-tests --module erpnext_taskview.tests.test_budgetIn the frappe-nix dev shell, frappe-test --reuse-site runs the same tests with coverage of the app alone (the floor is fail_under in pyproject.toml), checks that every whitelisted method and hook ran, and that the controller extensions compose with other apps'.
There are no JavaScript unit tests. yarn typecheck, yarn lint and yarn format:check are the front-end gates.
Continuous integration
The check workflow (.github/workflows/check.yml) runs on every pull request and every push to main. Its build job uses Node 24 and runs, in order:
yarn install --frozen-lockfileyarn typecheck,yarn lintandyarn format:checkyarn build, whosescripts/vite-register.mjsstep finds no bench in CI and skips itselfA check that
erpnext_taskview/www/projects.htmlexists and does not contain.__, because Frappe renders it as a Jinja template and refuses any template that does
Dependabot updates the JavaScript dependencies daily, in one group, and a workflow turns on auto-merge for its pull requests, so a pull request merges once its required checks pass.
The Docs workflow (.github/workflows/docs.yml) checks this documentation when docs/ changes. It is described below.
Conventions
Server code uses the Frappe ORM and Query Builder only, never raw SQL.
Python is formatted with ruff: tabs, double quotes and a line length of 110. Every source file starts with the copyright header the
validate_copyrightpre-commit hook checks.The API shapes are written twice, in Pydantic and in TypeScript, and kept in step by hand:
erpnext_taskview/erpnext_taskview/models.pywitherpnext_taskview/public/js/types.ts, anderpnext_taskview/portal/models.pywithportal/src/types.ts. Change both together.The Python code declares no dependencies of its own. It uses Pydantic, nh3 and BeautifulSoup, which Frappe v16 already depends on.
Patches are listed in
patches.txt. A fresh install marks every patch as done without running it, so anything a new site needs must also happen inafter_install(install.py): see Install and upgrade.
Pre-commit
.pre-commit-config.yaml is managed by frappe-nix (frappe-init --sync writes it from the app standards profile) and runs with prek. It runs ruff, ssort, oxlint and oxfmt, the standard file checks, workflow and shell linting, the conventional-commit check, frappe-nix compat, and Frappe's test_utils hooks (validate_copyright, validate_patches, static_analysis and others). ruff and the other Python tools are pinned in tools/uv.lock, oxlint and oxfmt in package.json. The dev shell (direnv allow, or nix develop --no-pure-eval) installs the hooks; uv run --frozen --project tools prek run --all-files runs them by hand. The documentation is excluded from the hooks that would otherwise rewrite it: validate_copyright stamps a copyright comment above the frontmatter of every Markdown file, so it skips docs/.
Working on these docs
The documentation is Markdown in docs/. It is published with the other Avunu projects at docs.avunu.net/erpnext-taskview/ by Avunu/site-docs, which picks up the latest release of this repository. Write the files so that they read well on GitHub too: frontmatter, GitHub alerts and relative links.
To check a change as the pull request check does, clone Avunu/site-docs beside this repository and, from its folder (Node 22.19 or newer), run:
npm ci --ignore-scripts && npm run build
node build/cli.js check --only erpnext-taskview --source erpnext-taskview=../erpnext_taskviewUse build --lenient instead of check to write a preview of the site into dist/. See previewing.