opendesk
PublicREADME.md
The customer portal is a Studio app (Studio ships with frappe). Customers use it at /help to read the knowledge base, raise tickets and reply to them. Agents use the desk app at /opendesk.
This file explains how the portal is built, where its code lives and how to work on it. For the knowledge base from end to end (DocTypes, APIs, desk authoring), read KNOWLEDGE_BASE.md.
How Studio runs the portal
Studio is a Frappe app that renders pages from JSON. Each page is a tree of blocks: Vue components with props, events and styles. A page can also have a TypeScript setup script. The values that the script returns are available to the blocks through {{ }} expressions.
Opendesk ships the portal as a standard app: the pages live as files in this folder, not only in the database.
- The files in git are the source of truth.
bench migrateimports them into theStudio App,Studio PageandStudio ComponentDocTypes. It also deletes pages that have no file. - With
developer_modeon, a save in the Studio editor writes the JSON back to these files. Commit the file diff. - Page scripts (
.ts) and Vue components (.vue) live only on disk. The database never holds them. bench build-studio-app opendesk --site <site>builds the production bundle intoopendesk/public/app_builds/opendesk/(gitignored).bench build --app opendeskalso builds it.
Studio documents its own rules. Read them before you change a page:
| Topic | File in apps/frappe |
|---|---|
Dev server and watch-studio setup | studio/frontend/README.md |
File layout, page JSON, setup(context), custom Vue components, sync loop | skills/studio-app-building/SKILL.md |
Runtime rules: {{ }}, event scripts, v-model, Repeater, icons, tokens | skills/studio-app-building/RULES.md |
Routing and the global boot | skills/studio-app-building/ROUTING.md |
| Visual design language | skills/studio-app-building/DESIGN.md |
What is where
studio/opendesk/
opendesk.json Studio App record: route "help", app_home = public_kb
router.ts opens the Settings dialog at <page>/settings/<screen>
studio_page/<page>/ <page>.json (block tree) + <page>.ts (setup script)
studio_components/ Studio Components: reusable block trees with inputs
components/ custom Vue components, by area
stores/ module-level state shared between pages
composables/ page logic, one concern per file
tailwind.config.js adds shared/src to Studio's Tailwind scan
tsconfig.json @app/* alias. Studio rewrites it on every app save
Pages
| Page | URL | Guest | Shows |
|---|---|---|---|
public_kb | /help | yes | KB home: banner, search, pinned categories, popular and recent articles |
categories | /help/categories | yes | All categories with a filter |
category | /help/category/:category | yes | Articles in one category |
article | /help/articles/:name | yes | Article body, contents, feedback, related articles |
customer_tickets | /help/my-tickets | no | Ticket list with saved views, filters, sort and export |
new_ticket | /help/tickets/new | no | New ticket form with article suggestions |
ticket | /help/tickets/:name | no | Ticket thread, reply composer, details and feedback |
The article URL param is <name>-<slug>. articleName() in stores/knowledgeBase.ts reads the name from it.
Studio Components
Every page embeds header and kb_settings.
header: the top bar. It has the brand, breadcrumbs, quick links, the theme toggle and the account menu. Its inputs (crumb,parentRoute,drawerToggleand others) are listed in the JSON.kb_settings: the portal Settings dialog. It has profile, members, knowledge base and portal settings tabs. It has no inputs and binds touseSettingsModal.category_grid: a Repeater of category cards.public_kbandcategoriesuse it.
Custom Vue components
Under components/, by area: ui, header, list, ticket, knowledge_base and settings. A block uses one with isCustomVueComponent: true and the file name as componentName.
- Portal-only components start with
Portal. - The file name must be unique in
studio/opendesk/and must not match a Studio built-in component. Studio skips a clash and writes an Error Log. - The desk imports
NewTicketForm.vueandCustomActions.vuefrom here by relative path. Test the desk when you change them.
Stores
The stores are module-level singletons, not Pinia.
settings/index.ts:useSettingsModal(context). Every page spreads it into its return value. It binds the router and socket, and it exposest(translate) androutes(ROUTES) to the blocks.session.ts: user and site flags fromopendesk.api.config.get_config(isGuest,isAgent,isPublicKnowledgeBase, brand).router.ts: holds the router, because page scripts get it only throughcontext. UsenavigateTo()from here.knowledgeBase.ts: the KB sidebar, an article cache and hover prefetch.views.ts: saved ticket list views (OD View).ticketMeta.ts: ticket statuses and priorities, loaded lazily because guests cannot read them.recent.ts,conversationLayout.ts: per-browser preferences in localStorage.
Composables
useKnowledgeBaseHeader: the header for the four KB pages. It runs KB form scripts into header links and custom actions.useArticleSearch: debounced article search, used by the search box and the new ticket form.useDrawer: the ticket page's mobile side drawer.useBottomSheet: open state for the knowledge base sidebar, a frappe-uiBottomSheeton phones.useTicketThread,useReplyComposer,useTicketDetails,useTicketFeedback,useOutsideHoursBanner,useChatRows: the parts of theticketpage.useRememberedCount: remembers a list's length so its skeleton keeps the same size.
Shared code: @opendesk/shared
shared/src holds the code that the portal and the desk both use. The root package.json links it as @opendesk/shared, so run yarn install in apps/opendesk before the first build.
portalRoutes.ts:ROUTES, every portal path. The desk uses it to link into the portal.translation.ts:__()andfetchTranslations().utils.ts: dates,CUSTOMER_PORTAL_ROOT,updateTicket,addHeadingIds,countLabeland more.settings/:KnowledgeBaseFormandPortalPermissionsForm, used by both the desk Settings and the portal Settings dialog.filter/,ticketCells.ts,ticketStatus.ts,tiptap.ts,formScripts.tsand shared icons.
Put code in shared/src only when both apps use it. Inside shared/src, use relative imports.
Backend and serving
opendesk/hooks.py:studio_app_bootsets the globalboot(timezone, home page, brand).update_website_contextsets the portal title and favicon.website_redirectssends the old/opendesk/my-ticketsand/opendesk/kb-publicURLs to/help.
opendesk/opendesk/hooks/studio_app.py: the two hook functions.opendesk/www/opendesk/index.py: sends a customer who opens/opendeskto the portal.opendesk/api/: the portal callsconfig.py,knowledge_base.py,article.pyandcustomer.py. Ticket calls go toopendesk/opendesk/doctype/od_ticket/api.py.
Set up for development
- Do the "Frontend Development & Exported Apps" setup in
apps/frappe/studio/frontend/README.md:developer_mode,ignore_csrf,bench setup requirements --devandyarn dev --hostinapps/frappe/studio/frontend. In developer modebench watchstarts the same dev server. - Run
yarn installinapps/opendesk. This links@opendesk/shared. - Start the watcher and keep it running:
bench --site <site> watch-studio. - Open
/dev/helpon the Vite dev server, for examplehttp://<site>:8080/dev/help.
Use /dev/help while you work. /help serves the last production build, so it does not show your changes until you build again.
What reloads by itself:
.tsand.vuefiles: Vite hot-reloads them.- Page and component JSON:
watch-studioimports it. tailwind.config.js: restart Vite.- A new folder under
studio/: restartwatch-studio.
Do not run bench migrate or build-studio-app in the edit loop. Build only to check the production bundle.
Make a change
Change a page
- Read the page's
.jsonand.tstogether. The blocks bind only to whatsetupreturns, toroute, toinputs(inside a Studio Component) and todataItem(inside a Repeater). - Put the logic in the
.tsfile, a composable or a store. Keep each{{ }}expression to a name or a short call. - Edit the block tree in the Studio editor or in the JSON file. If you edit the JSON while the page is open in the editor, refresh the editor before you save there.
- Check the page at
/dev/helpas a guest, a customer and an agent where they apply.
Add a page
- Make the page in the Studio editor, inside the Opendesk app. In developer mode the save writes
studio_page/<page>/<page>.json. - Add
<page>.tsnext to it withexport default function setup(context). SpreaduseSettingsModal(context)(oruseKnowledgeBaseHeader(context)on a KB page) into the return value. - Add
headerandkb_settingsblocks, like the other pages. - Add the path to
ROUTESinshared/src/portalRoutes.ts. - Set
allow_guestonly if guests must see the page.
Add a component
- Add
components/<area>/Portal<Name>.vue. Use frappe-ui components and Tailwind with the semantic tokens. - Use it in a block with
isCustomVueComponent: trueandcomponentName: "Portal<Name>". - Make it a Studio Component (
studio_components/) only if it is a block tree that pages repeat. Make it a Vue component when it has its own logic.
Rename or retitle a page
Keep page_name the same. Move the .json and .ts to the new folder name together. A new title also changes the route name, so search for router.push({ name: ... }) callers.
Conventions
- Routes: use
ROUTESin scripts and{{ routes.x(...) }}in blocks. Navigate withnavigateTo()fromstores/router.ts. - Translation: use
__('literal string')from@opendesk/shared/translationwith{0}placeholders. In blocks, use{{ t('...') }}. Build labels inside a function or computed, so they follow a language change. - Imports:
@app/...for code instudio/opendesk,@opendesk/shared/<module>for shared code. - Comments: one line, only for a reason the code cannot show.
- Guest APIs:
@frappe.whitelist(allow_guest=True, methods=["GET"]), withvalidate_public_access()and a@rate_limitwhere it can be abused. - Commits: conventional commits with the
portalorkbscope, for examplefix(portal): send a reply that has only attachments.
Gotchas
- If you edit
opendesk.jsonor a Studio Component JSON by hand, increase itsmodifiedvalue.bench migrateskips the file otherwise.watch-studioimports it anyway. watch-studiodoes not sync deletions.bench migratedeletes pages and apps that have no file. It never deletes Studio Components.- A database patch does not change these files. When a patch changes Studio records, change the files too.
- The Studio file explorer cannot edit exported JSON. Edit it in the editor or on disk.
- A component in a block tree that Studio does not know fails the build.
Tests
- End to end: Playwright specs in
e2e/tests/. Portal specs sit next to their desk page:knowledge-base/,tickets/customer/,tickets/list/,tickets/new/andsettings/portal-settings-walkthrough.spec.ts. Reade2e/README.mdfor how to run and write them. - Backend:
opendesk/tests/api/test_knowledge_base.py,test_article.py,test_customer.pyandtest_auth.py. Readopendesk/tests/AGENTS.mdfor the rules.