Standards that apply to all Conduction Nextcloud apps. These are enforced via ESLint rules and code review.
All apps that depend on OpenRegister (everything except nldesign and mydash) must show an empty state when OpenRegister is not installed, instead of a broken UI.
The settings endpoint must return openRegisters and isAdmin fields:
use OCP\App\IAppManager;
use OCP\IGroupManager;
use OCP\IUserSession;
// In constructor: inject IAppManager, IGroupManager, IUserSession
// In the index() / settings GET endpoint:
$user = $this->userSession->getUser();
$isAdmin = $user !== null && $this->groupManager->isAdmin($user->getUID());
return new JSONResponse([
'openRegisters' => in_array(needle: 'openregister', haystack: $this->appManager->getInstalledApps()),
'isAdmin' => $isAdmin,
'config' => $this->settingsService->getSettings(),
]);The controller should also have the standardized getObjectService() and getConfigurationService() methods for lazy-loading OpenRegister services (see softwarecatalog/opencatalogi for reference).
The settings store must expose:
openRegisters: falsein stateisAdmin: falsein statehasOpenRegistersgettergetIsAdmingetter- Read both from the API response in
fetchSettings()
Three-state conditional in the template:
- OpenRegister missing (
storesReady && !hasOpenRegisters):NcEmptyContentinsideNcAppContentwith classopen-register-missing— no sidebar, no navigation - Normal (
storesReady && hasOpenRegisters): full app with menu, content, sidebar - Loading (else): centered
NcLoadingIcon
The empty state uses:
NcEmptyContentwith:nameand:descriptionprops#iconslot with the app's own icon (imagePath('<appname>', 'app-dark.svg'))#actionslot withNcButtonlinking to app store (admin) or text hint (non-admin)- Admin detection comes from the backend (
settingsStore.getIsAdmin), NOT fromOC.isAdmin(which doesn't exist) - App store URL:
generateUrl('/settings/apps/integration/openregister')
The NcAppContent wrapper needs .open-register-missing class with flex centering. This goes in src/assets/app.css (not in a Vue <style> block).
All <style> blocks in .vue files must use the scoped attribute. Global styles go in src/assets/app.css and are imported in main.js.
Why: Unscoped styles leak into other components and cause hard-to-debug styling issues. The scoped attribute ensures styles only affect the component they belong to.
Enforced by: ESLint rule vue/enforce-style-attribute:
'vue/enforce-style-attribute': ['error', { allow: ['scoped'] }]src/assets/app.css— app-wide overrides (e.g., library component fixes, empty state centering)css/directory — styles loaded by Nextcloud outside of webpack (e.g., dashboard widget icons)- Import in
main.js:import './assets/app.css'
Never use OC.isAdmin — it doesn't exist in Nextcloud's frontend JavaScript API. Instead:
- Pass
isAdminfrom the backend via the settings endpoint usingIGroupManager::isAdmin() - Store it in the Pinia settings store
- Access via computed property in components
Pipelinq is the reference implementation for all these patterns:
- Backend:
pipelinq/lib/Controller/SettingsController.php - Store:
pipelinq/src/store/modules/settings.js - App.vue:
pipelinq/src/App.vue - CSS:
pipelinq/src/assets/app.css - ESLint:
pipelinq/eslint.config.js