Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import globals from "globals";
import pluginReactConfig from "eslint-plugin-react/configs/recommended.js";
import pluginReactHooks from "eslint-plugin-react-hooks";

import path from "path";
import { fileURLToPath } from "url";
Expand All @@ -16,8 +17,22 @@ export default [
...compat.extends("standard-with-typescript"),
pluginReactConfig,
{
settings: {
react: {
version: "detect",
},
},
plugins: {
"react-hooks": pluginReactHooks,
},
rules: {
"eslint-plugin-react-hooks/rules-of-hooks": "error",
"react-hooks/rules-of-hooks": "error",
}
},
{
files: ["src/locales/index.js"],
rules: {
"spaced-comment": "off",
},
}
];
31 changes: 13 additions & 18 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2025-11-15T14:56:01.306Z\n"
"PO-Revision-Date: 2025-11-15T14:56:01.306Z\n"
"POT-Creation-Date: 2026-06-02T09:10:34.358Z\n"
"PO-Revision-Date: 2026-06-02T09:10:34.359Z\n"

msgid "Data store keys created!"
msgstr "Data store keys created!"
Expand Down Expand Up @@ -111,12 +111,12 @@ msgstr "Enrollment configuration saved"
msgid "Failed to save enrollment configuration"
msgstr "Failed to save enrollment configuration"

msgid "Add"
msgstr "Add"

msgid "Add widget"
msgstr "Add widget"

msgid "Add"
msgstr "Add"

msgid "Add a widget from the list below to add it to the column"
msgstr "Add a widget from the list below to add it to the column"

Expand Down Expand Up @@ -181,23 +181,18 @@ msgstr "Edit event"
msgid "Edit"
msgstr "Edit"

msgid "Make the enrollment page your own"
msgstr "Make the enrollment page your own"
msgid "No custom layout yet"
msgstr "No custom layout yet"

msgid ""
"Switch to edit mode to build a layout that surfaces the data your team "
"needs most. Rearrange widgets, spotlight key indicators, and guide users "
"with a tailored flow."
"This page still uses the default layout. Jump into edit mode to add "
"widgets, reorder columns and create an experience that fits your workflows."
msgstr ""
"Switch to edit mode to build a layout that surfaces the data your team "
"needs most. Rearrange widgets, spotlight key indicators, and guide users "
"with a tailored flow."

msgid "Start designing"
msgstr "Start designing"
"This page still uses the default layout. Jump into edit mode to add "
"widgets, reorder columns and create an experience that fits your workflows."

msgid "You can always revert back to the default layout later."
msgstr "You can always revert back to the default layout later."
msgid "Start"
msgstr "Start"

msgid "Quick actions"
msgstr "Quick actions"
Expand Down
34 changes: 17 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,34 +34,34 @@
},
"dependencies": {
"@dhis2/app-runtime": "^3.14.1",
"@hookform/resolvers": "^3.3.4",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@hookform/resolvers": "^5.4.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-query-devtools": "^v4",
"@types/react-beautiful-dnd": "^13.1.8",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.364.0",
"next-themes": "^0.3.0",
"next-themes": "^0.4.6",
"react-beautiful-dnd": "^13.1.1",
"react-dom": "^16",
"react-hook-form": "^7.51.2",
"react-resizable-panels": "^2.0.17",
"react-hook-form": "^7.77.0",
"react-resizable-panels": "^4.11.2",
"react-router-dom": "6.3.0",
"sonner": "^1.4.41",
"sonner": "^2.0.7",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"typescript": "*",
"zod": "^3.22.4"
"zod": "^4.4.3"
}
}
4 changes: 2 additions & 2 deletions src/components/DataStoreKeyProvider/DataStoreKeyProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ export const DataStoreKeyProvider = ({ children }: Props) => {

if (!keysAreValid) {
return (
<div className={'flex flex-col min-h-[calc(100vh_-_48px)] pt-4 bg-muted/40'}>
<div className={'w-3/4 mt-8 flex flex-col gap-4 border mx-auto sm:mt-0 sm:w-1/4 px-4 py-6'}>
<div className={'flex min-h-[calc(100vh_-_48px)] flex-col bg-muted/40 px-4 pt-8 sm:pt-4'}>
<div className={'mx-auto flex w-full max-w-lg flex-col gap-4 border bg-background px-6 py-6 shadow-sm'}>
<h1 className={'text-lg font-bold'}>{i18n.t('Data store keys missing')}</h1>
<p>{i18n.t('It seems like you are missing some required data store keys.')}</p>
<p>{i18n.t('Do you want to create them?')}</p>
Expand Down
51 changes: 41 additions & 10 deletions src/components/MobileNavigation/MobileNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
import React from "react";
import {Sheet, SheetContent, SheetTrigger} from "../ui/sheet";
import {Sheet, SheetClose, SheetContent, SheetTrigger} from "../ui/sheet";
import {Button} from "../ui/button";
import {HomeIcon, Package2Icon, PanelLeftIcon} from "lucide-react";
import {BoxSelectIcon, ClipboardPenLineIcon, Package2Icon, PanelLeftIcon} from "lucide-react";
import {useLocation, useNavigate} from "react-router-dom";

const navigation = [
{
label: "Dashboard",
icon: Package2Icon,
path: "/",
activePath: ""
},
{
label: "Form Field Plugin",
icon: ClipboardPenLineIcon,
path: "/formField",
activePath: "formField"
},
{
label: "Enrollment Overview Plugin",
icon: BoxSelectIcon,
path: "/enrollmentOverview",
activePath: "enrollmentOverview"
}
]

export const MobileNavigation = () => {
const navigate = useNavigate();
const location = useLocation();

return (
<header className="sticky top-0 z-30 flex h-14 items-center gap-4 border-b bg-background px-4 sm:static sm:h-auto sm:border-0 sm:bg-transparent sm:px-6">
<Sheet>
Expand All @@ -15,17 +40,23 @@ export const MobileNavigation = () => {
</SheetTrigger>
<SheetContent className="sm:max-w-xs" side="left">
<nav className="grid gap-6 text-lg font-medium">
<a
<div
className="group flex h-10 w-10 shrink-0 items-center justify-center gap-2 rounded-full bg-primary text-lg font-semibold text-primary-foreground md:text-base"
href="#"
>
<Package2Icon className="h-5 w-5 transition-all group-hover:scale-110" />
<span className="sr-only">Acme Inc</span>
</a>
<a className="flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground" href="#">
<HomeIcon className="h-5 w-5" />
Dashboard
</a>
<span className="sr-only">Tracker Plugin Configurator</span>
</div>
{navigation.map(({ label, icon: Icon, path, activePath }) => (
<SheetClose asChild key={label}>
<button
className={`flex items-center gap-4 rounded-md px-2.5 py-2 text-left text-muted-foreground transition-colors hover:text-foreground ${location.pathname.split('/')[1] === activePath ? "bg-accent text-accent-foreground" : ""}`}
onClick={() => navigate(path)}
>
<Icon className="h-5 w-5" />
{label}
</button>
</SheetClose>
))}
</nav>
</SheetContent>
</Sheet>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Pages/Dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const Dashboard = () => {
const navigate = useNavigate();

return (
<div className={'w-3/4 mt-4 flex flex-col gap-4 border mx-auto sm:mt-0 sm:w-1/3 px-4 py-6'}>
<div className={'mx-auto mt-4 flex w-[calc(100%_-_2rem)] max-w-xl flex-col gap-4 border bg-background px-6 py-6 shadow-sm sm:mt-0'}>
<div>
<h1 className="text-2xl font-semibold text-gray-900 dark:text-gray-50">{i18n.t('Dashboard')}</h1>
<p className="text-gray-500 mt-1 dark:text-gray-400">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useMemo, useState } from "react";
import i18n from "@dhis2/d2-i18n";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "../../../../ui/sheet";
import { PlusCircleIcon } from "lucide-react";
Expand All @@ -22,12 +22,45 @@ type Props = {
page: 'overview' | 'newEvent' | 'editEvent';
}

const shouldMockLongSidebar = () => {
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const queryString = window.location.hash.split('?')[1] ?? '';
return isLocalhost && new URLSearchParams(queryString).get('mockLongSidebar') === 'true';
}

export const AddComponent = ({ columnName, availablePlugins, availableWidgets, allPlugins, page }: Props) => {
const [open, setOpen] = useState(false);
const {
setValue,
getValues,
} = useFormContext<z.infer<typeof ApiDataStoreInfoPerProgram>>();
const mockLongSidebar = shouldMockLongSidebar();
const { visiblePlugins, visibleWidgets } = useMemo(() => {
if (!mockLongSidebar) {
return {
visiblePlugins: availablePlugins,
visibleWidgets: availableWidgets,
}
}

const pluginSource = availablePlugins.length > 0 ? availablePlugins : allPlugins;
const widgetSource = availableWidgets.length > 0
? availableWidgets
: Object.values(Widgets).filter(widget => widget.allowedPages.includes(page));

return {
visiblePlugins: Array.from({ length: 16 }).flatMap((_, groupIndex) => pluginSource.map(plugin => ({
...plugin,
id: `${plugin.id}-mock-${groupIndex}`,
displayName: `${plugin.displayName} ${groupIndex + 1}`,
pluginLaunchUrl: `${plugin.pluginLaunchUrl}#mock-${groupIndex}`,
}))),
visibleWidgets: Array.from({ length: 12 }).flatMap((_, groupIndex) => widgetSource.map(widget => ({
...widget,
title: `${widget.title} ${groupIndex + 1}`,
}))),
}
}, [allPlugins, availablePlugins, availableWidgets, mockLongSidebar, page]);

const handleLocalPluginSubmit = (pluginUrl: string) => {
const pluginMetadata = {
Expand Down Expand Up @@ -95,24 +128,27 @@ export const AddComponent = ({ columnName, availablePlugins, availableWidgets, a
onOpenChange={setOpen}
>
<SheetTrigger asChild>
<button className={'border w-full border-dashed border-gray-200 py-6 flex flex-col gap-2 justify-center items-center cursor-pointer hover:border-gray-300'}>
<button
aria-label={i18n.t('Add widget')}
className={'border w-full border-dashed border-gray-200 py-6 flex flex-col gap-2 justify-center items-center cursor-pointer hover:border-gray-300'}
>
<PlusCircleIcon className={'h-8 w-8 mx-auto text-gray-400'} />
<p className={'text-center text-gray-600'}>
{i18n.t('Add')}
</p>
</button>
</SheetTrigger>

<SheetContent>
<ScrollArea className={'h-full mr-4 pr-4'}>
<SheetHeader>
<SheetTitle>{i18n.t('Add widget')}</SheetTitle>
<SheetDescription>
{i18n.t('Add a widget from the list below to add it to the column')}
</SheetDescription>
</SheetHeader>
<SheetContent className="flex h-dvh max-h-dvh flex-col overflow-hidden p-0 sm:max-w-md">
<SheetHeader className="shrink-0 border-b px-6 pb-4 pr-12 pt-6">
<SheetTitle>{i18n.t('Add widget')}</SheetTitle>
<SheetDescription>
{i18n.t('Add a widget from the list below to add it to the column')}
</SheetDescription>
</SheetHeader>

<div className={'mt-4'}>
<ScrollArea className={'min-h-0 flex-1'}>
<div className={'px-6 py-4 pr-8'}>
<h2 className={'my-2'}>{i18n.t('Add Local Plugin')}</h2>
<Separator className={'mb-4'} />

Expand All @@ -122,7 +158,7 @@ export const AddComponent = ({ columnName, availablePlugins, availableWidgets, a
<Separator className={'mb-4'} />


{availablePlugins.length ? availablePlugins.map(plugin => (
{visiblePlugins.length ? visiblePlugins.map(plugin => (
<div key={plugin.id} className={'border space-y-4 p-4 rounded mt-2 bg-white'}>
<div className={'space-y-1'}>
<p>{plugin.displayName}</p>
Expand All @@ -142,8 +178,8 @@ export const AddComponent = ({ columnName, availablePlugins, availableWidgets, a

<h2 className={'mt-4 mb-2'}>{i18n.t('Widgets')}</h2>
<Separator className={'mb-4'} />
{availableWidgets.length > 0 ? availableWidgets.map(widget => (
<div key={widget.name} className={'border p-4 space-y-4 rounded mt-2 bg-white'}>
{visibleWidgets.length > 0 ? visibleWidgets.map((widget, index) => (
<div key={`${widget.name}-${index}`} className={'border p-4 space-y-4 rounded mt-2 bg-white'}>
<p>{widget.title}</p>
<Button
onClick={() => addComponentToColumn(widget.name)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,32 @@ export const LocalPluginForm = ({ onSubmit }: Props) => {

return (
<div className={'border p-4 rounded mt-2 bg-white'}>
<div className="space-y-2">
<Label htmlFor="localPluginUrl">
{i18n.t('Plugin Launch URL')}
</Label>
<div className="flex gap-2">
<Input
id="localPluginUrl"
placeholder={i18n.t('Enter local plugin URL')}
{...form.register('localPluginUrl')}
className="flex-grow"
/>
<Button
type="button"
onClick={form.handleSubmit(handleSubmit)}
disabled={!form.formState.isValid}
>
{i18n.t('Add')}
</Button>
</div>
{form.formState.errors.localPluginUrl && (
<p className="text-sm text-red-500">
{form.formState.errors.localPluginUrl.message}
</p>
)}
<div className="space-y-2">
<Label htmlFor="localPluginUrl">
{i18n.t('Plugin Launch URL')}
</Label>
<div className="flex gap-2">
<Input
id="localPluginUrl"
placeholder={i18n.t('Enter local plugin URL')}
{...form.register('localPluginUrl')}
className="min-w-0 flex-1"
/>
<Button
type="button"
onClick={form.handleSubmit(handleSubmit)}
disabled={!form.formState.isValid}
className="shrink-0"
>
{i18n.t('Add')}
</Button>
</div>
{form.formState.errors.localPluginUrl && (
<p className="text-sm text-red-500">
{form.formState.errors.localPluginUrl.message}
</p>
)}
</div>
</div>
);
};
Loading
Loading