Browse Source

feat(dashboard): Country detail view

Michael Bromley 10 months ago
parent
commit
f33f7a156e

+ 38 - 3
packages/dashboard/src/routes/_authenticated/_countries/countries.graphql.ts

@@ -1,5 +1,4 @@
 import { graphql } from '@/graphql/graphql.js';
-import { gql } from 'graphql-tag';
 
 export const countryItemFragment = graphql(`
     fragment CountryItem on Country {
@@ -7,13 +6,14 @@ export const countryItemFragment = graphql(`
         name
         code
         enabled
+        customFields
     }
 `);
 
 export const countriesListQuery = graphql(
     `
-        query CountriesList {
-            countries {
+        query CountriesList($options: CountryListOptions) {
+            countries(options: $options) {
                 items {
                     ...CountryItem
                 }
@@ -23,3 +23,38 @@ export const countriesListQuery = graphql(
     `,
     [countryItemFragment],
 );
+
+export const countryDetailQuery = graphql(`
+    query CountryDetail($id: ID!) {
+        country(id: $id) {
+            id
+            name
+            code
+            enabled
+            translations {
+                id
+                createdAt
+                updatedAt
+                languageCode
+                name
+            }
+            customFields
+        }
+    }
+`);
+
+export const createCountryDocument = graphql(`
+    mutation CreateCountry($input: CreateCountryInput!) {
+        createCountry(input: $input) {
+            id
+        }
+    }
+`);
+
+export const updateCountryDocument = graphql(`
+    mutation UpdateCountry($input: UpdateCountryInput!) {
+        updateCountry(input: $input) {
+            id
+        }
+    }
+`);

+ 16 - 14
packages/dashboard/src/routes/_authenticated/_countries/countries.tsx

@@ -1,15 +1,14 @@
+import { BooleanDisplayBadge } from '@/components/data-display/boolean.js';
+import { DetailPageButton } from '@/components/shared/detail-page-button.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+import { Button } from '@/components/ui/button.js';
 import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import { PageActionBar } from '@/framework/layout-engine/page-layout.js';
+import { ListPage } from '@/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { Link, createFileRoute } from '@tanstack/react-router';
-import { countriesListQuery } from './countries.graphql.js';
-import { DetailPageButton } from '@/components/shared/detail-page-button.js';
-import { ListPage } from '@/framework/page/list-page.js';
-import { Badge } from '@/components/ui/badge.js';
-import { PageActionBar } from '@/framework/layout-engine/page-layout.js';
-import { PermissionGuard } from '@/components/shared/permission-guard.js';
 import { PlusIcon } from 'lucide-react';
-import { Button } from '@/components/ui/button.js';
-import { BooleanDisplayBadge } from '@/components/data-display/boolean.js';
+import { countriesListQuery } from './countries.graphql.js';
 
 export const Route = createFileRoute('/_authenticated/_countries/countries')({
     component: CountryListPage,
@@ -37,17 +36,20 @@ function CountryListPage() {
                     },
                 };
             }}
+            transformVariables={variables => {
+                return {
+                    ...variables,
+                    options: {
+                        ...variables.options,
+                        filterOperator: 'OR',
+                    }
+                };
+            }}
             customizeColumns={{
                 name: {
                     header: 'Name',
                     cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
                 },
-                enabled: {
-                    header: 'Enabled',
-                    cell: ({ row }) => (
-                        <BooleanDisplayBadge value={row.original.enabled} />
-                    ),
-                },
             }}
         >
             <PageActionBar>

+ 170 - 0
packages/dashboard/src/routes/_authenticated/_countries/countries_.$id.tsx

@@ -0,0 +1,170 @@
+import { ErrorPage } from '@/components/shared/error-page.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+import { TranslatableFormField } from '@/components/shared/translatable-form-field.js';
+import { Button } from '@/components/ui/button.js';
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form.js';
+import { Input } from '@/components/ui/input.js';
+import { NEW_ENTITY_PATH } from '@/constants.js';
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import {
+    CustomFieldsPageBlock,
+    Page,
+    PageActionBar,
+    PageBlock,
+    PageLayout,
+    PageTitle,
+} from '@/framework/layout-engine/page-layout.js';
+import { getDetailQueryOptions, useDetailPage } from '@/framework/page/use-detail-page.js';
+import { Trans, useLingui } from '@lingui/react/macro';
+import { createFileRoute, useNavigate } from '@tanstack/react-router';
+import { toast } from 'sonner';
+import {
+    countryDetailQuery,
+    createCountryDocument,
+    updateCountryDocument,
+} from './countries.graphql.js';
+import { Switch } from '@/components/ui/switch.js';
+export const Route = createFileRoute('/_authenticated/_countries/countries_/$id')({
+    component: CountryDetailPage,
+    loader: async ({ context, params }) => {
+        const isNew = params.id === NEW_ENTITY_PATH;
+        const result = isNew
+            ? null
+            : await context.queryClient.ensureQueryData(
+                  getDetailQueryOptions(addCustomFields(countryDetailQuery), { id: params.id }),
+                  { id: params.id },
+              );
+        if (!isNew && !result.country) {
+            throw new Error(`Country with the ID ${params.id} was not found`);
+        }
+        return {
+            breadcrumb: [
+                { path: '/countries', label: 'Countries' },
+                    isNew ? <Trans>New country</Trans> : result.country.name,
+            ],
+        };
+    },
+    errorComponent: ({ error }) => <ErrorPage message={error.message} />,
+});
+
+export function CountryDetailPage() {
+    const params = Route.useParams();
+    const navigate = useNavigate();
+    const creatingNewEntity = params.id === NEW_ENTITY_PATH;
+    const { i18n } = useLingui();
+
+    const { form, submitHandler, entity, isPending } = useDetailPage({
+        queryDocument: addCustomFields(countryDetailQuery),
+        entityField: 'country',
+        createDocument: createCountryDocument,
+        updateDocument: updateCountryDocument,
+        setValuesForUpdate: entity => {
+            return {
+                id: entity.id,
+                name: entity.name,
+                code: entity.code,
+                enabled: entity.enabled,
+                translations: entity.translations,
+                customFields: entity.customFields,
+            };
+        },
+        params: { id: params.id },
+        onSuccess: async data => {
+            console.log('data', data);
+            toast(i18n.t('Successfully updated country'), {
+                position: 'top-right',
+            });
+            form.reset(form.getValues());
+            if (creatingNewEntity) {
+                await navigate({ to: `../${data?.id}`, from: Route.id });
+            }
+        },
+        onError: err => {
+            toast(i18n.t('Failed to update country'), {
+                position: 'top-right',
+                description: err instanceof Error ? err.message : 'Unknown error',
+            });
+        },
+    });
+
+    return (
+        <Page>
+            <PageTitle>
+                {creatingNewEntity ? <Trans>New country</Trans> : (entity?.name ?? '')}
+            </PageTitle>
+            <Form {...form}>
+                <form onSubmit={submitHandler} className="space-y-8">
+                    <PageActionBar>
+                        <div></div>
+                        <PermissionGuard requires={['UpdateCountry']}>
+                            <Button
+                                type="submit"
+                                disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                            >
+                                <Trans>Update</Trans>
+                            </Button>
+                        </PermissionGuard>
+                    </PageActionBar>
+                    <PageLayout>
+                        <PageBlock column='side'>
+                            <FormField
+                                control={form.control}
+                                name="enabled"
+                                render={({ field }) => (
+                                    <FormItem>
+                                        <FormLabel>
+                                            <Trans>Enabled</Trans>
+                                        </FormLabel>
+                                        <FormControl>
+                                            <Switch checked={field.value} onCheckedChange={field.onChange} />
+                                        </FormControl>
+                                        <FormMessage />
+                                    </FormItem>
+                                )}
+                            />
+                        </PageBlock>
+                        <PageBlock column="main">
+                            <div className="md:grid md:grid-cols-2 gap-4">
+                                    <TranslatableFormField
+                                        control={form.control}
+                                        name="name"
+                                        render={({ field }) => (
+                                            <FormItem>
+                                                <FormLabel>
+                                                    <Trans>Name</Trans>
+                                                </FormLabel>
+                                                <FormControl>
+                                                    <Input placeholder="" {...field} />
+                                                </FormControl>
+                                                <FormMessage />
+                                            </FormItem>
+                                        )}
+                                    />
+                                    <FormField
+                                        control={form.control}
+                                        name="code"
+                                        render={({ field }) => (
+                                            <FormItem>
+                                                <FormLabel>
+                                                    <Trans>Code</Trans>
+                                                </FormLabel>
+                                                <FormControl>
+                                                    <Input placeholder="" {...field} />
+                                                </FormControl>
+                                                <FormMessage />
+                                            </FormItem>
+                                        )}
+                                    />
+                            </div>
+                        </PageBlock>
+                        <CustomFieldsPageBlock
+                            column="main"
+                            entityType="Country"
+                            control={form.control}
+                        />
+                    </PageLayout>
+                </form>
+            </Form>
+        </Page>
+    );
+}