Skip to content
Merged
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
46 changes: 46 additions & 0 deletions apps/docs/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3196,6 +3196,52 @@ export function LemlistIcon(props: SVGProps<SVGSVGElement>) {
)
}

/**
* The four brand fills are hardcoded rather than `currentColor`: this is a
* multi-color mark, so it stays legible on the white tile and bare on a neutral
* page in both themes. No `iconColor` is set for the same reason — there is no
* single brand tint to adopt.
*
* The source SVG carried its fills through `.st0`–`.st3` CSS classes; those are
* inlined here so the mark cannot depend on (or leak) global styles.
*/
export function ManageEngineIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox='0 0 120.2 118.8'
fill='none'
xmlns='http://www.w3.org/2000/svg'
aria-hidden='true'
>
<path
fillRule='evenodd'
clipRule='evenodd'
fill='#FFCD22'
d='M83.1,3.9c46.2,32.2,27,106.8-22.5,114.9l0,0C124.1,118.8,144.8,32.7,83.1,3.9'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
fill='#0078B6'
d='M33.9,3.4c55.2-8.1,77.8,60,53.6,91.5C127.5,54.7,95.5-16.4,33.9,3.4'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
fill='#009A50'
d='M1.3,36.7C28.5-3.6,85,21.3,90.7,62C97.2,11.7,30.8-16.2,1.3,36.7'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
fill='#CA2031'
d='M0,66.8C6.6,25,61,20.6,80.6,49.9C64.6,8.3,1.1,13.4,0,66.8'
/>
</svg>
)
}

export function TelegramIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/components/ui/icon-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ import {
MailchimpIcon,
MailgunIcon,
MailServerIcon,
ManageEngineIcon,
Mem0Icon,
MicrosoftDataverseIcon,
MicrosoftExcelIcon,
Expand Down Expand Up @@ -448,6 +449,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
mailchimp: MailchimpIcon,
mailgun: MailgunIcon,
managed_agent: ClaudeIcon,
manageengine_sdp: ManageEngineIcon,
mem0: Mem0Icon,
memory: BrainIcon,
microsoft_ad: AzureIcon,
Expand Down
1,396 changes: 1,396 additions & 0 deletions apps/docs/content/docs/integrations/manageengine_sdp.mdx

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/docs/content/docs/integrations/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
"mailchimp",
"mailgun",
"managed_agent",
"manageengine_sdp",
"mem0",
"memory",
"microsoft_ad",
Expand Down
133 changes: 133 additions & 0 deletions apps/sim/blocks/blocks/manageengine-sdp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { ManageEngineSdpBlock } from '@/blocks/blocks/manageengine-sdp'
import {
buildSdpChangeEntity,
buildSdpSolutionEntity,
} from '@/tools/manageengine_sdp/entity-builders'

type Params = Record<string, unknown>

const mapParams = (params: Params): Params =>
ManageEngineSdpBlock.tools.config?.params?.(params as never) as Params

const selectTool = (params: Params): string =>
ManageEngineSdpBlock.tools.config?.tool?.(params as never) as string

describe('ManageEngine SDP tool selection', () => {
it('maps every operation option to a tool the block declares access to', () => {
const operation = ManageEngineSdpBlock.subBlocks.find((s) => s.id === 'operation') as {
options: { id: string }[]
}
const access = new Set(ManageEngineSdpBlock.tools.access)
for (const { id } of operation.options) {
expect(access.has(selectTool({ operation: id }))).toBe(true)
}
// Every declared tool is reachable — no dead entries in tools.access.
const reachable = new Set(operation.options.map(({ id }) => selectTool({ operation: id })))
expect([...access].filter((id) => !reachable.has(id))).toEqual([])
})
})

describe('ManageEngine SDP cross-operation scoping', () => {
it("never sends another module's record id", () => {
// Every id field populated, as they would be after switching operations.
const stale = {
requestId: 'r1',
problemId: 'p1',
changeId: 'c1',
assetId: 'a1',
solutionId: 's1',
}
const mapped = mapParams({ operation: 'get_request', ...stale })
expect(mapped.requestId).toBe('r1')
expect(mapped.problemId).toBeUndefined()
expect(mapped.changeId).toBeUndefined()
expect(mapped.assetId).toBeUndefined()
expect(mapped.solutionId).toBeUndefined()
})

it("routes each module's title into the shared `title` param and clears the rest", () => {
const titles = { problemTitle: 'P', changeTitle: 'C', solutionTitle: 'S' }
expect(mapParams({ operation: 'create_problem', ...titles }).title).toBe('P')
expect(mapParams({ operation: 'create_change', ...titles }).title).toBe('C')
expect(mapParams({ operation: 'create_solution', ...titles }).title).toBe('S')
// A request write uses `subject`, so `title` must not leak into it.
expect(
mapParams({ operation: 'create_request', subject: 'R', ...titles }).title
).toBeUndefined()
})

it('does not carry a stale request status onto a change write', () => {
const mapped = mapParams({
operation: 'create_change',
status: 'Resolved',
changeStatus: 'Open',
})
expect(mapped.status).toBe('Open')
})
})

/**
* Regression: a switch cannot distinguish "untouched" from "explicitly off", so
* using one on an edit operation silently overwrites server state the user never
* intended to change. ServiceDesk Plus documents no default for either flag, so
* the create side still sends its value explicitly.
*/
describe('ManageEngine SDP edit-safe booleans', () => {
it('omits `emergency` when an update leaves the tri-state unchanged', () => {
const mapped = mapParams({
operation: 'update_change',
changeId: '99',
scheduledStartTime: '2026-09-10T09:00:00.000Z',
updateEmergency: '',
})
expect(mapped.emergency).toBeUndefined()
expect(buildSdpChangeEntity({ accessToken: 't', ...mapped } as never)).not.toHaveProperty(
'emergency'
)
})

it('sends `emergency` on an update only when explicitly chosen', () => {
expect(
mapParams({ operation: 'update_change', changeId: '99', updateEmergency: 'true' }).emergency
).toBe(true)
expect(
mapParams({ operation: 'update_change', changeId: '99', updateEmergency: 'false' }).emergency
).toBe(false)
})

it('sends the switch value explicitly on create, where there is no prior state', () => {
const mapped = mapParams({ operation: 'create_change', changeTitle: 'T', emergency: false })
expect(mapped.emergency).toBe(false)
expect(buildSdpChangeEntity({ accessToken: 't', ...mapped } as never)).toHaveProperty(
'emergency',
false
)
})

it('omits `is_public` when an update leaves the tri-state unchanged', () => {
const mapped = mapParams({
operation: 'update_solution',
solutionId: '55',
solutionTitle: 'New title',
updateSolutionIsPublic: '',
})
expect(mapped.isPublic).toBeUndefined()
expect(buildSdpSolutionEntity({ accessToken: 't', ...mapped } as never)).not.toHaveProperty(
'is_public'
)
})

it('sends `is_public` on an update only when explicitly chosen', () => {
expect(
mapParams({
operation: 'update_solution',
solutionId: '55',
updateSolutionIsPublic: 'false',
}).isPublic
).toBe(false)
})
})
Loading
Loading