Upgrading to frappe-ui 1.0.0
- Dominant language
- Python
- Stars
- 18
- Forks
- 11
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 20
Description
frappe-ui is heading to a `1.0.0` tag. The breaking changes land as separate PRs. This issue is the single place that tracks all of them for Central.
- Work top to bottom. Each section names the exact sites in this repo, with before/after.
- More sections get appended as more breaks land before the tag.
- Do not start until you bump the frappe-ui pin. Today it is `github:frappe/frappe-ui#v1.0.0-beta.24` in `dashboard/package.json:15`.
- Full list of changes: [migration guide](https://github.com/frappe/frappe-ui/blob/main/docs/content/docs/migration.md).
| # | Change | PR | Fails at build? |
| --- | --- | --- | --- |
| 1 | `$socket` and the `FrappeUI` plugin options | frappe/frappe-ui#948 | No — the whole signed-in dashboard throws at runtime |
| 2 | `useTheme` is `useColorScheme` | frappe/frappe-ui#949 | Yes |
| 3 | `Autocomplete` is deleted | frappe/frappe-ui#951 | No — two silent |
| 4 | `Popover` v0 API removed; `Tooltip` `placement` and `#body` renamed (1 Popover) | frappe/frappe-ui#956 | No — every break is silent |
| 5 | Dropdown `placement`, `{ group, items }` and `component:` rows removed (8 sites) | frappe/frappe-ui#957 | No — silent; `vue-tsc` flags them |
Do #1 first. It takes the app down.
Not affected: frappe/frappe-ui#946 (Central has no `useDoctype` and no `useList` — its 91 `useCall` instantiations across 40 files are untouched by that PR, which changed only `useAction.ts`, `useDoctype.ts` and `useList.ts`) and frappe/frappe-ui#947 (no `spritePlugin`, no `frappe-ui/icons` import, no `IconPicker`).
Line numbers are against `develop` as of 2026-08-08 (`138750e3d`).
---
# 1. The plugin no longer opens a socket, and reading `$socket` throws
PR: frappe/frappe-ui#948
**This blanks the entire signed-in dashboard.** Not one widget — every authenticated route.
## 1.1 What happens
`dashboard/src/main.ts:16-18` asks the plugin for a socket:
```ts
app.use(FrappeUI, {
socketio: window.socketio_port ? { port: window.socketio_port } : true,
})
```
`socketio` is no longer an option. The plugin ignores it (with a dev-only `console.warn`) and opens nothing. `initSocket` is deleted too.
`dashboard/src/composables/common/useFrappeRealtime.ts:177-190` then reads the global the plugin used to set:
```ts
function useFrappeSocket(): FrappeSocket {
const instance = getCurrentInstance()
const socket = instance?.appContext.config.globalProperties.$socket as // line 179
| FrappeSocket
| undefined
if (!socket) { // line 183
throw new Error(
'Frappe socket is unavailable. Call this composable inside setup() after installing FrappeUI.',
)
}
return socket
}
```
The plugin now installs a **throwing getter** on `globalProperties.$socket`, so line `:179` throws before line `:183` ever runs. Central's own guard and its own error message are dead code. What surfaces instead is frappe-ui's message.
## 1.2 Why the whole app goes
`dashboard/src/layouts/AppShell.vue:26` calls `useNotificationsRealtime()` at the top level of ``:
```ts
useNotificationsRealtime()
```
That reaches `useFrappeEventListener` (`useNotifications.ts:40`), which calls `useFrappeSocket()` on line `:47` of `useFrappeRealtime.ts`. The read throws during setup, so `AppShell` never renders.
`AppShell` is the parent route component for every authenticated route (`dashboard/src/router/index.ts:44`). Signed-in users get a blank page.
Three more entry points throw the same way once you get past that one:
| Feature | Chain |
| --- | --- |
| Notification bell | `AppShell.vue:26` → `useNotifications.ts:40` → `useFrappeEventListener` (`useFrappeRealtime.ts:47`) |
| Servers list | `useServers.ts:5,58` → `useFrappeList.ts:94` → `useFrappeListInvalidation` (`useFrappeRealtime.ts:172` → `useFrappeDocTypeEventListener` `:128`) |
| Server map | `useServerMapData.ts:4,50` → same `useFrappeListInvalidation` |
## 1.3 The fix
Open the connection yourself and assign it over the guard. Assigning still works — the plugin's getter has a matching setter, and it does not clobber a value assigned before install.
`dashboard/package.json` does not depend on `socket.io-client` today (it came in through frappe-ui). Add it:
```
yarn add socket.io-client
```
`dashboard/src/main.ts`:
```ts
// before
app.use(FrappeUI, {
socketio: window.socketio_port ? { port: window.socketio_port } : true,
})
// after
import { io } from 'socket.io-client'
app.use(FrappeUI)
const host = window.location.hostname
const siteName = import.meta.env.DEV ? host : window.site_name
const socketioPort = window.socketio_port || 9000
const port = window.location.port ? `:${socketioPort}` : ''
const protocol = port ? 'http' : 'https'
app.config.globalProperties.$socket = io(
`${protocol}://${host}${port}/${siteName}`,
{ withCredentials: true },
)
```
That is what `initSocket` did, line for line. `window.site_name` and `window.socketio_port` are both already declared (`dashboard/src/env.d.ts:17-18`) and both are injected by `central/www/dashboard.py:31-32`.
Then delete the dead guard. `useFrappeRealtime.ts:183-187` can no longer fire — a missing `$socket` throws at the read. Either drop the `if (!socket)` block, or read through a `try` if you want Central's own message back:
```ts
function useFrappeSocket(): FrappeSocket {
const instance = getCurrentInstance()
const globals = instance?.appContext.config.globalProperties
let socket: FrappeSocket | undefined
try {
socket = globals?.$socket as FrappeSocket | undefined
} catch {
socket = undefined
}
if (!socket) {
throw new Error('Frappe socket is unavailable. Assign app.config.globalProperties.$socket in main.ts.')
}
return socket
}
```
## 1.4 Nothing else in #948 touches Central
- `request` is not imported anywhere. `dashboard/src/composables/useAuth.ts:1` and `dashboard/src/lib/auth.ts:1` already use `frappeRequest`.
- `createCall` is never imported from frappe-ui. The `createCall` identifiers in `useTeamSettings.ts:33` and `useServers.ts:105` are local `useCall` instances that happen to share the name.
- No component declares an Options-API `resources: {}` block, so the `$resources` opt-in does not apply. Do **not** pass `{ resources: true }`.
- No `config: {}` option is passed, so there is nothing to move to `setConfig`.
---
# 2. `useTheme` is `useColorScheme`
PR: frappe/frappe-ui#949
Build error at the import line, then four dangling identifiers behind it.
## 2.1 The wrapper
`dashboard/src/composables/useTheme.ts:1` and `:13`:
```ts
// before
import { useTheme as useFrappeUITheme } from 'frappe-ui'
export function useTheme() {
if (!localStorage.getItem('theme')) {
localStorage.setItem('theme', 'light')
}
return useFrappeUITheme()
}
// after
import { useColorScheme } from 'frappe-ui'
export function useTheme() {
if (!localStorage.getItem('theme')) {
localStorage.setItem('theme', 'light')
}
const { colorScheme, setColorScheme, toggleColorScheme } = useColorScheme()
return {
currentTheme: colorScheme,
setTheme: setColorScheme,
toggleTheme: toggleColorScheme,
}
}
```
Mapping the names inside the wrapper keeps the three consumers below unchanged. Rename them through if you prefer the new vocabulary — it is four lines either way.
The storage key (`theme`) and the `<html data-theme>` attribute are unchanged by #949, so the seeding on `:10-12` keeps working and users keep their saved preference.
## 2.2 The consumers
Members become `{ colorScheme, setColorScheme, toggleColorScheme }`. Central reads the old names in three files:
- `dashboard/src/composables/useAppMenu.ts:15` — `const { currentTheme, setTheme } = useTheme()`, then `:36`, `:37`, `:59`, `:60`
- `dashboard/src/pages/settings/SettingsPage.vue:10-11`, `:47`, `:48`
- `dashboard/src/components/search/index.ts:50`, `:86` — reads `setTheme` off `useAppMenu()`, so it needs no change if you keep the wrapper's shape
## 2.3 `colorScheme` is read-only
Nothing in Central assigns to `currentTheme.value` today, so this costs nothing — but do not add one. The old ref moved only itself and left `<html>` and `localStorage` behind. Use `setColorScheme`.
## 2.4 The rest of #949 does not apply
No scroll composables. No frappe-ui directives. `dashboard/src/composables/common/useIsMobile.ts` is Central's own — the removed export was frappe-ui's `useIsMobile`, which Central never imported.
---
# 3. `Autocomplete` is deleted
PR: frappe/frappe-ui#951
Two sites, both silent. `FormControl type="autocomplete"` falls through to a plain text input and passes the type on, so you get `<input type="autocomplete">` — no build error, no runtime error, just a text box where the picker used to be.
`dashboard/src/components/billing/EditBillingProfileDialog.vue`, on frappe-ui's `FormControl` (imported at `:2`).
Country, `:188-194`:
```vue
<!-- before -->
<FormControl
v-model="countryModel"
type="autocomplete"
label="Country *"
placeholder="Select country"
:options="countryOptions"
/>
<!-- after -->
<FormControl
v-model="form.country"
type="combobox"
label="Country *"
placeholder="Select country"
:options="countryOptions"
/>
```
State, `:195-202`: the same change, with `v-model="form.state"`.
## 3.1 Delete the `optionModel` wrapper
`Combobox` binds the option's **value**, not the option object. The old `Autocomplete` bound the object, which is the only reason `:83-91` exists:
```ts
function optionModel(field: string) {
return computed<{ label: string; value: string } | null>({
get: () => (form[field] ? { label: form[field], value: form[field] } : null),
set: (opt) => {
form[field] = opt?.value ?? ''
},
})
}
const countryModel = optionModel('country') // line 92
const stateModel = optionModel('state') // line 93
```
Bind `form.country` and `form.state` straight through and delete lines `:83-93`. `form.country` is already a plain string — `:74-81` watches it and `:69` compares it to `'India'`.
`countryOptions` (`:65-67`) is already `{ label, value }[]`, which `Combobox` takes unchanged. `stateOptions` (`:68`) is a plain `string[]`, which it also takes.
## 3.2 What is not affected
Every other `autocomplete` in `dashboard/src` is the plain HTML attribute (`autocomplete="off"`, `autocomplete="email"`). There is no `import { Autocomplete }` anywhere and no local `Autocomplete.vue` fork.
Migration guide sections: "Selection family (Dropdown / Select / Combobox / MultiSelect)" and "`FormControl type=\"autocomplete\"`".
---
# 4. Popover and Tooltip: the v0 API is gone
PR: frappe/frappe-ui#956
frappe-ui 1.0.0 removes the v0 `Popover` API. Central has 1 affected site.
Nothing warns. Vue drops an unknown prop or slot in silence, so a missed site renders a popover with no trigger, an empty one, or a tooltip on the wrong edge — and the build stays green. Work the list.
Line numbers are against `develop` at `10f4c5d`.
## Popover
| v0 | v1 |
| --- | --- |
| `#body` slot | `#default` slot **plus** the `bare` prop — `#body` rendered outside the panel shell |
| `placement="bottom-end"` | `side="bottom"` + `align="end"` (a bare `placement="bottom"` is `align="center"`) |
`#body` replaced the panel shell, so it maps to `#default` **plus** `bare` — without `bare` your content lands inside a second panel.
```vue
<!-- before -->
<Popover>
<template #body><EmojiPicker /></template>
</Popover>
<!-- after -->
<Popover bare>
<EmojiPicker />
</Popover>
```
1 site:
- `dashboard/src/components/notifications/NotificationBell.vue:31` — #body slot, placement prop.
Full before/after for all of it: [migration guide](https://github.com/frappe/frappe-ui/blob/main/docs/content/docs/migration.md#popover--hovercard--tooltip).
---
# 5. Dropdown: `placement`, `{ group, items }` and `component:` rows are gone
PR: frappe/frappe-ui#957
frappe-ui 1.0.0 removes three shapes from `Dropdown` (`ContextMenu` shares the option types). Central has 8 affected sites.
Nothing fails the build. A `placement` prop is dropped and the menu shifts to the default alignment; a `{ group, items }` group resolves to zero options and renders an empty menu; a `component:` row renders as a plain action row using its `label`, which for most of these rows is empty. Dev builds log a console warning for the last two, and the removed keys stay in the types as `never`, so `vue-tsc` names every site. Work the list.
Line numbers are against `develop` at `10f4c5d`.
## `placement` → `align`
| before | after |
| --- | --- |
| `placement="right"` | `align="end"` |
| `placement="center"` | `align="center"` |
| `placement="left"` | delete the prop — `start` is the default |
`PaymentMethodRowActions.vue` and `SubscriptionRowActions.vue` pass `bottom-end`, a value `placement` never accepted (`'left' | 'right' | 'center'`) — it has been silently falling through to start-aligned all along. Delete the prop to keep what renders today, or write `align="end"` for what the site meant — that one changes what users see.
8 sites:
- `dashboard/src/components/addons/AIApiKeys.vue:223` — `right`.
- `dashboard/src/components/billing/PaymentMethodRowActions.vue:60` — `bottom-end — see note above`.
- `dashboard/src/components/billing/SubscriptionRowActions.vue:56` — `bottom-end — see note above`.
- `dashboard/src/components/servers/ServerRowActions.vue:100` — `right`.
- `dashboard/src/components/servers/SiteRowActions.vue:40` — `right`.
- `dashboard/src/components/team/InvitationRowActions.vue:37` — `right`.
- `dashboard/src/components/team/RoleRowActions.vue:29` — `right`.
- `dashboard/src/components/team/TeamMemberRowActions.vue:37` — `right`.
Full before/after: [migration guide](https://github.com/frappe/frappe-ui/blob/main/docs/content/docs/migration.md#dropdown-and-contextmenu).
Contributor guide
No contributing guide indexed for this repository
Research direction
Read the migration guide and dashboard/package.json:15 first, then inspect dashboard/src/main.ts and the listed composables and components section by section. Work from the frappe-ui 1.0.0 breaking-change list, run the dashboard build and vue-tsc checks, and verify authenticated routes and affected controls still render without silent API failures.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 52/100