Skip to content
Draft
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
9 changes: 9 additions & 0 deletions packages/design-system/src/components/Icon/mapping.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Brand logos belong here only as 16x16 glyphs; the plated versions live in the repo
// root's `src/public/images` — see `.agents/docs/icons-and-logos.md`.
import { ComponentProps } from 'react'

import Alphabet from '~/icons/alphabet.svg'
import Apps from '~/icons/apps.svg'
import ArrowBackUp from '~/icons/arrow-back-up.svg'
Expand Down Expand Up @@ -48,6 +50,7 @@ import ContentCenterAlign from '~/icons/content-center-align.svg'
import ContentJustifyAlign from '~/icons/content-justify-align.svg'
import ContentLeftAlign from '~/icons/content-left-align.svg'
import ContentRightAlign from '~/icons/content-right-align.svg'
import ContractAsset from '~/icons/contract.svg'
import Coupon from '~/icons/coupon.svg'
import Descending from '~/icons/descending.svg'
import Document from '~/icons/document.svg'
Expand Down Expand Up @@ -159,6 +162,11 @@ import Wallet from '~/icons/wallet.svg'
import WarningFilled from '~/icons/warning-filled.svg'
import WarningUnfilled from '~/icons/warning-unfilled.svg'
import WritingSign from '~/icons/writing-sign.svg'
import { tw } from '~/lib'

const Contract = (props: ComponentProps<typeof ContractAsset>): JSX.Element => (
<ContractAsset {...props} className={tw('lago-icon-contract', props.className)} />
)

export const ALL_ICONS = {
at: At,
Expand Down Expand Up @@ -205,6 +213,7 @@ export const ALL_ICONS = {
command: Command,
company: Company,
condition: Condition,
contract: Contract,
'content-center-align': ContentCenterAlign,
'content-justify-align': ContentJustifyAlign,
'content-left-align': ContentLeftAlign,
Expand Down
10 changes: 10 additions & 0 deletions packages/design-system/src/icons/contract.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions packages/design-system/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
@tailwind components;
@tailwind utilities;

.lago-icon-contract path {
fill: currentColor;
}

@layer base {
html,
body,
Expand Down
20 changes: 20 additions & 0 deletions src/core/router/ObjectsRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FeatureFlagEnum } from '~/generated/graphql'

import { CustomRouteObject } from './types'
import { lazyLoad } from './utils'

Expand All @@ -11,6 +13,7 @@ const InvoicesPage = lazyLoad(() => import('~/pages/InvoicesPage'))
const PaymentsPage = lazyLoad(() => import('~/pages/PaymentsPage'))
const CreditNotesPage = lazyLoad(() => import('~/pages/CreditNotesPage'))
const SubscriptionsPage = lazyLoad(() => import('~/pages/SubscriptionsPage'))
const ContractsPage = lazyLoad(() => import('~/pages/contracts/ContractsPage'))
const FeaturesList = lazyLoad(() => import('~/pages/features/FeaturesList'))

// Creation
Expand Down Expand Up @@ -46,6 +49,7 @@ const BillableMetricDetails = lazyLoad(() => import('~/pages/BillableMetricDetai
const FeatureDetails = lazyLoad(() => import('~/pages/features/FeatureDetails'))
const WalletDetails = lazyLoad(() => import('~/pages/wallet/WalletDetails'))
const WalletAlertForm = lazyLoad(() => import('~/pages/wallet/WalletAlertForm'))
const ContractDetails = lazyLoad(() => import('~/pages/contracts/ContractDetails'))

// ----------- Routes -----------
// Lists
Expand All @@ -57,6 +61,7 @@ export const INVOICES_ROUTE = '/invoices'
export const PAYMENTS_ROUTE = '/payments'
export const CREDIT_NOTES_ROUTE = '/credit-notes'
export const SUBSCRIPTIONS_ROUTE = '/subscriptions'
export const CONTRACTS_ROUTE = '/contracts'
export const FEATURES_ROUTE = '/features'

// Creation
Expand Down Expand Up @@ -147,6 +152,7 @@ export const PAYMENT_DETAILS_ROUTE = '/payment/:paymentId'
export const CUSTOMER_PAYMENT_DETAILS_ROUTE = '/customer/:customerId/payment/:paymentId'
export const BILLABLE_METRIC_DETAILS_ROUTE = '/billable-metric/:billableMetricId/:tab'
export const FEATURE_DETAILS_ROUTE = '/feature/:featureId/:tab'
export const CONTRACT_DETAILS_ROUTE = '/contracts/:id'

export const objectListRoutes: CustomRouteObject[] = [
{
Expand Down Expand Up @@ -191,6 +197,13 @@ export const objectListRoutes: CustomRouteObject[] = [
element: <CreditNotesPage />,
permissions: ['creditNotesView'],
},
{
path: [CONTRACTS_ROUTE],
private: true,
element: <ContractsPage />,
permissions: ['contractsView'],
featureFlag: FeatureFlagEnum.ProductCatalog,
},
{
path: [SUBSCRIPTIONS_ROUTE],
private: true,
Expand Down Expand Up @@ -339,6 +352,13 @@ export const objectCreationRoutes: CustomRouteObject[] = [
]

export const objectDetailsRoutes: CustomRouteObject[] = [
{
path: [CONTRACT_DETAILS_ROUTE],
private: true,
element: <ContractDetails />,
permissions: ['contractsView'],
featureFlag: FeatureFlagEnum.ProductCatalog,
},
{
path: [CUSTOMER_SUBSCRIPTION_DETAILS_ROUTE, PLAN_SUBSCRIPTION_DETAILS_ROUTE],
private: true,
Expand Down
75 changes: 75 additions & 0 deletions src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16581,6 +16581,16 @@ export type RateCardForListFragment = { __typename?: 'RateCard', id: string, nam

export type PropertiesForActiveRateFragment = { __typename?: 'Properties', amount?: string | null, rate?: string | null, packageSize?: any | null, graduatedRanges?: Array<{ __typename?: 'GraduatedRange', perUnitAmount: string }> | null, volumeRanges?: Array<{ __typename?: 'VolumeRange', perUnitAmount: string }> | null, graduatedPercentageRanges?: Array<{ __typename?: 'GraduatedPercentageRange', rate: string }> | null };

export type ContractForContractsListFragment = { __typename?: 'Contract', id: string, name?: string | null, externalId: string, status: ContractStatusEnum, startedAt?: any | null, endedAt?: any | null, customer: { __typename?: 'Customer', id: string, displayName: string } };

export type GetContractsListQueryVariables = Exact<{
page?: InputMaybe<Scalars['Int']['input']>;
limit?: InputMaybe<Scalars['Int']['input']>;
}>;


export type GetContractsListQuery = { __typename?: 'Query', contracts: { __typename?: 'ContractCollection', collection: Array<{ __typename?: 'Contract', id: string, name?: string | null, externalId: string, status: ContractStatusEnum, startedAt?: any | null, endedAt?: any | null, customer: { __typename?: 'Customer', id: string, displayName: string } }>, metadata: { __typename?: 'CollectionMetadata', currentPage: number, totalPages: number, totalCount: number } } };

export type SelectableBillableMetricForCouponsFragment = { __typename?: 'SelectableBillableMetric', id: string, name: string, code: string };

export type GetBillableMetricsForCouponsQueryVariables = Exact<{
Expand Down Expand Up @@ -23407,6 +23417,20 @@ export const RateCardForListFragmentDoc = gql`
${PropertiesForRateCardRateFragmentDoc}
${RateCardForDrawerFragmentDoc}
${RateCardForDeleteRateCardDialogFragmentDoc}`;
export const ContractForContractsListFragmentDoc = gql`
fragment ContractForContractsList on Contract {
id
name
externalId
status
startedAt
endedAt
customer {
id
displayName
}
}
`;
export const SelectableBillableMetricForCouponsFragmentDoc = gql`
fragment SelectableBillableMetricForCoupons on SelectableBillableMetric {
id
Expand Down Expand Up @@ -44067,6 +44091,57 @@ export function useUpdateRateCardRateMutation(baseOptions?: Apollo.MutationHookO
export type UpdateRateCardRateMutationHookResult = ReturnType<typeof useUpdateRateCardRateMutation>;
export type UpdateRateCardRateMutationResult = Apollo.MutationResult<UpdateRateCardRateMutation>;
export type UpdateRateCardRateMutationOptions = Apollo.BaseMutationOptions<UpdateRateCardRateMutation, UpdateRateCardRateMutationVariables>;
export const GetContractsListDocument = gql`
query getContractsList($page: Int, $limit: Int) {
contracts(page: $page, limit: $limit) {
collection {
...ContractForContractsList
}
metadata {
currentPage
totalPages
totalCount
}
}
}
${ContractForContractsListFragmentDoc}`;

/**
* __useGetContractsListQuery__
*
* To run a query within a React component, call `useGetContractsListQuery` and pass it any options that fit your needs.
* When your component renders, `useGetContractsListQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetContractsListQuery({
* variables: {
* page: // value for 'page'
* limit: // value for 'limit'
* },
* });
*/
export function useGetContractsListQuery(baseOptions?: Apollo.QueryHookOptions<GetContractsListQuery, GetContractsListQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetContractsListQuery, GetContractsListQueryVariables>(GetContractsListDocument, options);
}
export function useGetContractsListLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetContractsListQuery, GetContractsListQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetContractsListQuery, GetContractsListQueryVariables>(GetContractsListDocument, options);
}
// @ts-ignore
export function useGetContractsListSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions<GetContractsListQuery, GetContractsListQueryVariables>): Apollo.UseSuspenseQueryResult<GetContractsListQuery, GetContractsListQueryVariables>;
export function useGetContractsListSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<GetContractsListQuery, GetContractsListQueryVariables>): Apollo.UseSuspenseQueryResult<GetContractsListQuery | undefined, GetContractsListQueryVariables>;
export function useGetContractsListSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<GetContractsListQuery, GetContractsListQueryVariables>) {
const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
return Apollo.useSuspenseQuery<GetContractsListQuery, GetContractsListQueryVariables>(GetContractsListDocument, options);
}
export type GetContractsListQueryHookResult = ReturnType<typeof useGetContractsListQuery>;
export type GetContractsListLazyQueryHookResult = ReturnType<typeof useGetContractsListLazyQuery>;
export type GetContractsListSuspenseQueryHookResult = ReturnType<typeof useGetContractsListSuspenseQuery>;
export type GetContractsListQueryResult = Apollo.QueryResult<GetContractsListQuery, GetContractsListQueryVariables>;
export const GetBillableMetricsForCouponsDocument = gql`
query getBillableMetricsForCoupons($page: Int, $limit: Int, $searchTerm: String) {
selectableBillableMetrics(page: $page, limit: $limit, searchTerm: $searchTerm) {
Expand Down
10 changes: 10 additions & 0 deletions src/layouts/MainNavLayout/MainNavMenuSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
BILLABLE_METRICS_ROUTE,
CATALOG_PLAN_DETAILS_ROUTE,
CATALOG_PLAN_DETAILS_SECTION_ROUTE,
CONTRACT_DETAILS_ROUTE,
CONTRACTS_ROUTE,
COUPON_DETAILS_ROUTE,
COUPONS_ROUTE,
CREDIT_NOTES_ROUTE,
Expand Down Expand Up @@ -195,6 +197,14 @@ export const MainNavMenuSections = ({ isLoading, onItemClick }: MainNavMenuSecti
</span>
),
},
{
title: translate('text_17894894166553ysarr965xr'),
icon: 'contract',
link: CONTRACTS_ROUTE,
canBeClickedOnActive: true,
match: [CONTRACTS_ROUTE, CONTRACT_DETAILS_ROUTE],
hidden: !hasPermissions(['contractsView']) || !hasFeatureFlag(FeatureFlagEnum.ProductCatalog),
},
{
title: translate('text_6250304370f0f700a8fdc28d'),
icon: 'clock',
Expand Down
41 changes: 41 additions & 0 deletions src/layouts/MainNavLayout/__tests__/MainNavMenuSections.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { screen, within } from '@testing-library/react'
import {
CATALOG_PLAN_DETAILS_ROUTE,
CATALOG_PLAN_DETAILS_SECTION_ROUTE,
CONTRACT_DETAILS_ROUTE,
CONTRACTS_ROUTE,
CUSTOMERS_LIST_ROUTE,
PLAN_PRICING_ROUTE,
} from '~/core/router'
Expand Down Expand Up @@ -109,6 +111,44 @@ describe('MainNavMenuSections', () => {
})
})

describe('Contracts navigation', () => {
const getContractsTab = (): NavTab | undefined =>
mockVerticalMenuProps.mock.calls
.flatMap(([props]) => props.tabs as NavTab[])
.find((tab) => tab.link === CONTRACTS_ROUTE)

it('registers the contract icon and active route in Billing & operations', () => {
render(<MainNavMenuSections {...defaultProps} />)

expect(getContractsTab()).toEqual(
expect.objectContaining({
icon: 'contract',
match: [CONTRACTS_ROUTE, CONTRACT_DETAILS_ROUTE],
canBeClickedOnActive: true,
}),
)
const billing = screen.getByTestId(MAIN_NAV_BILLING_SECTION_TEST_ID)

expect(billing.querySelector(`a[href="${CONTRACTS_ROUTE}"]`)).toBeInTheDocument()
})

it('hides Contracts without contractsView permission', () => {
mockHasPermissions.mockImplementation(
(permissions: string[]) => !permissions.includes('contractsView'),
)
render(<MainNavMenuSections {...defaultProps} />)

expect(document.querySelector(`a[href="${CONTRACTS_ROUTE}"]`)).not.toBeInTheDocument()
})

it('hides Contracts when Product Catalog is disabled', () => {
mockHasFeatureFlag.mockImplementation((flag) => flag !== FeatureFlagEnum.ProductCatalog)
render(<MainNavMenuSections {...defaultProps} />)

expect(document.querySelector(`a[href="${CONTRACTS_ROUTE}"]`)).not.toBeInTheDocument()
})
})

describe('Component rendering', () => {
it('exposes a stable selector for the Customers navigation item', () => {
render(<MainNavMenuSections {...defaultProps} />)
Expand Down Expand Up @@ -222,6 +262,7 @@ describe('MainNavMenuSections', () => {
it('does not render billing section when all billing tabs are hidden', () => {
mockHasPermissions.mockImplementation((permissions: string[]) => {
const billingPermissions = [
'contractsView',
'customersView',
'quotesView',
'subscriptionsView',
Expand Down
11 changes: 11 additions & 0 deletions src/pages/contracts/ContractDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Typography } from '~/components/designSystem/Typography'

const ContractDetails = (): JSX.Element => {
return (
<div className="px-4 py-12 md:px-12">
<Typography variant="headline">Todo</Typography>
</div>
)
}

export default ContractDetails
Loading
Loading