@ikatec/digisac-api-sdk 4.4.1 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apis/cards/CardsApi.cjs +70 -0
- package/dist/apis/cards/CardsApi.cjs.map +1 -1
- package/dist/apis/cards/CardsApi.d.ts +53 -1
- package/dist/apis/cards/CardsApi.d.ts.map +1 -1
- package/dist/apis/cards/CardsApi.mjs +70 -0
- package/dist/apis/cards/CardsApi.mjs.map +1 -1
- package/dist/apis/cards/types.d.ts +84 -0
- package/dist/apis/cards/types.d.ts.map +1 -1
- package/dist/apis/kanbanBoards/types.d.ts +33 -4
- package/dist/apis/kanbanBoards/types.d.ts.map +1 -1
- package/dist/apis/kanbanCards/KanbanCardsApi.cjs +8 -0
- package/dist/apis/kanbanCards/KanbanCardsApi.cjs.map +1 -1
- package/dist/apis/kanbanCards/KanbanCardsApi.d.ts +7 -1
- package/dist/apis/kanbanCards/KanbanCardsApi.d.ts.map +1 -1
- package/dist/apis/kanbanCards/KanbanCardsApi.mjs +8 -0
- package/dist/apis/kanbanCards/KanbanCardsApi.mjs.map +1 -1
- package/dist/apis/kanbanCards/types.d.ts +17 -5
- package/dist/apis/kanbanCards/types.d.ts.map +1 -1
- package/dist/apis/kanbanColumns/KanbanColumnsApi.cjs +9 -0
- package/dist/apis/kanbanColumns/KanbanColumnsApi.cjs.map +1 -1
- package/dist/apis/kanbanColumns/KanbanColumnsApi.d.ts +8 -1
- package/dist/apis/kanbanColumns/KanbanColumnsApi.d.ts.map +1 -1
- package/dist/apis/kanbanColumns/KanbanColumnsApi.mjs +9 -0
- package/dist/apis/kanbanColumns/KanbanColumnsApi.mjs.map +1 -1
- package/dist/apis/kanbanColumns/types.d.ts +14 -3
- package/dist/apis/kanbanColumns/types.d.ts.map +1 -1
- package/dist/apis/permissions/types.d.ts +1 -1
- package/dist/apis/permissions/types.d.ts.map +1 -1
- package/dist/apis/pipeline/PipelineApi.cjs +133 -0
- package/dist/apis/pipeline/PipelineApi.cjs.map +1 -1
- package/dist/apis/pipeline/PipelineApi.d.ts +102 -1
- package/dist/apis/pipeline/PipelineApi.d.ts.map +1 -1
- package/dist/apis/pipeline/PipelineApi.mjs +133 -0
- package/dist/apis/pipeline/PipelineApi.mjs.map +1 -1
- package/dist/apis/pipeline/types.d.ts +164 -0
- package/dist/apis/pipeline/types.d.ts.map +1 -1
- package/dist/apis/tickets/types.d.ts +37 -1
- package/dist/apis/tickets/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/apis/cards/CardsApi.test.ts +128 -0
- package/src/apis/cards/CardsApi.ts +124 -1
- package/src/apis/cards/types.ts +81 -0
- package/src/apis/kanbanBoards/types.ts +36 -4
- package/src/apis/kanbanCards/KanbanCardsApi.test.ts +46 -0
- package/src/apis/kanbanCards/KanbanCardsApi.ts +23 -1
- package/src/apis/kanbanCards/types.ts +18 -5
- package/src/apis/kanbanColumns/KanbanColumnsApi.test.ts +46 -0
- package/src/apis/kanbanColumns/KanbanColumnsApi.ts +24 -1
- package/src/apis/kanbanColumns/types.ts +15 -3
- package/src/apis/payloads.test-d.ts +53 -1
- package/src/apis/permissions/types.ts +10 -4
- package/src/apis/pipeline/PipelineApi.test.ts +193 -0
- package/src/apis/pipeline/PipelineApi.ts +274 -1
- package/src/apis/pipeline/types.ts +162 -0
- package/src/apis/tickets/types.ts +40 -1
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
import type { Department } from '../departments/types'
|
|
2
2
|
import type { KanbanColumn } from '../kanbanColumns/types'
|
|
3
3
|
|
|
4
|
+
export type KanbanBoardVisibleTickets = 'mine' | 'all' | 'mine-and-not-assigned'
|
|
5
|
+
|
|
6
|
+
export type KanbanBoardClosedMaxTimeUnit = 'minutes' | 'hours' | 'days' | 'weeks' | 'months'
|
|
7
|
+
|
|
8
|
+
/** Card fields shown on the board. */
|
|
9
|
+
export type KanbanBoardSettings = {
|
|
10
|
+
lastMessage: boolean
|
|
11
|
+
departmentAndUser: boolean
|
|
12
|
+
tags: boolean
|
|
13
|
+
createdAtDate: boolean
|
|
14
|
+
newInteractions: boolean
|
|
15
|
+
timeoutAlert: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
4
18
|
export type KanbanBoard = {
|
|
5
19
|
id: string
|
|
6
20
|
name: string
|
|
7
|
-
timeoutAlertEnabled: boolean
|
|
8
21
|
accountId: string
|
|
22
|
+
visibleTickets: KanbanBoardVisibleTickets | null
|
|
23
|
+
closedMaxTime: number | null
|
|
24
|
+
closedMaxTimeUnit: KanbanBoardClosedMaxTimeUnit | null
|
|
25
|
+
/** `'initializing'` while the cards of the open tickets are being created. */
|
|
26
|
+
status: 'initializing' | 'ready'
|
|
27
|
+
settings: KanbanBoardSettings
|
|
9
28
|
createdAt: string
|
|
10
29
|
updatedAt: string
|
|
11
|
-
deletedAt: string | null
|
|
12
30
|
// relationships
|
|
13
31
|
departments?: Department[]
|
|
14
32
|
columns?: KanbanColumn[]
|
|
@@ -18,12 +36,26 @@ export type KanbanBoardRelationships = 'departments' | 'columns'
|
|
|
18
36
|
|
|
19
37
|
export type CreateKanbanBoardPayload = {
|
|
20
38
|
name: string
|
|
21
|
-
timeoutAlertEnabled?: boolean
|
|
22
39
|
/** Department IDs to link to the board. Required by the backend on creation. */
|
|
23
40
|
departmentIds: string[]
|
|
41
|
+
visibleTickets?: KanbanBoardVisibleTickets
|
|
42
|
+
/** Greater than 0. */
|
|
43
|
+
closedMaxTime?: number
|
|
44
|
+
closedMaxTimeUnit?: KanbanBoardClosedMaxTimeUnit
|
|
45
|
+
settings?: Partial<KanbanBoardSettings>
|
|
24
46
|
}
|
|
25
47
|
|
|
26
48
|
export type UpdateKanbanBoardPayload = {
|
|
27
49
|
name?: string
|
|
28
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Replaces the departments of the board (at least one): removing a department deletes the
|
|
52
|
+
* cards of its tickets, adding one creates cards for its open tickets.
|
|
53
|
+
*/
|
|
54
|
+
departmentIds?: string[]
|
|
55
|
+
visibleTickets?: KanbanBoardVisibleTickets
|
|
56
|
+
/** Greater than 0. */
|
|
57
|
+
closedMaxTime?: number
|
|
58
|
+
closedMaxTimeUnit?: KanbanBoardClosedMaxTimeUnit
|
|
59
|
+
/** Merged into the current settings — send only the keys to change. */
|
|
60
|
+
settings?: Partial<KanbanBoardSettings>
|
|
29
61
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import type { ApiClient } from '../../core/ApiClient'
|
|
3
|
+
import { KanbanCardsApi } from './KanbanCardsApi'
|
|
4
|
+
|
|
5
|
+
// ── Mock client factory ──────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
function createMockClient(): ApiClient {
|
|
8
|
+
return {
|
|
9
|
+
setAccessToken: vi.fn().mockReturnThis(),
|
|
10
|
+
request: vi.fn(),
|
|
11
|
+
get: vi.fn(),
|
|
12
|
+
post: vi.fn(),
|
|
13
|
+
put: vi.fn(),
|
|
14
|
+
patch: vi.fn(),
|
|
15
|
+
delete: vi.fn(),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const headers = { 'X-Custom': 'value' }
|
|
20
|
+
|
|
21
|
+
// ── Tests ────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
describe('KanbanCardsApi', () => {
|
|
24
|
+
let client: ReturnType<typeof createMockClient>
|
|
25
|
+
let api: KanbanCardsApi
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
client = createMockClient()
|
|
29
|
+
api = new KanbanCardsApi(client)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('reorder', () => {
|
|
33
|
+
it('calls client.patch with /kanban-cards/:id/reorder and body', async () => {
|
|
34
|
+
vi.mocked(client.patch).mockResolvedValue({ id: 'kc1', position: 'N' })
|
|
35
|
+
const body = { columnId: 'col2', moveTo: 'end' as const }
|
|
36
|
+
const result = await api.reorder('kc1', body, headers)
|
|
37
|
+
expect(client.patch).toHaveBeenCalledWith('/kanban-cards/kc1/reorder', body, headers)
|
|
38
|
+
expect(result).toEqual({ id: 'kc1', position: 'N' })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('propagates errors', async () => {
|
|
42
|
+
vi.mocked(client.patch).mockRejectedValue(new Error('Invalid reorder operation.'))
|
|
43
|
+
await expect(api.reorder('kc1', {})).rejects.toThrow('Invalid reorder operation.')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ApiClient } from '../../core/ApiClient'
|
|
2
2
|
import { BaseCrudApi } from '../../core/BaseCrudApi'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
KanbanCard,
|
|
5
|
+
CreateKanbanCardPayload,
|
|
6
|
+
ReorderKanbanCardPayload,
|
|
7
|
+
UpdateKanbanCardPayload,
|
|
8
|
+
} from './types'
|
|
4
9
|
|
|
5
10
|
export class KanbanCardsApi extends BaseCrudApi<
|
|
6
11
|
KanbanCard,
|
|
@@ -10,4 +15,21 @@ export class KanbanCardsApi extends BaseCrudApi<
|
|
|
10
15
|
constructor(client: ApiClient) {
|
|
11
16
|
super(client, '/kanban-cards')
|
|
12
17
|
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Moves a card within its column or to another column (see {@link ReorderKanbanCardPayload}).
|
|
21
|
+
* Resolves to the card with its new `position` and `columnId`.
|
|
22
|
+
* @permissions kanban.cards.move
|
|
23
|
+
*/
|
|
24
|
+
reorder(
|
|
25
|
+
id: string,
|
|
26
|
+
body: ReorderKanbanCardPayload,
|
|
27
|
+
headers?: Record<string, string>,
|
|
28
|
+
): Promise<KanbanCard> {
|
|
29
|
+
return this.client.patch<KanbanCard, ReorderKanbanCardPayload>(
|
|
30
|
+
`${this.basePath}/${id}/reorder`,
|
|
31
|
+
body,
|
|
32
|
+
headers,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
13
35
|
}
|
|
@@ -3,7 +3,6 @@ import type { Ticket } from '../tickets/types'
|
|
|
3
3
|
|
|
4
4
|
export type KanbanCard = {
|
|
5
5
|
id: string
|
|
6
|
-
ticketSubject: string | null
|
|
7
6
|
position: string
|
|
8
7
|
columnId: string
|
|
9
8
|
ticketId: string
|
|
@@ -12,10 +11,9 @@ export type KanbanCard = {
|
|
|
12
11
|
lastColumnChangeAt: string | null
|
|
13
12
|
createdAt: string
|
|
14
13
|
updatedAt: string
|
|
15
|
-
deletedAt: string | null
|
|
16
14
|
// relationships
|
|
17
|
-
column?: KanbanColumn
|
|
18
|
-
ticket?: Ticket
|
|
15
|
+
column?: KanbanColumn | null
|
|
16
|
+
ticket?: Ticket | null
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
export type KanbanCardRelationships = 'column' | 'ticket'
|
|
@@ -29,5 +27,20 @@ export type CreateKanbanCardPayload = {
|
|
|
29
27
|
export type UpdateKanbanCardPayload = {
|
|
30
28
|
columnId?: string
|
|
31
29
|
position?: string
|
|
32
|
-
|
|
30
|
+
/** The caller must share a department with the ticket (or transfer to all / be admin). */
|
|
31
|
+
ticketId?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Where the card goes. The target column is `columnId`, else the column of the neighbours, else
|
|
36
|
+
* the current one; `moveTo` puts it first/last of the target column. Moving into the `'final'`
|
|
37
|
+
* column closes the ticket, moving out of it reopens the ticket.
|
|
38
|
+
*/
|
|
39
|
+
export type ReorderKanbanCardPayload = {
|
|
40
|
+
/** The card that ends up above. Must be in the same column as `afterCardId`. */
|
|
41
|
+
beforeCardId?: string
|
|
42
|
+
/** The card that ends up below. */
|
|
43
|
+
afterCardId?: string
|
|
44
|
+
columnId?: string
|
|
45
|
+
moveTo?: 'start' | 'end'
|
|
33
46
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import type { ApiClient } from '../../core/ApiClient'
|
|
3
|
+
import { KanbanColumnsApi } from './KanbanColumnsApi'
|
|
4
|
+
|
|
5
|
+
// ── Mock client factory ──────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
function createMockClient(): ApiClient {
|
|
8
|
+
return {
|
|
9
|
+
setAccessToken: vi.fn().mockReturnThis(),
|
|
10
|
+
request: vi.fn(),
|
|
11
|
+
get: vi.fn(),
|
|
12
|
+
post: vi.fn(),
|
|
13
|
+
put: vi.fn(),
|
|
14
|
+
patch: vi.fn(),
|
|
15
|
+
delete: vi.fn(),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const headers = { 'X-Custom': 'value' }
|
|
20
|
+
|
|
21
|
+
// ── Tests ────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
describe('KanbanColumnsApi', () => {
|
|
24
|
+
let client: ReturnType<typeof createMockClient>
|
|
25
|
+
let api: KanbanColumnsApi
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
client = createMockClient()
|
|
29
|
+
api = new KanbanColumnsApi(client)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('reorder', () => {
|
|
33
|
+
it('calls client.patch with /kanban-columns/:id/reorder and body', async () => {
|
|
34
|
+
vi.mocked(client.patch).mockResolvedValue({ id: 'col1', position: 'N' })
|
|
35
|
+
const body = { beforeColumnId: 'col0', afterColumnId: 'col2' }
|
|
36
|
+
const result = await api.reorder('col1', body, headers)
|
|
37
|
+
expect(client.patch).toHaveBeenCalledWith('/kanban-columns/col1/reorder', body, headers)
|
|
38
|
+
expect(result).toEqual({ id: 'col1', position: 'N' })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('propagates errors', async () => {
|
|
42
|
+
vi.mocked(client.patch).mockRejectedValue(new Error('Invalid reorder operation.'))
|
|
43
|
+
await expect(api.reorder('col1', {})).rejects.toThrow('Invalid reorder operation.')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ApiClient } from '../../core/ApiClient'
|
|
2
2
|
import { BaseCrudApi } from '../../core/BaseCrudApi'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
KanbanColumn,
|
|
5
|
+
CreateKanbanColumnPayload,
|
|
6
|
+
ReorderKanbanColumnPayload,
|
|
7
|
+
UpdateKanbanColumnPayload,
|
|
8
|
+
} from './types'
|
|
4
9
|
|
|
5
10
|
export class KanbanColumnsApi extends BaseCrudApi<
|
|
6
11
|
KanbanColumn,
|
|
@@ -10,4 +15,22 @@ export class KanbanColumnsApi extends BaseCrudApi<
|
|
|
10
15
|
constructor(client: ApiClient) {
|
|
11
16
|
super(client, '/kanban-columns')
|
|
12
17
|
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Moves a column between two neighbours of the same board. Both the column and a neighbour
|
|
21
|
+
* must be draggable; swapped neighbours (`before` after `after`) fail with 500. Resolves to the
|
|
22
|
+
* column with its new `position`.
|
|
23
|
+
* @permissions kanban.columns.update
|
|
24
|
+
*/
|
|
25
|
+
reorder(
|
|
26
|
+
id: string,
|
|
27
|
+
body: ReorderKanbanColumnPayload,
|
|
28
|
+
headers?: Record<string, string>,
|
|
29
|
+
): Promise<KanbanColumn> {
|
|
30
|
+
return this.client.patch<KanbanColumn, ReorderKanbanColumnPayload>(
|
|
31
|
+
`${this.basePath}/${id}/reorder`,
|
|
32
|
+
body,
|
|
33
|
+
headers,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
13
36
|
}
|
|
@@ -14,18 +14,22 @@ export type KanbanColumn = {
|
|
|
14
14
|
accountId: string
|
|
15
15
|
createdAt: string
|
|
16
16
|
updatedAt: string
|
|
17
|
-
deletedAt: string | null
|
|
18
17
|
// relationships
|
|
19
|
-
|
|
18
|
+
board?: KanbanBoard | null
|
|
20
19
|
cards?: KanbanCard[]
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
export type KanbanColumnRelationships = '
|
|
22
|
+
export type KanbanColumnRelationships = 'cards'
|
|
24
23
|
|
|
25
24
|
export type CreateKanbanColumnPayload = {
|
|
26
25
|
name: string
|
|
27
26
|
kanbanBoardId: string
|
|
27
|
+
/** A board has one `'initial'` and one `'final'` column (a second one fails). */
|
|
28
28
|
role?: KanbanColumnRole
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated Ignored by the backend: the position is computed (`'initial'` first, `'final'`
|
|
31
|
+
* last, other columns just before the final one). Use `reorder` to move a column.
|
|
32
|
+
*/
|
|
29
33
|
position?: string
|
|
30
34
|
ticketWaitTimeMinutes?: number
|
|
31
35
|
isDraggable?: boolean
|
|
@@ -38,3 +42,11 @@ export type UpdateKanbanColumnPayload = {
|
|
|
38
42
|
ticketWaitTimeMinutes?: number | null
|
|
39
43
|
isDraggable?: boolean
|
|
40
44
|
}
|
|
45
|
+
|
|
46
|
+
/** Neighbours of the column after the move (omit both to move it to the middle, `'M'`). */
|
|
47
|
+
export type ReorderKanbanColumnPayload = {
|
|
48
|
+
/** The column that ends up on the left. */
|
|
49
|
+
beforeColumnId?: string
|
|
50
|
+
/** The column that ends up on the right. */
|
|
51
|
+
afterColumnId?: string
|
|
52
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { expectError } from 'tsd'
|
|
1
|
+
import { expectAssignable, expectError, expectNotAssignable, expectType } from 'tsd'
|
|
2
2
|
import type { ApiClient } from '../core/ApiClient'
|
|
3
|
+
import { CardsApi } from './cards/CardsApi'
|
|
3
4
|
import { ContactsApi } from './contacts/ContactsApi'
|
|
5
|
+
import { KanbanColumnsApi } from './kanbanColumns/KanbanColumnsApi'
|
|
6
|
+
import { PipelineApi } from './pipeline/PipelineApi'
|
|
7
|
+
import type { PermissionName } from './permissions/types'
|
|
4
8
|
import { PersonalAccessTokensApi } from './personalAccessTokens/PersonalAccessTokensApi'
|
|
5
9
|
import { RolesApi } from './roles/RolesApi'
|
|
6
10
|
import { TagsApi } from './tags/TagsApi'
|
|
11
|
+
import { TicketsApi } from './tickets/TicketsApi'
|
|
12
|
+
import type { Ticket, TicketTransfer } from './tickets/types'
|
|
7
13
|
import { TimetableApi } from './timetable/TimetableApi'
|
|
8
14
|
import { UsersApi } from './users/UsersApi'
|
|
9
15
|
|
|
@@ -63,3 +69,49 @@ const tags = new TagsApi(client)
|
|
|
63
69
|
tags.attachContacts('t-1', { where: { id: { $in: ['c-1', 'c-2'] } } })
|
|
64
70
|
expectError(tags.attachContacts('t-1', {}))
|
|
65
71
|
expectError(tags.detachContacts('t-1', { include: ['tags'] }))
|
|
72
|
+
|
|
73
|
+
// ── pipelines: moving the cards of a deleted status needs the target ─
|
|
74
|
+
|
|
75
|
+
const pipelines = new PipelineApi(client)
|
|
76
|
+
pipelines.deleteStatus('st-1', { action: 'move-to-another', newStatusId: 'st-2' })
|
|
77
|
+
pipelines.deleteStatus('st-1', { action: 'keep-without-status' })
|
|
78
|
+
expectError(pipelines.deleteStatus('st-1', { action: 'move-to-another' }))
|
|
79
|
+
expectError(pipelines.createAutomation({ pipelineId: 'p-1', type: 'email' }))
|
|
80
|
+
expectError(pipelines.deleteStage('s-1', { typeAction: 'archive' }))
|
|
81
|
+
|
|
82
|
+
// ── cards: getCards is not scoped by the backend and only answers the attributes asked ─
|
|
83
|
+
|
|
84
|
+
const cards = new CardsApi(client)
|
|
85
|
+
cards.getCards({
|
|
86
|
+
where: { accountId: 'a-1', pipelineId: 'p-1' },
|
|
87
|
+
attributes: ['id', 'title'],
|
|
88
|
+
order: [['contact.name', 'ASC']],
|
|
89
|
+
})
|
|
90
|
+
expectError(cards.getCards({ where: { pipelineId: 'p-1' }, attributes: ['id'] }))
|
|
91
|
+
expectError(cards.getCards({ where: { accountId: 'a-1' } }))
|
|
92
|
+
expectError(cards.saveComment({ comment: 'no card nor id' }))
|
|
93
|
+
|
|
94
|
+
// ── kanban columns: the backend computes the position ───────────────
|
|
95
|
+
|
|
96
|
+
const columns = new KanbanColumnsApi(client)
|
|
97
|
+
columns.reorder('col-1', { beforeColumnId: 'col-0', afterColumnId: 'col-2' })
|
|
98
|
+
expectError(columns.reorder('col-1', { position: 'M' }))
|
|
99
|
+
|
|
100
|
+
// ── permissions: the kanban permissions were split by the migration 20260209144630 ──
|
|
101
|
+
|
|
102
|
+
expectAssignable<PermissionName>('kanban.boards.view')
|
|
103
|
+
expectAssignable<PermissionName>('kanban.cards.move')
|
|
104
|
+
expectAssignable<PermissionName>('users.export')
|
|
105
|
+
expectNotAssignable<PermissionName>('kanban.view')
|
|
106
|
+
expectNotAssignable<PermissionName>('kanban.update')
|
|
107
|
+
|
|
108
|
+
// ── tickets: the history of the ticket comes in the ticketTransfers include ──
|
|
109
|
+
|
|
110
|
+
const tickets = new TicketsApi(client)
|
|
111
|
+
tickets.getById('t-1', { include: ['ticketTransfers'] })
|
|
112
|
+
tickets.getById('t-1', { include: [{ model: 'ticketTransfers', include: ['byUser'] }] })
|
|
113
|
+
declare const ticket: Ticket
|
|
114
|
+
expectType<TicketTransfer[] | undefined>(ticket.ticketTransfers)
|
|
115
|
+
declare const transfer: TicketTransfer
|
|
116
|
+
expectType<string | null>(transfer.byUserId)
|
|
117
|
+
expectType<'opened' | 'transferred' | 'closed' | null>(transfer.action)
|
|
@@ -47,6 +47,7 @@ export type PermissionName =
|
|
|
47
47
|
| 'users.create'
|
|
48
48
|
| 'users.update'
|
|
49
49
|
| 'users.archive'
|
|
50
|
+
| 'users.export'
|
|
50
51
|
| 'roles.view'
|
|
51
52
|
| 'roles.create'
|
|
52
53
|
| 'roles.update'
|
|
@@ -205,10 +206,15 @@ export type PermissionName =
|
|
|
205
206
|
| 'knowledgeBase.create'
|
|
206
207
|
| 'knowledgeBase.update'
|
|
207
208
|
| 'knowledgeBase.destroy'
|
|
208
|
-
|
|
209
|
-
| 'kanban.
|
|
210
|
-
| 'kanban.
|
|
211
|
-
| 'kanban.
|
|
209
|
+
// The migration 20260209144630 replaced kanban.view/create/update/destroy with the permissions below.
|
|
210
|
+
| 'kanban.boards.view'
|
|
211
|
+
| 'kanban.boards.create'
|
|
212
|
+
| 'kanban.boards.update'
|
|
213
|
+
| 'kanban.boards.destroy'
|
|
214
|
+
| 'kanban.columns.create'
|
|
215
|
+
| 'kanban.columns.update'
|
|
216
|
+
| 'kanban.columns.destroy'
|
|
217
|
+
| 'kanban.cards.move'
|
|
212
218
|
|
|
213
219
|
/**
|
|
214
220
|
* A permission, as `GET /permissions` answers it (the transformer only picks these fields).
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import type { ApiClient } from '../../core/ApiClient'
|
|
3
|
+
import { PipelineApi } from './PipelineApi'
|
|
4
|
+
|
|
5
|
+
// ── Mock client factory ──────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
function createMockClient(): ApiClient {
|
|
8
|
+
return {
|
|
9
|
+
setAccessToken: vi.fn().mockReturnThis(),
|
|
10
|
+
request: vi.fn(),
|
|
11
|
+
get: vi.fn(),
|
|
12
|
+
post: vi.fn(),
|
|
13
|
+
put: vi.fn(),
|
|
14
|
+
patch: vi.fn(),
|
|
15
|
+
delete: vi.fn(),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const headers = { 'X-Custom': 'value' }
|
|
20
|
+
|
|
21
|
+
// ── Tests ────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
describe('PipelineApi', () => {
|
|
24
|
+
let client: ReturnType<typeof createMockClient>
|
|
25
|
+
let api: PipelineApi
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
client = createMockClient()
|
|
29
|
+
api = new PipelineApi(client)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('stages', () => {
|
|
33
|
+
const stage = { name: 'Proposta', pipelineId: 'p1', position: 2 }
|
|
34
|
+
|
|
35
|
+
it('createStage calls client.post with /pipelines/stage and body', async () => {
|
|
36
|
+
vi.mocked(client.post).mockResolvedValue({ id: 's1', ...stage })
|
|
37
|
+
const result = await api.createStage(stage)
|
|
38
|
+
expect(client.post).toHaveBeenCalledWith('/pipelines/stage', stage, undefined)
|
|
39
|
+
expect(result).toEqual({ id: 's1', ...stage })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('createStage resolves to the refusal code the backend answers with 200', async () => {
|
|
43
|
+
vi.mocked(client.post).mockResolvedValue('STAGES_LIMIT')
|
|
44
|
+
await expect(api.createStage(stage)).resolves.toBe('STAGES_LIMIT')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('updateStage calls client.put with /pipelines/stage/:id and body', async () => {
|
|
48
|
+
const body = { ...stage, id: 's1', statuses: [{ id: 'st1', name: 'Novo', position: 1 }] }
|
|
49
|
+
await api.updateStage('s1', body, headers)
|
|
50
|
+
expect(client.put).toHaveBeenCalledWith('/pipelines/stage/s1', body, headers)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('deleteStage calls client.request with DELETE and the destination of the cards', async () => {
|
|
54
|
+
vi.mocked(client.request).mockResolvedValue('MESSAGE_STAGE_DELETED')
|
|
55
|
+
const body = { typeAction: 'move' as const, idNewStage: 's2', user: { id: 'u1' } }
|
|
56
|
+
const result = await api.deleteStage('s1', body)
|
|
57
|
+
expect(client.request).toHaveBeenCalledWith('DELETE', '/pipelines/stage/s1', body, undefined)
|
|
58
|
+
expect(result).toBe('MESSAGE_STAGE_DELETED')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('deleteStage sends an empty body by default', async () => {
|
|
62
|
+
await api.deleteStage('s1')
|
|
63
|
+
expect(client.request).toHaveBeenCalledWith('DELETE', '/pipelines/stage/s1', {}, undefined)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('getStageTotals calls client.post with /pipelines/stage-totals/:id and the filters', async () => {
|
|
67
|
+
const filters = { nameTitle: 'Acme', createdFrom: '01/09/2026' }
|
|
68
|
+
await api.getStageTotals('s1', filters)
|
|
69
|
+
expect(client.post).toHaveBeenCalledWith('/pipelines/stage-totals/s1', filters, undefined)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('getStageTotals sends no filter by default', async () => {
|
|
73
|
+
await api.getStageTotals('s1')
|
|
74
|
+
expect(client.post).toHaveBeenCalledWith('/pipelines/stage-totals/s1', {}, undefined)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('statuses', () => {
|
|
79
|
+
it('getStatuses calls client.get with /pipelines/statuses/:pipelineId', async () => {
|
|
80
|
+
vi.mocked(client.get).mockResolvedValue([{ id: 'st1', name: 'Novo' }])
|
|
81
|
+
const result = await api.getStatuses('p1', headers)
|
|
82
|
+
expect(client.get).toHaveBeenCalledWith('/pipelines/statuses/p1', headers)
|
|
83
|
+
expect(result).toEqual([{ id: 'st1', name: 'Novo' }])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('getStatus calls client.get with /pipelines/status/:id', async () => {
|
|
87
|
+
await api.getStatus('st1')
|
|
88
|
+
expect(client.get).toHaveBeenCalledWith('/pipelines/status/st1', undefined)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('deleteStatus calls client.post with /pipelines/delete-status/:id and the action', async () => {
|
|
92
|
+
vi.mocked(client.post).mockResolvedValue('MESSAGE_STATUS_DELETED')
|
|
93
|
+
const body = { action: 'move-to-another' as const, newStatusId: 'st2' }
|
|
94
|
+
const result = await api.deleteStatus('st1', body)
|
|
95
|
+
expect(client.post).toHaveBeenCalledWith('/pipelines/delete-status/st1', body, undefined)
|
|
96
|
+
expect(result).toBe('MESSAGE_STATUS_DELETED')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('renameStatus posts only newStatusName to the delete-status route', async () => {
|
|
100
|
+
vi.mocked(client.post).mockResolvedValue('MESSAGE_STATUS_RENAMED')
|
|
101
|
+
const result = await api.renameStatus('st1', 'Qualificado', headers)
|
|
102
|
+
expect(client.post).toHaveBeenCalledWith(
|
|
103
|
+
'/pipelines/delete-status/st1',
|
|
104
|
+
{ newStatusName: 'Qualificado' },
|
|
105
|
+
headers,
|
|
106
|
+
)
|
|
107
|
+
expect(result).toBe('MESSAGE_STATUS_RENAMED')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('automations', () => {
|
|
112
|
+
const body = {
|
|
113
|
+
pipelineId: 'p1',
|
|
114
|
+
type: 'notification' as const,
|
|
115
|
+
config: { notify: 'high' as const },
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
it('createAutomation calls client.post with /pipelines/automations and body', async () => {
|
|
119
|
+
await api.createAutomation(body)
|
|
120
|
+
expect(client.post).toHaveBeenCalledWith('/pipelines/automations', body, undefined)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('updateAutomation calls client.put with /pipelines/automations/:id and body', async () => {
|
|
124
|
+
await api.updateAutomation('a1', body, headers)
|
|
125
|
+
expect(client.put).toHaveBeenCalledWith('/pipelines/automations/a1', body, headers)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('deleteAutomation calls client.delete with /pipelines/automations/:id', async () => {
|
|
129
|
+
await api.deleteAutomation('a1')
|
|
130
|
+
expect(client.delete).toHaveBeenCalledWith('/pipelines/automations/a1', undefined)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('getAutomation calls client.get with /pipelines/automations/:pipelineId', async () => {
|
|
134
|
+
vi.mocked(client.get).mockResolvedValue([])
|
|
135
|
+
const result = await api.getAutomation('p1')
|
|
136
|
+
expect(client.get).toHaveBeenCalledWith('/pipelines/automations/p1', undefined)
|
|
137
|
+
expect(result).toEqual([])
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('pipeline actions', () => {
|
|
142
|
+
it('duplicate calls client.post with /pipelines/duplicate/:pipelineId and no body', async () => {
|
|
143
|
+
await api.duplicate('p1', headers)
|
|
144
|
+
expect(client.post).toHaveBeenCalledWith('/pipelines/duplicate/p1', undefined, headers)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('archive posts archive: true to /pipelines/:id/archive', async () => {
|
|
148
|
+
await api.archive('p1')
|
|
149
|
+
expect(client.post).toHaveBeenCalledWith(
|
|
150
|
+
'/pipelines/p1/archive',
|
|
151
|
+
{ archive: true },
|
|
152
|
+
undefined,
|
|
153
|
+
)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('unarchive posts archive: false to /pipelines/:id/archive', async () => {
|
|
157
|
+
await api.unarchive('p1')
|
|
158
|
+
expect(client.post).toHaveBeenCalledWith(
|
|
159
|
+
'/pipelines/p1/archive',
|
|
160
|
+
{ archive: false },
|
|
161
|
+
undefined,
|
|
162
|
+
)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('exportCsv calls client.post with /pipelines/:id/export and resolves to the CSV text', async () => {
|
|
166
|
+
vi.mocked(client.post).mockResolvedValue('Funil;\n')
|
|
167
|
+
const body = { createdAtInit: '2026-09-01 00:00:00', createdAtFinal: '2026-09-30 23:59:59' }
|
|
168
|
+
const result = await api.exportCsv('p1', body)
|
|
169
|
+
expect(client.post).toHaveBeenCalledWith('/pipelines/p1/export', body, undefined)
|
|
170
|
+
expect(result).toBe('Funil;\n')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('getManyWithTotals serializes the query on /pipelines/withTotals', async () => {
|
|
174
|
+
await api.getManyWithTotals({ archive: true })
|
|
175
|
+
const expected = `/pipelines/withTotals?query=${encodeURIComponent(JSON.stringify({ archive: true }))}`
|
|
176
|
+
expect(client.get).toHaveBeenCalledWith(expected, undefined)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('getManyWithTotals sends no query string without a query', async () => {
|
|
180
|
+
await api.getManyWithTotals()
|
|
181
|
+
expect(client.get).toHaveBeenCalledWith('/pipelines/withTotals', undefined)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('error propagation', () => {
|
|
186
|
+
it('propagates errors from createStage', async () => {
|
|
187
|
+
vi.mocked(client.post).mockRejectedValue(new Error('Validation failed'))
|
|
188
|
+
await expect(api.createStage({ name: 'x', pipelineId: 'p1', position: 1 })).rejects.toThrow(
|
|
189
|
+
'Validation failed',
|
|
190
|
+
)
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
})
|