Compare commits

...

17 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
a5680c08c2 Merge pull request #943 from bigcapitalhq/fix/abouolia/bank-rule-payee-field-validation
fix(server): add missing S3_FORCE_PATH_STYLE environment variable
2026-02-11 19:36:52 +02:00
Ahmed Bouhuolia
d909dad1bf fix: add missing S3_FORCE_PATH_STYLE environment variable
The S3 module was referencing config.forcePathStyle but the value
was never being read from the environment. This adds the missing
forcePathStyle configuration to the S3 config.

Closes #940

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 19:35:21 +02:00
Ahmed Bouhuolia
f32cc752ef Merge pull request #942 from bigcapitalhq/fix/abouolia/bank-rule-payee-field-validation
fix(server): allow 'payee' field in bank rule conditions validation
2026-02-11 19:13:35 +02:00
Ahmed Bouhuolia
a7f98201cc fix: allow 'payee' field in bank rule conditions validation
The BankRuleConditionDto validation only allowed 'description' and 'amount'
fields, but the frontend also sends 'payee' as a valid condition field.
This caused a 400 Bad Request error when creating rules with payee conditions.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 19:11:58 +02:00
Ahmed Bouhuolia
a1d0fc3f0a Merge pull request #941 from bigcapitalhq/fix/ahmedbouhuolia/phone-validation-formatted-numbers
fix(webapp): allow formatted phone numbers in customer and vendor forms
2026-02-11 18:39:52 +02:00
Ahmed Bouhuolia
11575cfb96 fix(webapp): allow formatted phone numbers in customer and vendor forms 2026-02-11 18:37:39 +02:00
Ahmed Bouhuolia
a2160c0595 Merge pull request #939 from bigcapitalhq/fix/ahmedbouhuolia/docker-healthcheck-endpoint
fix(server): fix Docker healthcheck endpoint
2026-02-10 00:25:25 +02:00
Ahmed Bouhuolia
956a9b58dd fix(server): register SystemDatabaseController and add PublicRoute decorator
- Register SystemDatabaseController in SystemDatabaseModule to expose /api/system_db endpoint
- Add PublicRoute decorator to bypass authentication for healthcheck endpoint
- Update ping() method to return { status: 'ok' } with HTTP 200

This fixes the Docker healthcheck that was failing with 404 Not Found errors.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 00:22:40 +02:00
Ahmed Bouhuolia
acb701d618 Merge pull request #938 from bigcapitalhq/fix/universal-search-api-endpoints
refactor: update UniversalSearch components with TypeScript and TextStatus
2026-02-09 19:55:37 +02:00
Ahmed Bouhuolia
09ff72d302 fix: add TypeScript types to If component 2026-02-09 19:52:17 +02:00
Ahmed Bouhuolia
7375512fec refactor: update UniversalSearch components with TypeScript and TextStatus 2026-02-09 19:26:26 +02:00
Ahmed Bouhuolia
77e65389a4 Merge pull request #937 from bigcapitalhq/fix/universal-search-api-endpoints
fix: universal search API endpoint errors
2026-02-09 13:42:53 +02:00
Ahmed Bouhuolia
1972861c97 fix(server): add missing searchRoles to Item model
Add searchRoles static property to enable searching items by name and code.
This fixes the 500 Internal Server Error when searching items via
/api/items?search_keyword=...

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-09 13:40:34 +02:00
Ahmed Bouhuolia
c47acdee03 fix(webapp): correct API endpoint URLs for universal search
Update resource URL mappings to match backend NestJS controller routes:
- /sales/invoices -> /sale-invoices
- /sales/estimates -> /sale-estimates
- /sales/receipts -> /sale-receipts
- /purchases/bills -> /bills
- /sales/payment_receives -> /payments-received
- /purchases/bill_payments -> /bill-payments
- /sales/credit_notes -> /credit-notes
- /purchases/vendor-credit -> /vendor-credits

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-09 13:40:18 +02:00
Ahmed Bouhuolia
8689962bf3 Merge pull request #935 from bigcapitalhq/feat/abouolia/add-throttle-organization-build-job
feat: expand rate limiting of getting org build job endpoint
2026-02-09 13:23:49 +02:00
Ahmed Bouhuolia
3258159474 feat: add rate limiting to organization build job endpoint
Add @Throttle decorator to GET /build/:buildJobId endpoint to limit
to 300 requests per minute to prevent abuse.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-08 09:39:55 +02:00
Ahmed Bouhuolia
36bfa573ad 🐛 fix(manual-journal): fix race condition in form submission handlers
Fix the order of setSubmitPayload and submitForm calls in all six
button handlers to prevent race condition where submitForm reads
stale state before setSubmitPayload updates it.

Changes:
- handleSubmitPublishBtnClick
- handleSubmitPublishAndNewBtnClick
- handleSubmitPublishContinueEditingBtnClick
- handleSubmitDraftBtnClick
- handleSubmitDraftAndNewBtnClick
- handleSubmitDraftContinueEditingBtnClick

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-07 16:58:57 +02:00
21 changed files with 538 additions and 355 deletions

View File

@@ -6,4 +6,5 @@ export default registerAs('s3', () => ({
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
endpoint: process.env.S3_ENDPOINT, endpoint: process.env.S3_ENDPOINT,
bucket: process.env.S3_BUCKET, bucket: process.env.S3_BUCKET,
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
})); }));

View File

@@ -16,7 +16,7 @@ import { ToNumber } from '@/common/decorators/Validators';
class BankRuleConditionDto { class BankRuleConditionDto {
@IsNotEmpty() @IsNotEmpty()
@IsIn(['description', 'amount']) @IsIn(['description', 'amount', 'payee'])
field: string; field: string;
@IsNotEmpty() @IsNotEmpty()

View File

@@ -70,6 +70,16 @@ export class Item extends TenantBaseModel {
}; };
} }
/**
* Model search roles.
*/
static get searchRoles() {
return [
{ condition: 'or', fieldKey: 'name', comparator: 'contains' },
{ condition: 'or', fieldKey: 'code', comparator: 'like' },
];
}
/** /**
* Relationship mapping. * Relationship mapping.
*/ */

View File

@@ -17,6 +17,7 @@ import {
HttpCode, HttpCode,
Param, Param,
} from '@nestjs/common'; } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { BuildOrganizationService } from './commands/BuildOrganization.service'; import { BuildOrganizationService } from './commands/BuildOrganization.service';
import { import {
BuildOrganizationDto, BuildOrganizationDto,
@@ -50,7 +51,7 @@ export class OrganizationController {
private readonly updateOrganizationService: UpdateOrganizationService, private readonly updateOrganizationService: UpdateOrganizationService,
private readonly getBuildOrganizationJobService: GetBuildOrganizationBuildJob, private readonly getBuildOrganizationJobService: GetBuildOrganizationBuildJob,
private readonly orgBaseCurrencyLockingService: OrganizationBaseCurrencyLocking, private readonly orgBaseCurrencyLockingService: OrganizationBaseCurrencyLocking,
) { } ) {}
@Post('build') @Post('build')
@HttpCode(200) @HttpCode(200)
@@ -77,6 +78,7 @@ export class OrganizationController {
} }
@Get('build/:buildJobId') @Get('build/:buildJobId')
@Throttle({ default: { limit: 300, ttl: 60000 } }) // 300 req/min
@ApiParam({ @ApiParam({
name: 'buildJobId', name: 'buildJobId',
required: true, required: true,

View File

@@ -1,12 +1,14 @@
import { Controller, Get, Post } from '@nestjs/common'; import { Controller, Get, HttpCode } from '@nestjs/common';
import { PublicRoute } from '@/modules/Auth/guards/jwt.guard';
@Controller('/system_db') @Controller('system_db')
@PublicRoute()
export class SystemDatabaseController { export class SystemDatabaseController {
constructor() {} constructor() {}
@Post()
@Get() @Get()
ping(){ @HttpCode(200)
ping() {
return { status: 'ok' };
} }
} }

View File

@@ -6,6 +6,7 @@ import {
SystemKnexConnectionConfigure, SystemKnexConnectionConfigure,
} from './SystemDB.constants'; } from './SystemDB.constants';
import { knexSnakeCaseMappers } from 'objection'; import { knexSnakeCaseMappers } from 'objection';
import { SystemDatabaseController } from './SystemDB.controller';
const providers = [ const providers = [
{ {
@@ -42,6 +43,7 @@ const providers = [
@Global() @Global()
@Module({ @Module({
controllers: [SystemDatabaseController],
providers: [...providers], providers: [...providers],
exports: [...providers], exports: [...providers],
}) })

View File

@@ -10,12 +10,17 @@ const TextStatusRoot = styled.span`
${(props) => ${(props) =>
props.intent === 'warning' && props.intent === 'warning' &&
` `
color: #ec5b0a;`} color: #c87619;`}
${(props) =>
props.intent === 'danger' &&
`
color: #f17377;`}
${(props) => ${(props) =>
props.intent === 'success' && props.intent === 'success' &&
` `
color: #2ba01d;`} color: #238551;`}
${(props) => ${(props) =>
props.intent === 'none' && props.intent === 'none' &&

View File

@@ -1,7 +1,5 @@
// @ts-nocheck import React, { KeyboardEvent, ReactNode } from 'react';
import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import classNames from 'classnames';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import { import {
Overlay, Overlay,
@@ -10,11 +8,14 @@ import {
MenuItem, MenuItem,
Spinner, Spinner,
Intent, Intent,
OverlayProps,
Button,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { QueryList } from '@blueprintjs/select'; import { QueryList, ItemRenderer } from '@blueprintjs/select';
import { CLASSES } from '@/constants/classes'; import { x } from '@xstyled/emotion';
import { css } from '@emotion/css';
import { Icon, If, ListSelect, FormattedMessage as T } from '@/components'; import { Icon, If, FormattedMessage as T } from '@/components';
import { Select } from '@blueprintjs-formik/select';
import { import {
UniversalSearchProvider, UniversalSearchProvider,
useUniversalSearchContext, useUniversalSearchContext,
@@ -22,59 +23,297 @@ import {
import { filterItemsByResourceType } from './utils'; import { filterItemsByResourceType } from './utils';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes'; import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
// Resource type from RESOURCES_TYPES constant
type ResourceType = string;
// Search type option item
interface SearchTypeOption {
key: ResourceType;
label: string;
}
// Universal search item
interface UniversalSearchItem {
id: number | string;
_type: ResourceType;
text: string;
subText?: string;
label?: string;
[key: string]: any;
}
// CSS styles for complex selectors
const overlayStyles = css`
.bp4-overlay-appear,
.bp4-overlay-enter {
filter: blur(20px);
opacity: 0.2;
}
.bp4-overlay-appear-active,
.bp4-overlay-enter-active {
filter: blur(0);
opacity: 1;
transition:
filter 0.2s cubic-bezier(0.4, 1, 0.75, 0.9),
opacity 0.2s cubic-bezier(0.4, 1, 0.75, 0.9);
}
.bp4-overlay-exit {
filter: blur(0);
opacity: 1;
}
.bp4-overlay-exit-active {
filter: blur(20px);
opacity: 0.2;
transition:
filter 0.2s cubic-bezier(0.4, 1, 0.75, 0.9),
opacity 0.2s cubic-bezier(0.4, 1, 0.75, 0.9);
}
`;
const containerStyles = css`
position: fixed;
filter: blur(0);
opacity: 1;
background-color: var(--color-universal-search-background);
border-radius: 3px;
box-shadow:
0 0 0 1px rgba(16, 22, 26, 0.1),
0 4px 8px rgba(16, 22, 26, 0.2),
0 18px 46px 6px rgba(16, 22, 26, 0.2);
left: calc(50% - 250px);
top: 20vh;
width: 500px;
z-index: 20;
.bp4-input-group {
.bp4-icon {
margin: 16px;
color: var(--color-universal-search-icon);
svg {
stroke: currentColor;
fill: none;
fill-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2;
--text-opacity: 1;
}
}
}
.bp4-input-group .bp4-input {
border: 0;
box-shadow: 0 0 0 0;
height: 50px;
line-height: 50px;
font-size: 20px;
}
.bp4-input-group.bp4-large .bp4-input:not(:first-child) {
padding-left: 50px !important;
}
.bp4-input-group.bp4-large .bp4-input:not(:last-child) {
padding-right: 130px !important;
}
.bp4-menu {
border-top: 1px solid var(--color-universal-search-menu-border);
max-height: calc(60vh - 20px);
overflow: auto;
.bp4-menu-item {
.bp4-text-muted {
font-size: 12px;
.bp4-icon {
color: var(--bp4-gray-600);
}
}
&.bp4-intent-primary {
&.bp4-active {
background-color: var(--bp4-blue-100);
color: var(--bp4-dark-gray-800);
.bp4-menu-item-label {
color: var(--bp4-gray-600);
}
}
}
&-label {
flex-direction: row;
text-align: right;
}
}
}
.bp4-input-action {
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
}
`;
const inputRightElementsStyles = css`
display: flex;
margin: 10px;
.bp4-spinner {
margin-right: 6px;
}
`;
const footerStyles = css`
padding: 12px 12px;
border-top: 1px solid var(--color-universal-search-footer-divider);
`;
const actionBaseStyles = css`
&:not(:first-of-type) {
margin-left: 14px;
}
.bp4-tag {
background: var(--color-universal-search-tag-background);
color: var(--color-universal-search-tag-text);
}
`;
const actionArrowsStyles = css`
&:not(:first-of-type) {
margin-left: 14px;
}
.bp4-tag {
background: var(--color-universal-search-tag-background);
color: var(--color-universal-search-tag-text);
padding: 0;
text-align: center;
line-height: 16px;
margin-left: 4px;
svg {
fill: var(--color-universal-search-tag-text);
height: 100%;
display: block;
width: 100%;
padding: 2px;
}
}
`;
// UniversalSearchInputRightElements props
interface UniversalSearchInputRightElementsProps {
/** Callback when search type changes */
onSearchTypeChange?: (option: SearchTypeOption) => void;
}
/** /**
* Universal search input action. * Universal search input action.
*/ */
function UniversalSearchInputRightElements({ onSearchTypeChange }) { function UniversalSearchInputRightElements({
const { isLoading, searchType, defaultSearchResource, searchTypeOptions } = onSearchTypeChange,
}: UniversalSearchInputRightElementsProps) {
const { isLoading, searchType, searchTypeOptions } =
useUniversalSearchContext(); useUniversalSearchContext();
// Find the currently selected item object.
const selectedItem = searchTypeOptions.find(
(item) => item.key === searchType,
);
// Handle search type option change. // Handle search type option change.
const handleSearchTypeChange = (option) => { const handleSearchTypeChange = (option: SearchTypeOption) => {
onSearchTypeChange && onSearchTypeChange(option); onSearchTypeChange?.(option);
};
// Item renderer for the select dropdown.
const itemRenderer: ItemRenderer<SearchTypeOption> = (
item,
{ handleClick },
) => {
return <MenuItem text={item.label} key={item.key} onClick={handleClick} />;
}; };
return ( return (
<div className={CLASSES.UNIVERSAL_SEARCH_INPUT_RIGHT_ELEMENTS}> <x.div display="flex" m="10px" className={inputRightElementsStyles}>
<If condition={isLoading}> <If condition={isLoading}>
<Spinner tagName="div" intent={Intent.NONE} size={18} value={null} /> <Spinner tagName="div" intent={Intent.NONE} size={18} />
</If> </If>
<ListSelect <Select<SearchTypeOption>
items={searchTypeOptions} items={searchTypeOptions}
itemRenderer={itemRenderer}
onItemSelect={handleSearchTypeChange} onItemSelect={handleSearchTypeChange}
selectedValue={selectedItem?.key}
valueAccessor={'key'}
labelAccessor={'label'}
filterable={false} filterable={false}
initialSelectedItem={defaultSearchResource}
selectedItem={searchType}
selectedItemProp={'key'}
textProp={'label'}
// defaultText={intl.get('type')}
popoverProps={{ popoverProps={{
minimal: true, minimal: true,
captureDismiss: true, captureDismiss: true,
className: CLASSES.UNIVERSAL_SEARCH_TYPE_SELECT_OVERLAY,
}}
buttonProps={{
minimal: true,
className: CLASSES.UNIVERSAL_SEARCH_TYPE_SELECT_BTN,
}} }}
input={({ activeItem }) => (
<Button minimal={true} text={activeItem?.label} />
)}
/> />
</div> </x.div>
); );
} }
// QueryList renderer props
interface QueryListRendererProps {
/** Current query string */
query: string;
/** Callback when query changes */
handleQueryChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
/** Item list element */
itemList: ReactNode;
/** Class name */
className?: string;
/** Handle key down */
handleKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
/** Handle key up */
handleKeyUp?: (event: KeyboardEvent<HTMLDivElement>) => void;
}
// UniversalSearchQueryList props
interface UniversalSearchQueryListProps {
/** Whether the search is open */
isOpen: boolean;
/** Whether the search is loading */
isLoading: boolean;
/** Callback when search type changes */
onSearchTypeChange?: (option: SearchTypeOption) => void;
/** Current search type */
searchType: ResourceType;
/** Items to display */
items: UniversalSearchItem[];
/** Renderer for items */
itemRenderer?: ItemRenderer<UniversalSearchItem>;
/** Callback when an item is selected */
onItemSelect?: (item: UniversalSearchItem, event?: any) => void;
/** Current query string */
query: string;
/** Callback when query changes */
onQueryChange?: (query: string) => void;
}
/** /**
* Universal search query list. * Universal search query list.
*/ */
function UniversalSearchQueryList(props) { function UniversalSearchQueryList({
const { isOpen, isLoading, onSearchTypeChange, searchType, ...restProps } = isOpen,
props; isLoading,
onSearchTypeChange,
...restProps
}: UniversalSearchQueryListProps) {
return ( return (
<QueryList <QueryList<UniversalSearchItem>
{...restProps} {...(restProps as any)}
initialContent={null} initialContent={null}
renderer={(listProps) => ( renderer={(listProps: QueryListRendererProps) => (
<UniversalSearchBar <UniversalSearchBar
isOpen={isOpen} isOpen={isOpen}
onSearchTypeChange={onSearchTypeChange} onSearchTypeChange={onSearchTypeChange}
@@ -100,47 +339,53 @@ function UniversalSearchQueryList(props) {
*/ */
function UniversalQuerySearchActions() { function UniversalQuerySearchActions() {
return ( return (
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTIONS)}> <x.div display="flex">
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_SELECT)}> <x.div className={actionBaseStyles}>
<Tag>ENTER</Tag> <Tag>ENTER</Tag>
<span class={'text'}>{intl.get('universal_search.enter_text')}</span> <x.span ml="6px">{intl.get('universal_search.enter_text')}</x.span>
</div> </x.div>
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_CLOSE)}> <x.div className={actionBaseStyles}>
<Tag>ESC</Tag>{' '} <Tag>ESC</Tag>{' '}
<span class={'text'}>{intl.get('universal_search.close_text')}</span> <x.span ml="6px">{intl.get('universal_search.close_text')}</x.span>
</div> </x.div>
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_ARROWS)}> <x.div className={actionArrowsStyles}>
<Tag> <Tag>
<Icon icon={'arrow-up-24'} iconSize={16} /> <Icon icon={'arrow-up-24'} iconSize={16} />
</Tag> </Tag>
<Tag> <Tag>
<Icon icon={'arrow-down-24'} iconSize={16} /> <Icon icon={'arrow-down-24'} iconSize={16} />
</Tag> </Tag>
<span class="text">{intl.get('universal_seach.navigate_text')}</span> <x.span ml="6px">{intl.get('universal_seach.navigate_text')}</x.span>
</div> </x.div>
</div> </x.div>
); );
} }
// UniversalSearchBar props
interface UniversalSearchBarProps extends QueryListRendererProps {
/** Whether the search is open */
isOpen: boolean;
/** Callback when search type changes */
onSearchTypeChange?: (option: SearchTypeOption) => void;
}
/** /**
* Universal search input bar with items list. * Universal search input bar with items list.
*/ */
function UniversalSearchBar({ isOpen, onSearchTypeChange, ...listProps }) { function UniversalSearchBar({
isOpen,
onSearchTypeChange,
...listProps
}: UniversalSearchBarProps) {
const { handleKeyDown, handleKeyUp } = listProps; const { handleKeyDown, handleKeyUp } = listProps;
const handlers = isOpen const handlers = isOpen
? { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp } ? { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp }
: {}; : {};
return ( return (
<div <x.div {...handlers}>
className={classNames(
CLASSES.UNIVERSAL_SEARCH_OMNIBAR,
listProps.className,
)}
{...handlers}
>
<InputGroup <InputGroup
large={true} large={true}
leftIcon={<Icon icon={'universal-search'} iconSize={20} />} leftIcon={<Icon icon={'universal-search'} iconSize={20} />}
@@ -155,17 +400,44 @@ function UniversalSearchBar({ isOpen, onSearchTypeChange, ...listProps }) {
autoFocus={true} autoFocus={true}
/> />
{listProps.itemList} {listProps.itemList}
</div> </x.div>
); );
} }
// UniversalSearch props
export interface UniversalSearchProps {
/** Default search resource type */
defaultSearchResource?: ResourceType;
/** Controlled search resource type */
searchResource?: ResourceType;
/** Overlay props */
overlayProps?: OverlayProps;
/** Whether the search overlay is open */
isOpen: boolean;
/** Whether the search is loading */
isLoading: boolean;
/** Callback when search type changes */
onSearchTypeChange?: (resource: SearchTypeOption) => void;
/** Items to display */
items: UniversalSearchItem[];
/** Available search type options */
searchTypeOptions: SearchTypeOption[];
/** Renderer for items */
itemRenderer?: ItemRenderer<UniversalSearchItem>;
/** Callback when an item is selected */
onItemSelect?: (item: UniversalSearchItem, event?: any) => void;
/** Current query string */
query: string;
/** Callback when query changes */
onQueryChange?: (query: string) => void;
}
/** /**
* Universal search. * Universal search.
*/ */
export function UniversalSearch({ export function UniversalSearch({
defaultSearchResource, defaultSearchResource,
searchResource, searchResource,
overlayProps, overlayProps,
isOpen, isOpen,
isLoading, isLoading,
@@ -173,9 +445,9 @@ export function UniversalSearch({
items, items,
searchTypeOptions, searchTypeOptions,
...queryListProps ...queryListProps
}) { }: UniversalSearchProps) {
// Search type state. // Search type state.
const [searchType, setSearchType] = React.useState( const [searchType, setSearchType] = React.useState<ResourceType>(
defaultSearchResource || RESOURCES_TYPES.CUSTOMER, defaultSearchResource || RESOURCES_TYPES.CUSTOMER,
); );
// Handle search resource type controlled mode. // Handle search resource type controlled mode.
@@ -189,9 +461,9 @@ export function UniversalSearch({
}, [searchResource, defaultSearchResource]); }, [searchResource, defaultSearchResource]);
// Handle search type change. // Handle search type change.
const handleSearchTypeChange = (searchTypeResource) => { const handleSearchTypeChange = (searchTypeResource: SearchTypeOption) => {
setSearchType(searchTypeResource.key); setSearchType(searchTypeResource.key);
onSearchTypeChange && onSearchTypeChange(searchTypeResource); onSearchTypeChange?.(searchTypeResource);
}; };
// Filters query list items based on the given search type. // Filters query list items based on the given search type.
const filteredItems = filterItemsByResourceType(items, searchType); const filteredItems = filterItemsByResourceType(items, searchType);
@@ -200,7 +472,7 @@ export function UniversalSearch({
<Overlay <Overlay
hasBackdrop={true} hasBackdrop={true}
isOpen={isOpen} isOpen={isOpen}
className={classNames(CLASSES.UNIVERSAL_SEARCH_OVERLAY)} className={overlayStyles}
{...overlayProps} {...overlayProps}
> >
<UniversalSearchProvider <UniversalSearchProvider
@@ -209,7 +481,7 @@ export function UniversalSearch({
defaultSearchResource={defaultSearchResource} defaultSearchResource={defaultSearchResource}
searchTypeOptions={searchTypeOptions} searchTypeOptions={searchTypeOptions}
> >
<div className={classNames(CLASSES.UNIVERSAL_SEARCH)}> <x.div className={containerStyles}>
<UniversalSearchQueryList <UniversalSearchQueryList
isOpen={isOpen} isOpen={isOpen}
isLoading={isLoading} isLoading={isLoading}
@@ -218,10 +490,10 @@ export function UniversalSearch({
{...queryListProps} {...queryListProps}
items={filteredItems} items={filteredItems}
/> />
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_FOOTER)}> <x.div className={footerStyles}>
<UniversalQuerySearchActions /> <UniversalQuerySearchActions />
</div> </x.div>
</div> </x.div>
</UniversalSearchProvider> </UniversalSearchProvider>
</Overlay> </Overlay>
); );

View File

@@ -1,30 +1,82 @@
// @ts-nocheck import React, { createContext, ReactNode, useContext } from 'react';
import React, { createContext } from 'react';
const UniversalSearchContext = createContext(); // The resource type value from RESOURCES_TYPES constant
type ResourceType = string;
// Search type option item
interface SearchTypeOption {
key: ResourceType;
label: string;
}
// Context value type
interface UniversalSearchContextValue {
/** Whether the search is loading */
isLoading: boolean;
/** Current search type/resource type */
searchType: ResourceType;
/** Default search resource type */
defaultSearchResource?: ResourceType;
/** List of available search type options */
searchTypeOptions: SearchTypeOption[];
}
// Create the context with undefined as initial value
const UniversalSearchContext = createContext<
UniversalSearchContextValue | undefined
>(undefined);
// Provider props interface
interface UniversalSearchProviderProps {
/** Whether the search is loading */
isLoading: boolean;
/** Default search resource type */
defaultSearchResource?: ResourceType;
/** Current search type/resource type */
searchType: ResourceType;
/** List of available search type options */
searchTypeOptions: SearchTypeOption[];
/** Child elements */
children: ReactNode;
}
/** /**
* Universal search data provider. * Universal search data provider.
*/ */
function UniversalSearchProvider({ export function UniversalSearchProvider({
isLoading, isLoading,
defaultSearchResource, defaultSearchResource,
searchType, searchType,
searchTypeOptions, searchTypeOptions,
...props children,
}) { }: UniversalSearchProviderProps) {
// Provider payload. // Provider payload.
const provider = { const provider: UniversalSearchContextValue = {
isLoading, isLoading,
searchType, searchType,
defaultSearchResource, defaultSearchResource,
searchTypeOptions, searchTypeOptions,
}; };
return <UniversalSearchContext.Provider value={provider} {...props} />; return (
<UniversalSearchContext.Provider value={provider}>
{children}
</UniversalSearchContext.Provider>
);
} }
const useUniversalSearchContext = () => /**
React.useContext(UniversalSearchContext); * Hook to access the universal search context.
* @throws Error if used outside of UniversalSearchProvider
*/
export const useUniversalSearchContext = (): UniversalSearchContextValue => {
const context = useContext(UniversalSearchContext);
export { UniversalSearchProvider, useUniversalSearchContext }; if (context === undefined) {
throw new Error(
'useUniversalSearchContext must be used within a UniversalSearchProvider',
);
}
return context;
};

View File

@@ -1,12 +1,10 @@
// @ts-nocheck import React, { ReactNode } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
export const If = (props) => interface IfProps {
props.condition ? (props.render ? props.render() : props.children) : null; condition: boolean;
children?: ReactNode;
render?: () => ReactNode;
}
If.propTypes = { export const If = (props: IfProps): React.ReactElement | null =>
// condition: PropTypes.bool.isRequired, props.condition ? (props.render ? <>{props.render()}</> : <>{props.children}</>) : null;
children: PropTypes.node,
render: PropTypes.func,
};

View File

@@ -32,38 +32,38 @@ export default function MakeJournalFloatingAction() {
// Handle submit & publish button click. // Handle submit & publish button click.
const handleSubmitPublishBtnClick = (event) => { const handleSubmitPublishBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: true, publish: true }); setSubmitPayload({ redirect: true, publish: true });
submitForm();
}; };
// Handle submit, publish & new button click. // Handle submit, publish & new button click.
const handleSubmitPublishAndNewBtnClick = (event) => { const handleSubmitPublishAndNewBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: true, resetForm: true }); setSubmitPayload({ redirect: false, publish: true, resetForm: true });
submitForm();
}; };
// Handle submit, publish & edit button click. // Handle submit, publish & edit button click.
const handleSubmitPublishContinueEditingBtnClick = (event) => { const handleSubmitPublishContinueEditingBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: true }); setSubmitPayload({ redirect: false, publish: true });
submitForm();
}; };
// Handle submit as draft button click. // Handle submit as draft button click.
const handleSubmitDraftBtnClick = (event) => { const handleSubmitDraftBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: true, publish: false }); setSubmitPayload({ redirect: true, publish: false });
submitForm();
}; };
// Handle submit as draft & new button click. // Handle submit as draft & new button click.
const handleSubmitDraftAndNewBtnClick = (event) => { const handleSubmitDraftAndNewBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: false, resetForm: true }); setSubmitPayload({ redirect: false, publish: false, resetForm: true });
submitForm();
}; };
// Handle submit as draft & continue editing button click. // Handle submit as draft & continue editing button click.
const handleSubmitDraftContinueEditingBtnClick = (event) => { const handleSubmitDraftContinueEditingBtnClick = (event) => {
submitForm();
setSubmitPayload({ redirect: false, publish: false }); setSubmitPayload({ redirect: false, publish: false });
submitForm();
}; };
// Handle cancel button click. // Handle cancel button click.

View File

@@ -17,8 +17,8 @@ const Schema = Yup.object().shape({
.label(intl.get('display_name_')), .label(intl.get('display_name_')),
email: Yup.string().email().nullable(), email: Yup.string().email().nullable(),
work_phone: Yup.number(), work_phone: Yup.string().nullable(),
personal_phone: Yup.number(), personal_phone: Yup.string().nullable(),
website: Yup.string().url().nullable(), website: Yup.string().url().nullable(),
active: Yup.boolean(), active: Yup.boolean(),
@@ -30,7 +30,7 @@ const Schema = Yup.object().shape({
billing_address_city: Yup.string().trim(), billing_address_city: Yup.string().trim(),
billing_address_state: Yup.string().trim(), billing_address_state: Yup.string().trim(),
billing_address_postcode: Yup.string().nullable(), billing_address_postcode: Yup.string().nullable(),
billing_address_phone: Yup.number(), billing_address_phone: Yup.string().nullable(),
shipping_address_country: Yup.string().trim(), shipping_address_country: Yup.string().trim(),
shipping_address_1: Yup.string().trim(), shipping_address_1: Yup.string().trim(),
@@ -38,7 +38,7 @@ const Schema = Yup.object().shape({
shipping_address_city: Yup.string().trim(), shipping_address_city: Yup.string().trim(),
shipping_address_state: Yup.string().trim(), shipping_address_state: Yup.string().trim(),
shipping_address_postcode: Yup.string().nullable(), shipping_address_postcode: Yup.string().nullable(),
shipping_address_phone: Yup.number(), shipping_address_phone: Yup.string().nullable(),
opening_balance: Yup.number().nullable(), opening_balance: Yup.number().nullable(),
currency_code: Yup.string(), currency_code: Yup.string(),

View File

@@ -1,10 +1,10 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core'; import { MenuItem, Intent } from '@blueprintjs/core';
import { formattedAmount } from '@/utils'; import { formattedAmount } from '@/utils';
import { T, Icon, Choose, If } from '@/components'; import { T, Icon, Choose, If, TextStatus } from '@/components';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes'; import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
import { AbilitySubject, BillAction } from '@/constants/abilityOption'; import { AbilitySubject, BillAction } from '@/constants/abilityOption';
@@ -41,35 +41,35 @@ export function BillStatus({ bill }) {
return ( return (
<Choose> <Choose>
<Choose.When condition={bill.is_fully_paid && bill.is_open}> <Choose.When condition={bill.is_fully_paid && bill.is_open}>
<span class="fully-paid-text"> <TextStatus intent={Intent.SUCCESS}>
<T id={'paid'} /> <T id={'paid'} />
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.When condition={bill.is_open}> <Choose.When condition={bill.is_open}>
<Choose> <Choose>
<Choose.When condition={bill.is_overdue}> <Choose.When condition={bill.is_overdue}>
<span className={'overdue-status'}> <TextStatus intent={Intent.DANGER}>
{intl.get('overdue_by', { overdue: bill.overdue_days })} {intl.get('overdue_by', { overdue: bill.overdue_days })}
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<span className={'due-status'}> <TextStatus intent={Intent.WARNING}>
{intl.get('due_in', { due: bill.remaining_days })} {intl.get('due_in', { due: bill.remaining_days })}
</span> </TextStatus>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
<If condition={bill.is_partially_paid}> <If condition={bill.is_partially_paid}>
<span className="partial-paid"> <TextStatus intent={Intent.WARNING}>
{intl.get('day_partially_paid', { {intl.get('day_partially_paid', {
due: formattedAmount(bill.due_amount, bill.currency_code), due: formattedAmount(bill.due_amount, bill.currency_code),
})} })}
</span> </TextStatus>
</If> </If>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<span class="draft"> <TextStatus intent={Intent.NONE}>
<T id={'draft'} /> <T id={'draft'} />
</span> </TextStatus>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
); );

View File

@@ -1,9 +1,9 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core'; import { MenuItem, Intent } from '@blueprintjs/core';
import { Choose, T, Icon } from '@/components'; import { Choose, T, Icon, TextStatus } from '@/components';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes'; import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
import { AbilitySubject, SaleEstimateAction } from '@/constants/abilityOption'; import { AbilitySubject, SaleEstimateAction } from '@/constants/abilityOption';
@@ -37,28 +37,28 @@ export const EstimateUniversalSearchSelect = withDrawerActions(
export const EstimateStatus = ({ estimate }) => ( export const EstimateStatus = ({ estimate }) => (
<Choose> <Choose>
<Choose.When condition={estimate.is_delivered && estimate.is_approved}> <Choose.When condition={estimate.is_delivered && estimate.is_approved}>
<span class="approved"> <TextStatus intent={Intent.SUCCESS}>
<T id={'approved'} /> <T id={'approved'} />
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.When condition={estimate.is_delivered && estimate.is_rejected}> <Choose.When condition={estimate.is_delivered && estimate.is_rejected}>
<span class="reject"> <TextStatus intent={Intent.DANGER}>
<T id={'rejected'} /> <T id={'rejected'} />
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.When <Choose.When
condition={ condition={
estimate.is_delivered && !estimate.is_rejected && !estimate.is_approved estimate.is_delivered && !estimate.is_rejected && !estimate.is_approved
} }
> >
<span class="delivered"> <TextStatus intent={Intent.SUCCESS}>
<T id={'delivered'} /> <T id={'delivered'} />
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<span class="draft"> <TextStatus intent={Intent.NONE}>
<T id={'draft'} /> <T id={'draft'} />
</span> </TextStatus>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
); );

View File

@@ -1,9 +1,9 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core'; import { MenuItem, Intent } from '@blueprintjs/core';
import { T, Choose, Icon } from '@/components'; import { T, Choose, Icon, TextStatus } from '@/components';
import { highlightText } from '@/utils'; import { highlightText } from '@/utils';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes'; import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
@@ -39,29 +39,29 @@ function InvoiceStatus({ customer }) {
return ( return (
<Choose> <Choose>
<Choose.When condition={customer.is_fully_paid && customer.is_delivered}> <Choose.When condition={customer.is_fully_paid && customer.is_delivered}>
<span class="status status-success"> <TextStatus intent={Intent.SUCCESS}>
<T id={'paid'} /> <T id={'paid'} />
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.When condition={customer.is_delivered}> <Choose.When condition={customer.is_delivered}>
<Choose> <Choose>
<Choose.When condition={customer.is_overdue}> <Choose.When condition={customer.is_overdue}>
<span className={'status status-warning'}> <TextStatus intent={Intent.DANGER}>
{intl.get('overdue_by', { overdue: customer.overdue_days })} {intl.get('overdue_by', { overdue: customer.overdue_days })}
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<span className={'status status-warning'}> <TextStatus intent={Intent.WARNING}>
{intl.get('due_in', { due: customer.remaining_days })} {intl.get('due_in', { due: customer.remaining_days })}
</span> </TextStatus>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<span class="status status--gray"> <TextStatus intent={Intent.NONE}>
<T id={'draft'} /> <T id={'draft'} />
</span> </TextStatus>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
); );
@@ -94,7 +94,6 @@ export function InvoiceUniversalSearchItem(
</> </>
} }
onClick={handleClick} onClick={handleClick}
className={'universal-search__item--invoice'}
/> />
); );
} }

View File

@@ -1,9 +1,8 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core'; import { MenuItem, Intent } from '@blueprintjs/core';
import { Icon, Choose, T, TextStatus } from '@/components';
import { Icon, Choose, T } from '@/components';
import { RESOURCES_TYPES } from '@/constants/resourcesTypes'; import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
import { AbilitySubject, SaleReceiptAction } from '@/constants/abilityOption'; import { AbilitySubject, SaleReceiptAction } from '@/constants/abilityOption';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions'; import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
@@ -39,15 +38,15 @@ function ReceiptStatus({ receipt }) {
return ( return (
<Choose> <Choose>
<Choose.When condition={receipt.is_closed}> <Choose.When condition={receipt.is_closed}>
<span class="closed"> <TextStatus intent={Intent.SUCCESS}>
<T id={'closed'} /> <T id={'closed'} />
</span> </TextStatus>
</Choose.When> </Choose.When>
<Choose.Otherwise> <Choose.Otherwise>
<span class="draft"> <TextStatus intent={Intent.NONE}>
<T id={'draft'} /> <T id={'draft'} />
</span> </TextStatus>
</Choose.Otherwise> </Choose.Otherwise>
</Choose> </Choose>
); );

View File

@@ -10,8 +10,8 @@ const Schema = Yup.object().shape({
display_name: Yup.string().trim().required().label(intl.get('display_name_')), display_name: Yup.string().trim().required().label(intl.get('display_name_')),
email: Yup.string().email().nullable(), email: Yup.string().email().nullable(),
work_phone: Yup.number(), work_phone: Yup.string().nullable(),
personal_phone: Yup.number(), personal_phone: Yup.string().nullable(),
website: Yup.string().url().nullable(), website: Yup.string().url().nullable(),
active: Yup.boolean(), active: Yup.boolean(),
@@ -23,7 +23,7 @@ const Schema = Yup.object().shape({
billing_address_city: Yup.string().trim(), billing_address_city: Yup.string().trim(),
billing_address_state: Yup.string().trim(), billing_address_state: Yup.string().trim(),
billing_address_postcode: Yup.string().nullable(), billing_address_postcode: Yup.string().nullable(),
billing_address_phone: Yup.number(), billing_address_phone: Yup.string().nullable(),
shipping_address_country: Yup.string().trim(), shipping_address_country: Yup.string().trim(),
shipping_address_1: Yup.string().trim(), shipping_address_1: Yup.string().trim(),
@@ -31,7 +31,7 @@ const Schema = Yup.object().shape({
shipping_address_city: Yup.string().trim(), shipping_address_city: Yup.string().trim(),
shipping_address_state: Yup.string().trim(), shipping_address_state: Yup.string().trim(),
shipping_address_postcode: Yup.string().nullable(), shipping_address_postcode: Yup.string().nullable(),
shipping_address_phone: Yup.number(), shipping_address_phone: Yup.string().nullable(),
opening_balance: Yup.number().nullable(), opening_balance: Yup.number().nullable(),
currency_code: Yup.string(), currency_code: Yup.string(),

View File

@@ -32,19 +32,19 @@ export function useResourceData(type, query, props) {
*/ */
function getResourceUrlFromType(type) { function getResourceUrlFromType(type) {
const config = { const config = {
[RESOURCES_TYPES.INVOICE]: '/sales/invoices', [RESOURCES_TYPES.INVOICE]: '/sale-invoices',
[RESOURCES_TYPES.ESTIMATE]: '/sales/estimates', [RESOURCES_TYPES.ESTIMATE]: '/sale-estimates',
[RESOURCES_TYPES.ITEM]: '/items', [RESOURCES_TYPES.ITEM]: '/items',
[RESOURCES_TYPES.RECEIPT]: '/sales/receipts', [RESOURCES_TYPES.RECEIPT]: '/sale-receipts',
[RESOURCES_TYPES.BILL]: '/purchases/bills', [RESOURCES_TYPES.BILL]: '/bills',
[RESOURCES_TYPES.PAYMENT_RECEIVE]: '/sales/payment_receives', [RESOURCES_TYPES.PAYMENT_RECEIVE]: '/payments-received',
[RESOURCES_TYPES.PAYMENT_MADE]: '/purchases/bill_payments', [RESOURCES_TYPES.PAYMENT_MADE]: '/bill-payments',
[RESOURCES_TYPES.CUSTOMER]: '/customers', [RESOURCES_TYPES.CUSTOMER]: '/customers',
[RESOURCES_TYPES.VENDOR]: '/vendors', [RESOURCES_TYPES.VENDOR]: '/vendors',
[RESOURCES_TYPES.MANUAL_JOURNAL]: '/manual-journals', [RESOURCES_TYPES.MANUAL_JOURNAL]: '/manual-journals',
[RESOURCES_TYPES.ACCOUNT]: '/accounts', [RESOURCES_TYPES.ACCOUNT]: '/accounts',
[RESOURCES_TYPES.CREDIT_NOTE]: '/sales/credit_notes', [RESOURCES_TYPES.CREDIT_NOTE]: '/credit-notes',
[RESOURCES_TYPES.VENDOR_CREDIT]: '/purchases/vendor-credit', [RESOURCES_TYPES.VENDOR_CREDIT]: '/vendor-credits',
}; };
return config[type] || ''; return config[type] || '';
} }

View File

@@ -31,7 +31,6 @@
@import 'components/Overlay'; @import 'components/Overlay';
@import 'components/Menu'; @import 'components/Menu';
@import 'components/SidebarOverlay'; @import 'components/SidebarOverlay';
@import 'components/UniversalSearch';
// Pages // Pages
@import 'pages/view-form'; @import 'pages/view-form';

View File

@@ -7,6 +7,27 @@ $ns: bp4;
--color-primary: #8abbff; --color-primary: #8abbff;
--color-danger: red; --color-danger: red;
// Green colors
--color-green-500: #165a36;
--color-green-400: #1c6e42;
--color-green-300: #238551;
--color-green-200: #32a467;
--color-green-100: #72ca9b;
// Red colors
--color-red-500: #8e292c;
--color-red-400: #ac2f33;
--color-red-300: #cd4246;
--color-red-200: #e76a6e;
--color-red-100: #fa999c;
// Orange colors
--color-orange-500: #77450d;
--color-orange-400: #935610;
--color-orange-300: #c87619;
--color-orange-200: #ec9a3c;
--color-orange-100: #fbb360;
--color-dark-gray5: #404854; --color-dark-gray5: #404854;
--color-dark-gray4: #383e47; --color-dark-gray4: #383e47;
--color-dark-gray3: #2f343c; --color-dark-gray3: #2f343c;
@@ -301,6 +322,27 @@ body.bp4-dark {
--color-primary: #8abbff; --color-primary: #8abbff;
--color-danger: rgb(213, 103, 103); --color-danger: rgb(213, 103, 103);
// Green colors (dark mode - lighter variants)
--color-green-500: #72ca9b;
--color-green-400: #32a467;
--color-green-300: #238551;
--color-green-200: #1c6e42;
--color-green-100: #165a36;
// Red colors (dark mode - lighter variants)
--color-red-500: #fa999c;
--color-red-400: #e76a6e;
--color-red-300: #cd4246;
--color-red-200: #ac2f33;
--color-red-100: #8e292c;
// Orange colors (dark mode - lighter variants)
--color-orange-500: #fbb360;
--color-orange-400: #ec9a3c;
--color-orange-300: #c87619;
--color-orange-200: #935610;
--color-orange-100: #77450d;
--color-dark-gray5: #404854; --color-dark-gray5: #404854;
--color-dark-gray4: #383e47; --color-dark-gray4: #383e47;
--color-dark-gray3: #2f343c; --color-dark-gray3: #2f343c;

View File

@@ -1,200 +0,0 @@
.universal-search {
position: fixed;
filter: blur(0);
opacity: 1;
background-color: var(--color-universal-search-background);
border-radius: 3px;
box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.1),
0 4px 8px rgba(16, 22, 26, 0.2),
0 18px 46px 6px rgba(16, 22, 26, 0.2);
left: calc(50% - 250px);
top: 20vh;
width: 500px;
z-index: 20;
&.bp4-overlay-appear,
&.bp4-overlay-enter {
filter: blur(20px);
opacity: 0.2;
}
&.bp4-overlay-appear-active,
&.bp4-overlay-enter-active {
filter: blur(0);
opacity: 1;
transition-delay: 0;
transition-duration: 0.2s;
transition-property: filter, opacity;
transition-timing-function: cubic-bezier(0.4, 1, 0.75, 0.9);
}
&.bp4-overlay-exit {
filter: blur(0);
opacity: 1;
}
&.bp4-overlay-exit-active {
filter: blur(20px);
opacity: 0.2;
transition-delay: 0;
transition-duration: 0.2s;
transition-property: filter, opacity;
transition-timing-function: cubic-bezier(0.4, 1, 0.75, 0.9);
}
&__omnibar {
.bp4-input-group {
.bp4-icon {
svg {
stroke: currentColor;
fill: none;
fill-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
}
}
}
.bp4-input-group .bp4-input {
border: 0;
box-shadow: 0 0 0 0;
height: 50px;
line-height: 50px;
font-size: 20px;
}
.bp4-input-group.bp4-large .bp4-input:not(:first-child) {
padding-left: 50px !important;
}
.bp4-input-group.bp4-large .bp4-input:not(:last-child) {
padding-right: 130px !important;
}
.bp4-input-group {
.bp4-icon {
margin: 16px;
color: var(--color-universal-search-icon);
svg {
stroke-width: 2;
--text-opacity: 1;
}
}
}
.bp4-menu {
border-top: 1px solid var(--color-universal-search-menu-border);
max-height: calc(60vh - 20px);
overflow: auto;
.bp4-menu-item {
.bp4-text-muted {
font-size: 12px;
.bp4-icon {
color: #8499a7;
}
}
&.bp4-intent-primary {
&.bp4-active {
background-color: rgb(235, 241, 246);
color: #252b30;
.bp4-menu-item-label {
color: #5c7080;
}
}
}
&-label {
flex-direction: row;
text-align: right;
}
}
}
.bp4-input-action {
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
}
}
&__type-select-overlay {
.bp4-button {
margin: 0 !important;
}
}
&__footer {
padding: 12px 12px;
border-top: 1px solid var(--color-universal-search-footer-divider);
}
&__actions {
display: flex;
}
&__action {
&:not(:first-of-type) {
margin-left: 14px;
}
.bp4-tag {
background: var(--color-universal-search-tag-background);
color: var(--color-universal-search-tag-text);
}
&--arrows {
.bp4-tag {
padding: 0;
text-align: center;
line-height: 16px;
margin-left: 4px;
svg {
fill: var(--color-universal-search-tag-text);
height: 100%;
display: block;
width: 100%;
padding: 2px;
}
}
}
.text {
margin-left: 6px;
}
}
&__footer {
}
&-input-right-elements {
display: flex;
margin: 10px;
.bp4-spinner {
margin-right: 6px;
}
}
&__item {
&--invoice,
&--estimate,
&--bill,
&--receipt {
.amount {
color: #252b30;
}
.status {
font-size: 13px;
&.status-warning {
color: rgb(236, 91, 10);
}
&.status-success {
color: #249017;
}
}
}
}
}