Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
33 changes: 22 additions & 11 deletions frontend/core-ui/src/modules/app/components/SettingsRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { SettingsExperiencePage } from '~/pages/settings/account/ExperiencePage'
import { getPluginsSettingsRoutes } from '@/app/hooks/usePluginsRouter';
import { Skeleton } from 'erxes-ui';
import { SettingsPageEffect } from '@/settings/components/SettingsPageEffect';
import { currentOrganizationState } from 'ui-modules';
import { useAtomValue } from 'jotai';

const SettingsProfile = lazy(() =>
import('~/pages/settings/account/ProfilePage').then((module) => ({
Expand Down Expand Up @@ -104,6 +106,8 @@ const PropertiesSettins = lazy(() =>
);

export function SettingsRoutes() {
const currentOrganization = useAtomValue(currentOrganizationState);
const isOs = currentOrganization?.type === 'os';
return (
<Suspense fallback={<Skeleton />}>
<Routes>
Expand All @@ -112,10 +116,10 @@ export function SettingsRoutes() {
element={<Navigate to={`${SettingsPath.Profile}`} replace />}
/>
<Route path={SettingsPath.Profile} element={<SettingsProfile />} />
<Route
{/* <Route
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove stale commented-out routes (e.g. Notification, Experience, General) if they are no longer needed to keep the code clean.

path={SettingsPath.Notification}
element={<NotificationSettingsRoutes />}
/>
/> */}
<Route
path={SettingsPath.ChangePassword}
element={<SettingsChangePassword />}
Expand All @@ -124,14 +128,18 @@ export function SettingsRoutes() {
path={SettingsPath.Experience}
element={<SettingsExperiencePage />}
/> */}
<Route
path={SettingsWorkspacePath.FileUpload}
element={<SettingsFileUpload />}
/>
<Route
path={SettingsWorkspacePath.MailConfig}
element={<SettingsMailConfig />}
/>
{isOs && (
<Route
path={SettingsWorkspacePath.FileUpload}
element={<SettingsFileUpload />}
/>
)}
{isOs && (
<Route
path={SettingsWorkspacePath.MailConfig}
element={<SettingsMailConfig />}
/>
)}
{/* <Route
path={SettingsWorkspacePath.General}
element={<GeneralSettings />}
Expand Down Expand Up @@ -161,7 +169,10 @@ export function SettingsRoutes() {
path={SettingsWorkspacePath.AutomationsCatchAll}
element={<AutomationSettingsRoutes />}
/>
<Route path={SettingsWorkspacePath.Apps} element={<AppsSettings />} />
<Route
path={SettingsWorkspacePath.AppsCatchAll}
element={<AppsSettings />}
/>
<Route
path={SettingsWorkspacePath.Properties}
element={<PropertiesSettins />}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { PageHeader, PageHeaderEnd, PageHeaderStart } from 'ui-modules';
import { Breadcrumb, Button } from 'erxes-ui';
import { Link } from 'react-router-dom';
import { IconShieldCog } from '@tabler/icons-react';
import { Link, useLocation } from 'react-router-dom';
import { IconApi, IconPlus } from '@tabler/icons-react';

export function AppsHeader() {
const { pathname } = useLocation();
return (
<PageHeader>
<PageHeaderStart>
Expand All @@ -12,17 +13,31 @@ export function AppsHeader() {
<Breadcrumb.Item>
<Button variant="ghost" asChild>
<Link to="/settings/apps">
<IconShieldCog />
<IconApi />
Apps
</Link>
</Button>
</Breadcrumb.Item>
{pathname.includes('/create-new-app') && (
<>
<Breadcrumb.Separator />
<Breadcrumb.Item>
<Button variant={'ghost'} asChild>
<Link to="/settings/apps/create-new-app">New token</Link>
</Button>
</Breadcrumb.Item>
</>
)}
</Breadcrumb.List>
</Breadcrumb>
</PageHeaderStart>
<PageHeaderEnd>
<></>
{/* You can add any additional components or buttons here */}
<Button asChild>
<Link to="/settings/apps/create-new-app">
<IconPlus />
Create App token
</Link>
</Button>
</PageHeaderEnd>
</PageHeader>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import { AppsHeader } from '@/settings/apps/components/AppsHeader';
import { appsSettingsColumns } from '@/settings/apps/components/table/AppsSettingsColumns';
import { useAppsTokens } from '@/settings/apps/hooks/useAppsTokens';
import { IApp } from '@/settings/apps/types';
import { RecordTable } from 'erxes-ui';

export const AppsSettings = () => {
const { apps, loading } = useAppsTokens();
return (
<>
<AppsHeader />
</>
<section className="max-w-xl w-full mx-auto">
<legend className="font-semibold text-lg pt-4 pb-6">Apps settings</legend>

<RecordTable.Provider
columns={appsSettingsColumns}
data={apps as IApp[]}
>
<RecordTable className='w-full'>
<RecordTable.Header />
<RecordTable.Body>
<RecordTable.RowList />
{loading && <RecordTable.RowSkeleton rows={30} />}
</RecordTable.Body>
</RecordTable>
</RecordTable.Provider>
</section>
);
};
159 changes: 159 additions & 0 deletions frontend/core-ui/src/modules/settings/apps/components/CreateToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { useAddAppToken } from '@/settings/apps/hooks/useAddAppToken';
import { useCreateAppForm } from '@/settings/apps/hooks/useCreateAppForm';
import { TCreateAppForm } from '@/settings/apps/types';
import { SettingsWorkspacePath } from '@/types/paths/SettingsPath';
import { IconChevronLeft, IconPlus } from '@tabler/icons-react';
import {
Button,
Checkbox,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the unused import 'Checkbox' to avoid unnecessary code clutter.

Suggested change
Checkbox,

cn,
DatePicker,
Form,
Input,
Spinner,
Switch,
toast,
} from 'erxes-ui';
import React from 'react';
import { useNavigate } from 'react-router';
import { SelectUsersGroup } from 'ui-modules';

export const CreateToken = () => {
const navigate = useNavigate();
const { methods } = useCreateAppForm();
const { control, handleSubmit, reset, watch } = methods;
const { appsAdd, loading } = useAddAppToken();

const [noExpire, allowAllPermission] = watch([
'noExpire',
'allowAllPermission',
]);

const onSubmit = (data: TCreateAppForm) => {
appsAdd({
variables: data,
onCompleted: () => {
toast({ title: 'Created a token' });
navigate(SettingsWorkspacePath.Apps);
reset();
},
onError: (error) =>
toast({ title: error.message, variant: 'destructive' }),
});
};
Comment on lines +30 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Normalize payload before mutation (trim, gate fields, ISO date)

Prevents sending empty userGroupId or expireDate when toggles disable them; avoids backend reject.

Apply:

   const onSubmit = (data: TCreateAppForm) => {
-    appsAdd({
-      variables: data,
+    const variables = {
+      ...data,
+      name: data.name.trim(),
+      userGroupId: data.allowAllPermission
+        ? undefined
+        : data.userGroupId?.trim() || undefined,
+      expireDate: data.noExpire
+        ? undefined
+        : data.expireDate?.toISOString(),
+    };
+    appsAdd({
+      variables,
       onCompleted: () => {
         toast({ title: 'Created a token' });
         navigate(SettingsWorkspacePath.Apps);
         reset();
       },
       onError: (error) =>
         toast({ title: error.message, variant: 'destructive' }),
     });
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const onSubmit = (data: TCreateAppForm) => {
appsAdd({
variables: data,
onCompleted: () => {
toast({ title: 'Created a token' });
navigate(SettingsWorkspacePath.Apps);
reset();
},
onError: (error) =>
toast({ title: error.message, variant: 'destructive' }),
});
};
const onSubmit = (data: TCreateAppForm) => {
const variables = {
...data,
name: data.name.trim(),
userGroupId: data.allowAllPermission
? undefined
: data.userGroupId?.trim() || undefined,
expireDate: data.noExpire
? undefined
: data.expireDate?.toISOString(),
};
appsAdd({
variables,
onCompleted: () => {
toast({ title: 'Created a token' });
navigate(SettingsWorkspacePath.Apps);
reset();
},
onError: (error) =>
toast({ title: error.message, variant: 'destructive' }),
});
};
🤖 Prompt for AI Agents
In frontend/core-ui/src/modules/settings/apps/components/CreateToken.tsx around
lines 30 to 41, the mutation is sending raw form data which can include empty or
disabled fields and non-ISO dates; normalize the payload before calling appsAdd
by trimming string fields (e.g., name, description), omitting userGroupId when
the corresponding toggle is off or the value is empty, omitting expireDate when
the expiration toggle is off, and converting any expireDate to an ISO string
(e.g., expireDate.toISOString()) before passing variables; then call appsAdd
with this sanitized payload so the backend doesn't receive empty/invalid fields.


return (
<Form {...methods}>
<form
onSubmit={handleSubmit(onSubmit)}
className="max-w-lg w-full mx-auto flex flex-col space-y-4"
>
<fieldset className="grid grid-cols-2 gap-3">
<legend className="font-semibold text-lg pt-4 pb-6 flex items-center gap-x-1">
<Button
variant={'ghost'}
size={'icon'}
onClick={() => navigate(-1)}
>
<IconChevronLeft />
</Button>
New token
</legend>
<Form.Field
control={control}
name="name"
render={({ field }) => (
<Form.Item className="col-span-2">
<Form.Label>Name</Form.Label>
<Form.Control>
<Input {...field} placeholder="Name" />
</Form.Control>
<Form.Message />
</Form.Item>
)}
/>
<span className="grid grid-cols-2 col-span-2">
<Form.Field
control={control}
name="allowAllPermission"
render={({ field }) => (
<Form.Item className="flex flex-wrap">
<Form.Label className="w-full">Allow all</Form.Label>
<Form.Control>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</Form.Control>
<Form.Message />
</Form.Item>
)}
/>
{!allowAllPermission && (
<Form.Field
control={control}
name="userGroupId"
render={({ field }) => (
<Form.Item>
<Form.Label>User group</Form.Label>
<Form.Control>
<SelectUsersGroup.FormItem
value={field.value}
mode="single"
onValueChange={field.onChange}
/>
</Form.Control>
<Form.Message />
</Form.Item>
)}
/>
)}
</span>
<span className="grid grid-cols-2 col-span-2">
<Form.Field
control={control}
name="noExpire"
render={({ field }) => (
<Form.Item className="flex flex-wrap">
<Form.Label className="w-full">No expire</Form.Label>
<Form.Control>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</Form.Control>
<Form.Message />
</Form.Item>
)}
/>
{!noExpire && (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Expire date field default value may be confusing.

Defaulting expireDate to the current date could cause confusion. Suggest prompting the user to choose an expiration date or setting a more meaningful default.

Suggested implementation:

  const { control, handleSubmit, formState } = useForm({
    defaultValues: {
      name: "",
      expireDate: undefined, // No default, user must choose
      // ...other defaults
    },
  });
            {!noExpire && (
              <Form.Field
                control={control}
                name="expireDate"
                render={({ field }) => (
                  <Form.Item className="col-span-2">
                    <Form.Label>Expiration Date</Form.Label>
                    <Form.Control>
                      <Input
                        {...field}
                        type="date"
                        placeholder="Select expiration date"
                        min={new Date().toISOString().split("T")[0]} // Prevent past dates
                      />
                    </Form.Control>
                    <Form.Message />
                  </Form.Item>
                )}
              />

If you have validation logic elsewhere that expects expireDate to always be set, you may need to update it to handle the case where the field is empty or undefined. Also, ensure that the UI clearly indicates that the user must select an expiration date.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick update!

Currently, the default value for expireDate in your form hook is still set to new Date(), which means the expiration date will default to today unless the user changes it. Your schema validation does require the user to set an expiration date when noExpire is false, and the UI uses a DatePicker that defaults to today if no value is set.

If you want to ensure users actively choose an expiration date (rather than accepting today's date by default), you could set the default to undefined and rely on your validation and UI to prompt the user. Otherwise, your current setup is functional and will prevent empty expiration dates when required.

Let me know if you'd like further suggestions or a more detailed review of the updated logic!

<Form.Field
control={control}
name="expireDate"
render={({ field }) => (
<Form.Item>
<Form.Label>Expire date</Form.Label>
<Form.Control>
<DatePicker
defaultMonth={field.value || new Date()}
value={field.value}
onChange={field.onChange}
/>
</Form.Control>
<Form.Message />
</Form.Item>
)}
/>
)}
</span>
</fieldset>
<div role="group" className="w-full flex items-center pt-6">
<Button type="submit" className="isolate ml-auto" disabled={loading}>
{(loading && <Spinner />) || <IconPlus />}
Add
</Button>
</div>
</form>
</Form>
);
};
Loading