diff --git a/frontend/core-ui/src/modules/app/components/SettingsRoutes.tsx b/frontend/core-ui/src/modules/app/components/SettingsRoutes.tsx index aaeb2bebd8..b118cd17ce 100644 --- a/frontend/core-ui/src/modules/app/components/SettingsRoutes.tsx +++ b/frontend/core-ui/src/modules/app/components/SettingsRoutes.tsx @@ -8,6 +8,8 @@ import { SettingsWorkspacePath, } from '@/types/paths/SettingsPath'; import { Skeleton } from 'erxes-ui'; +import { currentOrganizationState } from 'ui-modules'; +import { useAtomValue } from 'jotai'; const SettingsProfile = lazy(() => import('~/pages/settings/account/ProfilePage').then((module) => ({ @@ -96,6 +98,8 @@ const PropertiesSettins = lazy(() => ); export function SettingsRoutes() { + const currentOrganization = useAtomValue(currentOrganizationState); + const isOs = currentOrganization?.type === 'os'; return ( }> @@ -104,6 +108,10 @@ export function SettingsRoutes() { element={} /> } /> + {/* } + /> */} } @@ -112,14 +120,18 @@ export function SettingsRoutes() { path={SettingsPath.Experience} element={} /> */} - } - /> - } - /> + {isOs && ( + } + /> + )} + {isOs && ( + } + /> + )} {/* } @@ -149,7 +161,10 @@ export function SettingsRoutes() { path={SettingsWorkspacePath.AutomationsCatchAll} element={} /> - } /> + } + /> } diff --git a/frontend/core-ui/src/modules/settings/apps/components/AppsHeader.tsx b/frontend/core-ui/src/modules/settings/apps/components/AppsHeader.tsx index 866ac40b97..286208ee39 100644 --- a/frontend/core-ui/src/modules/settings/apps/components/AppsHeader.tsx +++ b/frontend/core-ui/src/modules/settings/apps/components/AppsHeader.tsx @@ -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 ( @@ -12,17 +13,31 @@ export function AppsHeader() { + {pathname.includes('/create-new-app') && ( + <> + + + + + + )} - <> - {/* You can add any additional components or buttons here */} + ); diff --git a/frontend/core-ui/src/modules/settings/apps/components/AppsSettings.tsx b/frontend/core-ui/src/modules/settings/apps/components/AppsSettings.tsx index af743766fe..c8302c365b 100644 --- a/frontend/core-ui/src/modules/settings/apps/components/AppsSettings.tsx +++ b/frontend/core-ui/src/modules/settings/apps/components/AppsSettings.tsx @@ -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 ( - <> - - +
+ Apps settings + + + + + + + {loading && } + + + +
); }; diff --git a/frontend/core-ui/src/modules/settings/apps/components/CreateToken.tsx b/frontend/core-ui/src/modules/settings/apps/components/CreateToken.tsx new file mode 100644 index 0000000000..ce180ff529 --- /dev/null +++ b/frontend/core-ui/src/modules/settings/apps/components/CreateToken.tsx @@ -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, + DatePicker, + Form, + Input, + Spinner, + Switch, + toast, +} from 'erxes-ui'; +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +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' }), + }); + }; + + return ( +
+ +
+ + + New token + + ( + + Name + + + + + + )} + /> + + ( + + Allow all + + + + + + )} + /> + {(!allowAllPermission && ( + ( + + User group + + + + + + )} + /> + )) || + null} + + + ( + + No expire + + + + + + )} + /> + {(!noExpire && ( + ( + + Expire date + + + + + + )} + /> + )) || + null} + +
+
+ +
+
+ + ); +}; diff --git a/frontend/core-ui/src/modules/settings/apps/components/table/AppsSettingsColumns.tsx b/frontend/core-ui/src/modules/settings/apps/components/table/AppsSettingsColumns.tsx index 04f5f091d7..0961bd96a7 100644 --- a/frontend/core-ui/src/modules/settings/apps/components/table/AppsSettingsColumns.tsx +++ b/frontend/core-ui/src/modules/settings/apps/components/table/AppsSettingsColumns.tsx @@ -1,42 +1,108 @@ +import useRemoveToken from '@/settings/apps/hooks/useRemoveToken'; import { IApp } from '@/settings/apps/types'; -import { ColumnDef } from '@tanstack/table-core'; +import { IconCopy, IconTrash } from '@tabler/icons-react'; +import { Cell, ColumnDef } from '@tanstack/table-core'; import { format } from 'date-fns'; -import { RecordTableInlineCell } from 'erxes-ui'; +import { Button, RecordTableInlineCell, toast, useConfirm } from 'erxes-ui'; + +const RemoveButton = ({ cell }: { cell: Cell }) => { + const { _id, name } = cell.row.original; + const { appsRemove } = useRemoveToken(); + const { confirm } = useConfirm(); + const confirmOptions = { confirmationValue: 'delete' }; + + return ( + + ); +}; + +const CopyTokenButton = ({ cell }: { cell: Cell }) => { + const { accessToken } = cell.row.original; + async function copy() { + if (typeof navigator !== 'undefined' && navigator.clipboard) { + try { + await navigator.clipboard.writeText(accessToken); + toast({ + title: 'Token copied to clipboard', + }); + } catch { + toast({ + title: 'Failed to copy token', + variant: 'destructive', + }); + } + } + } + return ( + + ); +}; export const appsSettingsColumns: ColumnDef[] = [ { id: 'name', accessorKey: 'name', header: 'App Name', - cell: ({ cell }) => ( - {cell.getValue() as string} - ), - }, - { - id: 'clientId', - accessorKey: 'clientId', - header: 'Client ID', - cell: ({ cell }) => ( - {cell.getValue() as string} - ), + cell: ({ cell }) => { + const { name } = cell.row.original; + return {name ?? ''}; + }, + size: 250, }, { - id: 'clientSecret', - accessorKey: 'clientSecret', - header: 'Client Secret', - cell: ({ cell }) => ( - {cell.getValue() as string} - ), + id: 'expireDate', + accessorKey: 'expireDate', + header: 'Expiration', + cell: ({ cell }) => { + const { expireDate } = cell.row.original; + return ( + + {format(expireDate, 'yyyy-MM-dd')} + + ); + }, }, { - id: 'createdAt', - accessorKey: 'createdAt', - header: 'Created At', - cell: ({ cell }) => ( - - {format(new Date(cell.getValue() as string), 'yyyy/MM/dd') || - 'YYYY/MM/DD'} - - ), + id: 'actions', + cell: ({ cell }) => { + return ( + + + + + ); + }, + size: 100, }, ]; diff --git a/frontend/core-ui/src/modules/settings/apps/graphql/index.ts b/frontend/core-ui/src/modules/settings/apps/graphql/index.ts new file mode 100644 index 0000000000..3cf1ef310b --- /dev/null +++ b/frontend/core-ui/src/modules/settings/apps/graphql/index.ts @@ -0,0 +1 @@ +export * from './queries'; diff --git a/frontend/core-ui/src/modules/settings/apps/graphql/mutations/addToken.ts b/frontend/core-ui/src/modules/settings/apps/graphql/mutations/addToken.ts new file mode 100644 index 0000000000..e2977cdacd --- /dev/null +++ b/frontend/core-ui/src/modules/settings/apps/graphql/mutations/addToken.ts @@ -0,0 +1,32 @@ +import { gql } from '@apollo/client'; + +const ADD_TOKEN = gql` + mutation AppsAdd( + $name: String + $userGroupId: String + $expireDate: Date + $allowAllPermission: Boolean + $noExpire: Boolean + ) { + appsAdd( + name: $name + userGroupId: $userGroupId + expireDate: $expireDate + allowAllPermission: $allowAllPermission + noExpire: $noExpire + ) { + _id + accessToken + allowAllPermission + createdAt + expireDate + isEnabled + name + noExpire + refreshToken + userGroupId + } + } +`; + +export { ADD_TOKEN }; diff --git a/frontend/core-ui/src/modules/settings/apps/graphql/mutations/index.ts b/frontend/core-ui/src/modules/settings/apps/graphql/mutations/index.ts new file mode 100644 index 0000000000..d0a40dc069 --- /dev/null +++ b/frontend/core-ui/src/modules/settings/apps/graphql/mutations/index.ts @@ -0,0 +1,2 @@ +export * from './addToken'; +export * from './removeToken'; diff --git a/frontend/core-ui/src/modules/settings/apps/graphql/mutations/removeToken.ts b/frontend/core-ui/src/modules/settings/apps/graphql/mutations/removeToken.ts new file mode 100644 index 0000000000..8d2d4fa2f6 --- /dev/null +++ b/frontend/core-ui/src/modules/settings/apps/graphql/mutations/removeToken.ts @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; + +const REMOVE_TOKEN = gql` + mutation AppsRemove($_id: String!) { + appsRemove(_id: $_id) + } +`; + +export { REMOVE_TOKEN }; diff --git a/frontend/core-ui/src/modules/settings/apps/graphql/queries/getApps.ts b/frontend/core-ui/src/modules/settings/apps/graphql/queries/getApps.ts new file mode 100644 index 0000000000..b69112c017 --- /dev/null +++ b/frontend/core-ui/src/modules/settings/apps/graphql/queries/getApps.ts @@ -0,0 +1,19 @@ +import { gql } from '@apollo/client'; + +const GET_APPS = gql` + query Apps { + apps { + _id + accessToken + allowAllPermission + createdAt + expireDate + isEnabled + name + noExpire + userGroupId + } + } +`; + +export { GET_APPS }; diff --git a/frontend/core-ui/src/modules/settings/apps/graphql/queries/index.ts b/frontend/core-ui/src/modules/settings/apps/graphql/queries/index.ts new file mode 100644 index 0000000000..d8b69e8119 --- /dev/null +++ b/frontend/core-ui/src/modules/settings/apps/graphql/queries/index.ts @@ -0,0 +1 @@ +export * from './getApps'; diff --git a/frontend/core-ui/src/modules/settings/apps/hooks/useAddAppToken.tsx b/frontend/core-ui/src/modules/settings/apps/hooks/useAddAppToken.tsx new file mode 100644 index 0000000000..7d16b7113a --- /dev/null +++ b/frontend/core-ui/src/modules/settings/apps/hooks/useAddAppToken.tsx @@ -0,0 +1,16 @@ +import { ADD_TOKEN } from '@/settings/apps/graphql/mutations'; +import { IApp } from '@/settings/apps/types'; +import { MutationFunctionOptions, useMutation } from '@apollo/client'; + +interface IResult { + appsAdd: IApp; +} + +export const useAddAppToken = () => { + const [mutate, { loading, error }] = useMutation(ADD_TOKEN); + return { + appsAdd: mutate, + loading, + error, + }; +}; diff --git a/frontend/core-ui/src/modules/settings/apps/hooks/useAppsTokens.tsx b/frontend/core-ui/src/modules/settings/apps/hooks/useAppsTokens.tsx new file mode 100644 index 0000000000..70069c0736 --- /dev/null +++ b/frontend/core-ui/src/modules/settings/apps/hooks/useAppsTokens.tsx @@ -0,0 +1,19 @@ +import { GET_APPS } from '@/settings/apps/graphql'; +import { IApp } from '@/settings/apps/types'; +import { QueryHookOptions, useQuery } from '@apollo/client'; + +interface IApssResponse { + apps: IApp[]; +} + +export const useAppsTokens = (options?: QueryHookOptions) => { + const { data, error, loading } = useQuery(GET_APPS, { + ...options, + }); + const apps = data?.apps || []; + return { + apps, + loading, + error, + }; +}; diff --git a/frontend/core-ui/src/modules/settings/apps/hooks/useCreateAppForm.tsx b/frontend/core-ui/src/modules/settings/apps/hooks/useCreateAppForm.tsx new file mode 100644 index 0000000000..f58eda97a9 --- /dev/null +++ b/frontend/core-ui/src/modules/settings/apps/hooks/useCreateAppForm.tsx @@ -0,0 +1,21 @@ +import { CREATE_TOKEN_SCHEMA } from '@/settings/apps/schema'; +import { TCreateAppForm } from '@/settings/apps/types'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; + +export const useCreateAppForm = () => { + const form = useForm({ + mode: 'onBlur', + defaultValues: { + name: '', + noExpire: false, + allowAllPermission: false, + expireDate: new Date(), + userGroupId: '', + }, + resolver: zodResolver(CREATE_TOKEN_SCHEMA), + }); + return { + methods: form, + }; +}; diff --git a/frontend/core-ui/src/modules/settings/apps/hooks/useRemoveToken.tsx b/frontend/core-ui/src/modules/settings/apps/hooks/useRemoveToken.tsx new file mode 100644 index 0000000000..daa67a89a9 --- /dev/null +++ b/frontend/core-ui/src/modules/settings/apps/hooks/useRemoveToken.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { MutationFunctionOptions, useMutation } from '@apollo/client'; +import { REMOVE_TOKEN } from '../graphql/mutations'; +import { toast } from 'erxes-ui'; + +interface IRemoveResults { + appsRemove: boolean; +} + +const useRemoveToken = () => { + const [mutate, { loading, error }] = + useMutation(REMOVE_TOKEN); + + const handleRemoveToken = ( + options: MutationFunctionOptions, + ) => { + return mutate({ + ...options, + onError: (error) => { + toast({ + title: 'Error', + description: error.message, + variant: 'destructive', + }); + }, + refetchQueries: ['Apps'], + }); + }; + return { + appsRemove: handleRemoveToken, + loading, + error, + }; +}; + +export default useRemoveToken; diff --git a/frontend/core-ui/src/modules/settings/apps/schema.ts b/frontend/core-ui/src/modules/settings/apps/schema.ts new file mode 100644 index 0000000000..ecee6ed7b2 --- /dev/null +++ b/frontend/core-ui/src/modules/settings/apps/schema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +export const CREATE_TOKEN_SCHEMA = z.object({ + name: z + .string() + .trim() + .min(1, 'Name is required') + .max(100, 'Name must be ≤ 100 chars'), + userGroupId: z.string().trim().optional(), + expireDate: z.coerce.date().optional(), + allowAllPermission: z.boolean().default(false), + noExpire: z.boolean().default(false), +}); diff --git a/frontend/core-ui/src/modules/settings/apps/types.ts b/frontend/core-ui/src/modules/settings/apps/types.ts index 6986f8e549..8f833bb1cb 100644 --- a/frontend/core-ui/src/modules/settings/apps/types.ts +++ b/frontend/core-ui/src/modules/settings/apps/types.ts @@ -1,7 +1,17 @@ +import { CREATE_TOKEN_SCHEMA } from '@/settings/apps/schema'; +import { z } from 'zod'; + export interface IApp { _id: string; - clientId: string; - clientSecret: string; + accessToken: string; + allowAllPermission: boolean; + createdAt: Date; + expireDate: Date; + isEnabled: boolean; name: string; - createdAt: string; + noExpire: boolean; + refreshToken: string; + userGroupId: string; } + +export type TCreateAppForm = z.infer; diff --git a/frontend/core-ui/src/modules/settings/components/SettingsSidebar.tsx b/frontend/core-ui/src/modules/settings/components/SettingsSidebar.tsx index 33530a33a2..aef365d986 100644 --- a/frontend/core-ui/src/modules/settings/components/SettingsSidebar.tsx +++ b/frontend/core-ui/src/modules/settings/components/SettingsSidebar.tsx @@ -6,16 +6,20 @@ import { Sidebar, IUIConfig, NavigationMenuLinkItem } from 'erxes-ui'; import { AppPath } from '@/types/paths/AppPath'; import { CORE_MODULES } from '~/plugins/constants/core-plugins.constants'; -import { pluginsConfigState } from 'ui-modules'; +import { pluginsConfigState, currentOrganizationState } from 'ui-modules'; import { useAtomValue } from 'jotai'; import { SETTINGS_PATH_DATA } from '../constants/data'; import { useMemo } from 'react'; import { usePageTrackerStore } from 'react-page-tracker'; +import { SettingsWorkspacePath } from '@/types/paths/SettingsPath'; export function SettingsSidebar() { const pluginsMetaData = useAtomValue(pluginsConfigState) || {}; + const currentOrganization = useAtomValue(currentOrganizationState); + const isOs = currentOrganization?.type === 'os'; + const pluginsWithSettingsModules: Map = useMemo(() => { if (pluginsMetaData) { @@ -54,14 +58,22 @@ export function SettingsSidebar() { ))} - {SETTINGS_PATH_DATA.nav.map((item) => ( - - ))} + {SETTINGS_PATH_DATA.nav + .filter((item) => { + const isRestricted = + item.path === SettingsWorkspacePath.FileUpload || + item.path === SettingsWorkspacePath.MailConfig; + return isOs || !isRestricted; + }) + .map((item) => ( + + + + ))} diff --git a/frontend/core-ui/src/modules/settings/constants/data.ts b/frontend/core-ui/src/modules/settings/constants/data.ts index fa0ca57122..e21d26a40e 100644 --- a/frontend/core-ui/src/modules/settings/constants/data.ts +++ b/frontend/core-ui/src/modules/settings/constants/data.ts @@ -9,6 +9,7 @@ import { IconMail, IconPassword, IconTag, + IconApi, IconUserCircle, IconUserCog, IconUsersGroup, @@ -118,6 +119,11 @@ export const SETTINGS_PATH_DATA: { [key: string]: TSettingPath[] } = { // icon: IconColorSwatch, // path: SettingsPath.Experience, // }, + // { + // name: 'Notification', + // icon: IconBellRinging, + // path: SettingsPath.Notification, + // }, ], nav: [ { @@ -161,5 +167,10 @@ export const SETTINGS_PATH_DATA: { [key: string]: TSettingPath[] } = { icon: IconChessKnight, path: SettingsWorkspacePath.Brands, }, + { + name: 'Apps', + icon: IconApi, + path: SettingsWorkspacePath.Apps, + }, ], }; diff --git a/frontend/core-ui/src/modules/types/paths/SettingsPath.ts b/frontend/core-ui/src/modules/types/paths/SettingsPath.ts index 24419ff1f1..89a54594f6 100644 --- a/frontend/core-ui/src/modules/types/paths/SettingsPath.ts +++ b/frontend/core-ui/src/modules/types/paths/SettingsPath.ts @@ -12,6 +12,7 @@ export enum SettingsWorkspacePath { FileUpload = 'file-upload', MailConfig = 'mail-config', Apps = 'apps', + AppsCatchAll = 'apps/*', Permissions = 'permissions', Properties = 'properties', TeamMember = 'team-member', diff --git a/frontend/core-ui/src/pages/settings/workspace/AppSettingsPage.tsx b/frontend/core-ui/src/pages/settings/workspace/AppSettingsPage.tsx index 160d574fcb..88877c874a 100644 --- a/frontend/core-ui/src/pages/settings/workspace/AppSettingsPage.tsx +++ b/frontend/core-ui/src/pages/settings/workspace/AppSettingsPage.tsx @@ -1,10 +1,26 @@ +import { AppsHeader } from '@/settings/apps/components/AppsHeader'; import { AppsSettings } from '@/settings/apps/components/AppsSettings'; -import { PageContainer } from 'erxes-ui'; +import { CreateToken } from '@/settings/apps/components/CreateToken'; +import { PageContainer, Spinner } from 'erxes-ui'; +import { Suspense } from 'react'; +import { Route, Routes } from 'react-router-dom'; export function AppSettingsPage() { return ( - + + + + + } + > + + } /> + } /> + + ); } diff --git a/frontend/libs/ui-modules/src/modules/structure/components/SelectPositions.tsx b/frontend/libs/ui-modules/src/modules/structure/components/SelectPositions.tsx index 0774f07417..4019aa3256 100644 --- a/frontend/libs/ui-modules/src/modules/structure/components/SelectPositions.tsx +++ b/frontend/libs/ui-modules/src/modules/structure/components/SelectPositions.tsx @@ -317,7 +317,7 @@ const SelectPositionsBadgesView = () => { { if (!position) return; if (positionIds.includes(position._id)) { diff --git a/frontend/libs/ui-modules/src/states/currentOrganizationState.ts b/frontend/libs/ui-modules/src/states/currentOrganizationState.ts index 5990bb537f..e7db123dca 100644 --- a/frontend/libs/ui-modules/src/states/currentOrganizationState.ts +++ b/frontend/libs/ui-modules/src/states/currentOrganizationState.ts @@ -12,6 +12,7 @@ export type CurrentOrganization = { name: string; url: string; }[]; + type?: string; }; export const currentOrganizationState = atom(null);