planko-mcp-server 0.4.1 → 0.5.0
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/README.md +43 -1
- package/index.js +70 -13
- package/package.json +2 -2
- package/src/projectLock.js +81 -0
- package/src/sanitize.js +14 -0
package/README.md
CHANGED
|
@@ -158,7 +158,9 @@ These also work with the **same API key** and require **no folder setup**. `list
|
|
|
158
158
|
| `planko_view_task` | View one task by id | `taskId` (required) |
|
|
159
159
|
| `planko_view_note` | View one note by id (shares the view endpoint) | `taskId` (required) |
|
|
160
160
|
|
|
161
|
-
**Scope.** Without `projectId`/`projectName`, a list is scoped to **your own** items. With a project (resolved from `projectName` via your accessible projects), it includes that project's items **including workspace-shared** ones from other members. `view_*` returns the item if you own it or can access its project, otherwise it errors (404 if missing, 403 if not visible). `view_task`/`view_note` do not filter by type — either id resolves; the `task`/`note` label is informational.
|
|
161
|
+
**Scope.** Without `projectId`/`projectName`, a list is scoped to **your own** items. With a project (resolved from `projectName` via your accessible projects), it includes that project's items **including workspace-shared** ones from other members. Use `assigneeId` to narrow a project listing to one member; omit it for all members. `view_*` returns the item if you own it or can access its project, otherwise it errors (404 if missing, 403 if not visible). `view_task`/`view_note` do not filter by type — either id resolves; the `task`/`note` label is informational.
|
|
162
|
+
|
|
163
|
+
**Owner in output.** When the backend populates an item's `userId` as an object `{ _id, name, email }`, list lines and `view_*` detail append `owner: <name>` so you can tell whose task it is in a multi-member project listing. If `userId` is a bare id/string, nothing extra is shown.
|
|
162
164
|
|
|
163
165
|
**List filters** (`planko_list_tasks` accepts all; `planko_list_notes` accepts the applicable subset):
|
|
164
166
|
|
|
@@ -169,6 +171,7 @@ These also work with the **same API key** and require **no folder setup**. `list
|
|
|
169
171
|
| `priority` | `1` \| `2` \| `3` | (tasks only) |
|
|
170
172
|
| `projectName` | string | Resolved to a project id via your accessible projects |
|
|
171
173
|
| `projectId` | ObjectId | Alternative to `projectName` |
|
|
174
|
+
| `assigneeId` | ObjectId | Filter to a single project member by their user id. Omit for **all members**. Applies within a project listing (needs a project scope to be meaningful) |
|
|
172
175
|
| `parentId` | ObjectId | (tasks only) list subtasks of this parent |
|
|
173
176
|
| `tags` | array of ObjectId | AND semantics — item must have all tags |
|
|
174
177
|
| `search` | string | Case-insensitive match on the name |
|
|
@@ -213,6 +216,45 @@ Sync order: delete, pull, push. Local changes overwrite remote on conflict.
|
|
|
213
216
|
|---|---|---|
|
|
214
217
|
| `PLANKO_API_KEY` | Yes | Your MCP API key from app.planko.io/integrations |
|
|
215
218
|
| `PLANKO_API_BASE` | No | API base URL override (defaults to production) |
|
|
219
|
+
| `PLANKO_PROJECT_LOCK` | No | Confine every operation to a single project id (see below). **Off by default** — when unset the server behaves exactly as before. |
|
|
220
|
+
|
|
221
|
+
### Project lock (`PLANKO_PROJECT_LOCK`)
|
|
222
|
+
|
|
223
|
+
Set `PLANKO_PROJECT_LOCK` to a project id to restrict this server instance to that
|
|
224
|
+
one project. This is a **hard override the agent cannot bypass** — useful when a
|
|
225
|
+
shared automation should only ever touch a single project.
|
|
226
|
+
|
|
227
|
+
When set, it changes behavior as follows:
|
|
228
|
+
|
|
229
|
+
- **Create** (`create_task` / `create_note`): the new item is always created in the
|
|
230
|
+
locked project; any `projectName` the caller passes is ignored (and not resolved).
|
|
231
|
+
- **List** (`list_tasks` / `list_notes`): always scoped to the locked project
|
|
232
|
+
(ignoring `projectName`/`projectId`), which yields the all-members listing for
|
|
233
|
+
that project. Combine with `assigneeId` to narrow to one member.
|
|
234
|
+
- **Edit** (`edit_task` / `edit_note`): if the caller supplies a `projectId`, it is
|
|
235
|
+
forced back to the locked project (an edit can never move a task out of it). If no
|
|
236
|
+
project field is supplied, the project is left unchanged.
|
|
237
|
+
- **By-id operations** (`edit_*`, `complete_task`, `delete_*`, `view_*`): the target
|
|
238
|
+
task is fetched first, and the operation is refused with an error if the task does
|
|
239
|
+
not belong to the locked project.
|
|
240
|
+
|
|
241
|
+
**Off by default:** when `PLANKO_PROJECT_LOCK` is unset (or blank), none of the above
|
|
242
|
+
applies and the server is fully backward-compatible.
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"mcpServers": {
|
|
247
|
+
"planko": {
|
|
248
|
+
"command": "npx",
|
|
249
|
+
"args": ["-y", "planko-mcp-server"],
|
|
250
|
+
"env": {
|
|
251
|
+
"PLANKO_API_KEY": "your-api-key",
|
|
252
|
+
"PLANKO_PROJECT_LOCK": "6742635e764bda007ab987ed"
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
216
258
|
|
|
217
259
|
## How it works
|
|
218
260
|
|
package/index.js
CHANGED
|
@@ -20,7 +20,13 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
|
|
22
22
|
import { createApiClient } from './src/api.js';
|
|
23
|
-
import { isBlankValue } from './src/sanitize.js';
|
|
23
|
+
import { isBlankValue, applyDueDateFallback } from './src/sanitize.js';
|
|
24
|
+
import {
|
|
25
|
+
lockProjectId,
|
|
26
|
+
sanitizeFilters,
|
|
27
|
+
ownerName,
|
|
28
|
+
assertProjectAllowed,
|
|
29
|
+
} from './src/projectLock.js';
|
|
24
30
|
import {
|
|
25
31
|
blockNoteToMarkdown,
|
|
26
32
|
markdownToBlockNote,
|
|
@@ -54,6 +60,11 @@ if (!API_KEY) {
|
|
|
54
60
|
|
|
55
61
|
const api = createApiClient({ apiKey: API_KEY });
|
|
56
62
|
|
|
63
|
+
// Optional project lock. When set, EVERY operation is confined to this single
|
|
64
|
+
// project id (a hard override the agent cannot bypass). Read once at startup.
|
|
65
|
+
// When null (the default) behavior is identical to before this feature.
|
|
66
|
+
const PROJECT_LOCK = (process.env.PLANKO_PROJECT_LOCK || '').trim() || null;
|
|
67
|
+
|
|
57
68
|
// --- Tool helpers ---
|
|
58
69
|
|
|
59
70
|
function toolOk(text) {
|
|
@@ -361,7 +372,7 @@ async function executeSync(projectId, folder, sync) {
|
|
|
361
372
|
|
|
362
373
|
const server = new McpServer({
|
|
363
374
|
name: 'planko-mcp-server',
|
|
364
|
-
version: '0.
|
|
375
|
+
version: '0.5.0',
|
|
365
376
|
});
|
|
366
377
|
|
|
367
378
|
// ---- planko_setup ----
|
|
@@ -637,6 +648,7 @@ function buildTaskBody(params) {
|
|
|
637
648
|
if (isBlankValue(value)) continue;
|
|
638
649
|
body[key] = value;
|
|
639
650
|
}
|
|
651
|
+
applyDueDateFallback(body);
|
|
640
652
|
if ('description' in body) {
|
|
641
653
|
body.description = markdownToDescription(body.description);
|
|
642
654
|
}
|
|
@@ -670,6 +682,16 @@ function describeTask(res) {
|
|
|
670
682
|
};
|
|
671
683
|
}
|
|
672
684
|
|
|
685
|
+
/**
|
|
686
|
+
* When PROJECT_LOCK is set, fetch a by-id task and refuse the operation unless
|
|
687
|
+
* it belongs to the locked project. No-op when the lock is null.
|
|
688
|
+
*/
|
|
689
|
+
async function assertTaskInLock(taskId) {
|
|
690
|
+
if (!PROJECT_LOCK) return;
|
|
691
|
+
const res = await api.getTask(taskId);
|
|
692
|
+
assertProjectAllowed(PROJECT_LOCK, unwrapItem(res));
|
|
693
|
+
}
|
|
694
|
+
|
|
673
695
|
/**
|
|
674
696
|
* Shared create handler for tasks (type=1) and notes (type=2).
|
|
675
697
|
*/
|
|
@@ -679,9 +701,13 @@ async function handleCreate(type, params, kindLabel) {
|
|
|
679
701
|
body.name = name;
|
|
680
702
|
body.type = type;
|
|
681
703
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
704
|
+
if (PROJECT_LOCK) {
|
|
705
|
+
// Hard override: always the locked project; ignore any caller project field
|
|
706
|
+
// and do NOT resolve projectName.
|
|
707
|
+
body.projectId = lockProjectId(PROJECT_LOCK, body.projectId);
|
|
708
|
+
} else if (!isBlankValue(projectName)) {
|
|
709
|
+
// Resolve projectName -> projectId via the API. When omitted, OMIT the key
|
|
710
|
+
// entirely so the backend applies the user's default project.
|
|
685
711
|
body.projectId = await resolveProjectId(projectName);
|
|
686
712
|
}
|
|
687
713
|
|
|
@@ -703,6 +729,16 @@ async function handleEdit(params, kindLabel) {
|
|
|
703
729
|
if (changed.length === 0) {
|
|
704
730
|
return toolError('Nothing to update: provide at least one field to change.');
|
|
705
731
|
}
|
|
732
|
+
if (PROJECT_LOCK) {
|
|
733
|
+
// Refuse to touch a task outside the locked project.
|
|
734
|
+
await assertTaskInLock(taskId);
|
|
735
|
+
// If the caller tried to move the task to another project, force it back to
|
|
736
|
+
// the lock (never let an edit move a task OUT of the locked project). If no
|
|
737
|
+
// project field was passed, leave the project unchanged.
|
|
738
|
+
if ('projectId' in body) {
|
|
739
|
+
body.projectId = lockProjectId(PROJECT_LOCK, body.projectId);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
706
742
|
const res = await api.updateTask(taskId, body);
|
|
707
743
|
const { id, name: savedName } = describeTask(res);
|
|
708
744
|
return toolOk(
|
|
@@ -807,6 +843,7 @@ server.tool(
|
|
|
807
843
|
},
|
|
808
844
|
async ({ taskId }) => {
|
|
809
845
|
try {
|
|
846
|
+
await assertTaskInLock(taskId);
|
|
810
847
|
const res = await api.completeTask(taskId);
|
|
811
848
|
const { id, name } = describeTask(res);
|
|
812
849
|
return toolOk(`Completed task "${name}" (id: ${id}).`);
|
|
@@ -825,6 +862,7 @@ server.tool(
|
|
|
825
862
|
},
|
|
826
863
|
async ({ taskId }) => {
|
|
827
864
|
try {
|
|
865
|
+
await assertTaskInLock(taskId);
|
|
828
866
|
await api.deleteTask(taskId);
|
|
829
867
|
return toolOk(`Deleted task (id: ${taskId}).`);
|
|
830
868
|
} catch (err) {
|
|
@@ -842,6 +880,7 @@ server.tool(
|
|
|
842
880
|
},
|
|
843
881
|
async ({ taskId }) => {
|
|
844
882
|
try {
|
|
883
|
+
await assertTaskInLock(taskId);
|
|
845
884
|
await api.deleteTask(taskId);
|
|
846
885
|
return toolOk(`Deleted note (id: ${taskId}).`);
|
|
847
886
|
} catch (err) {
|
|
@@ -884,12 +923,17 @@ function formatTags(tags) {
|
|
|
884
923
|
/** Build the API filter params from tool params, resolving projectName. */
|
|
885
924
|
async function buildListParams(type, params) {
|
|
886
925
|
const { projectName, ...rest } = params;
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
926
|
+
// Sanitize scalar filters (drops blank/placeholder values, incl. a blank
|
|
927
|
+
// assigneeId => "all members"). projectId/assigneeId pass through here.
|
|
928
|
+
const out = { type, ...sanitizeFilters(rest) };
|
|
929
|
+
if (PROJECT_LOCK) {
|
|
930
|
+
// Hard override: force the project regardless of caller input. Do NOT
|
|
931
|
+
// resolve projectName here — the override wins unconditionally, so an
|
|
932
|
+
// invalid projectName must not be able to fail an otherwise-locked list
|
|
933
|
+
// (keeps list consistent with create, which also ignores projectName).
|
|
934
|
+
out.projectId = lockProjectId(PROJECT_LOCK, out.projectId);
|
|
935
|
+
} else if (!isBlankValue(projectName)) {
|
|
936
|
+
// Unlocked (default) path: unchanged behavior.
|
|
893
937
|
out.projectId = await resolveProjectId(projectName);
|
|
894
938
|
}
|
|
895
939
|
return out;
|
|
@@ -905,6 +949,8 @@ function summarizeItem(item, index) {
|
|
|
905
949
|
const tagStr = formatTags(item.tags);
|
|
906
950
|
if (tagStr) parts.push(`tags: ${tagStr}`);
|
|
907
951
|
if (item.projectId) parts.push(`project: ${item.projectId}`);
|
|
952
|
+
const owner = ownerName(item.userId);
|
|
953
|
+
if (owner) parts.push(`owner: ${owner}`);
|
|
908
954
|
return (
|
|
909
955
|
`${index}. ${item.name || '(untitled)'} [id: ${item._id}]\n` +
|
|
910
956
|
` ${parts.join(' | ')}`
|
|
@@ -940,6 +986,8 @@ function renderItem(item, kindLabel) {
|
|
|
940
986
|
const tagStr = formatTags(item.tags);
|
|
941
987
|
if (tagStr) lines.push(`tags: ${tagStr}`);
|
|
942
988
|
if (item.projectId) lines.push(`project: ${item.projectId}`);
|
|
989
|
+
const owner = ownerName(item.userId);
|
|
990
|
+
if (owner) lines.push(`owner: ${owner}`);
|
|
943
991
|
if (item.parentId) lines.push(`parent: ${item.parentId}`);
|
|
944
992
|
if (item.createdAt) lines.push(`created: ${item.createdAt}`);
|
|
945
993
|
if (item.updatedAt) lines.push(`updated: ${item.updatedAt}`);
|
|
@@ -970,6 +1018,9 @@ const listProjectName = z
|
|
|
970
1018
|
.optional()
|
|
971
1019
|
.describe('Filter to a project by name (resolved to its id)');
|
|
972
1020
|
const listProjectId = objectId.optional().describe('Filter to a project by id');
|
|
1021
|
+
const listAssigneeId = objectId
|
|
1022
|
+
.optional()
|
|
1023
|
+
.describe('Filter to a single project member by their user id (omit for all members)');
|
|
973
1024
|
const listTags = z.array(objectId).optional().describe('Filter by tag ids (AND — item must have all)');
|
|
974
1025
|
const listSearch = z.string().optional().describe('Case-insensitive search on the name');
|
|
975
1026
|
const listSortBy = sortBySchema.optional();
|
|
@@ -995,6 +1046,7 @@ server.tool(
|
|
|
995
1046
|
.describe('Filter by priority: 1, 2, or 3'),
|
|
996
1047
|
projectName: listProjectName,
|
|
997
1048
|
projectId: listProjectId,
|
|
1049
|
+
assigneeId: listAssigneeId,
|
|
998
1050
|
parentId: objectId.optional().describe('List subtasks of this parent task id'),
|
|
999
1051
|
tags: listTags,
|
|
1000
1052
|
search: listSearch,
|
|
@@ -1023,6 +1075,7 @@ server.tool(
|
|
|
1023
1075
|
showCompleted: listShowCompleted,
|
|
1024
1076
|
projectName: listProjectName,
|
|
1025
1077
|
projectId: listProjectId,
|
|
1078
|
+
assigneeId: listAssigneeId,
|
|
1026
1079
|
tags: listTags,
|
|
1027
1080
|
search: listSearch,
|
|
1028
1081
|
sortBy: listSortBy,
|
|
@@ -1050,7 +1103,9 @@ server.tool(
|
|
|
1050
1103
|
async ({ taskId }) => {
|
|
1051
1104
|
try {
|
|
1052
1105
|
const res = await api.getTask(taskId);
|
|
1053
|
-
|
|
1106
|
+
const item = unwrapItem(res);
|
|
1107
|
+
assertProjectAllowed(PROJECT_LOCK, item);
|
|
1108
|
+
return toolOk(renderItem(item, 'task'));
|
|
1054
1109
|
} catch (err) {
|
|
1055
1110
|
return toolError(`View task failed: ${err.message}`);
|
|
1056
1111
|
}
|
|
@@ -1067,7 +1122,9 @@ server.tool(
|
|
|
1067
1122
|
async ({ taskId }) => {
|
|
1068
1123
|
try {
|
|
1069
1124
|
const res = await api.getTask(taskId);
|
|
1070
|
-
|
|
1125
|
+
const item = unwrapItem(res);
|
|
1126
|
+
assertProjectAllowed(PROJECT_LOCK, item);
|
|
1127
|
+
return toolOk(renderItem(item, 'note'));
|
|
1071
1128
|
} catch (err) {
|
|
1072
1129
|
return toolError(`View note failed: ${err.message}`);
|
|
1073
1130
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "planko-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server for syncing Planko tasks with local Markdown files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"test": "vitest run",
|
|
20
|
-
"prepublishOnly": "node --check index.js && node --check src/api.js && node --check src/converters.js && node --check src/sync-state.js"
|
|
20
|
+
"prepublishOnly": "node --check index.js && node --check src/api.js && node --check src/converters.js && node --check src/sync-state.js && node --check src/projectLock.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-lock decision logic (pure, testable).
|
|
3
|
+
*
|
|
4
|
+
* When the env var PLANKO_PROJECT_LOCK is set, the MCP server restricts EVERY
|
|
5
|
+
* operation to that single project id — a hard override the calling agent
|
|
6
|
+
* cannot bypass. When the lock is null (the default), all functions here behave
|
|
7
|
+
* as pass-throughs so existing callers (e.g. the "clawis" agent) are unaffected.
|
|
8
|
+
*
|
|
9
|
+
* These helpers are kept free of any I/O so they can be unit-tested directly;
|
|
10
|
+
* the server entrypoint (index.js) wires them into the tool handlers.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { isBlankValue } from './sanitize.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Decide the effective projectId.
|
|
17
|
+
* - lock set (truthy) → always returns `lock` (ignores `requested`).
|
|
18
|
+
* - lock null/blank → returns `requested` unchanged (pass-through).
|
|
19
|
+
*/
|
|
20
|
+
export function lockProjectId(lock, requested) {
|
|
21
|
+
if (lock) return lock;
|
|
22
|
+
return requested;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sanitize a bag of list/query filter params, dropping blank/placeholder values
|
|
27
|
+
* (empty strings, all-zero ObjectId, empty arrays) via isBlankValue. Used for
|
|
28
|
+
* every list filter including the optional `assigneeId` — when it is blank the
|
|
29
|
+
* key is omitted, which the backend treats as "all members".
|
|
30
|
+
* Returns a new object; does not mutate the input.
|
|
31
|
+
*/
|
|
32
|
+
export function sanitizeFilters(params) {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const [key, value] of Object.entries(params || {})) {
|
|
35
|
+
if (isBlankValue(value)) continue;
|
|
36
|
+
out[key] = value;
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format an owner label from an item's `userId` field.
|
|
43
|
+
* The backend may populate `userId` as an object { _id, name, email } on
|
|
44
|
+
* list/view responses. Returns the owner's name when present, else null.
|
|
45
|
+
* Tolerates a bare string/id (returns null) and never throws.
|
|
46
|
+
*/
|
|
47
|
+
export function ownerName(userId) {
|
|
48
|
+
if (
|
|
49
|
+
userId &&
|
|
50
|
+
typeof userId === 'object' &&
|
|
51
|
+
!Array.isArray(userId) &&
|
|
52
|
+
typeof userId.name === 'string' &&
|
|
53
|
+
userId.name.trim() !== ''
|
|
54
|
+
) {
|
|
55
|
+
return userId.name;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract a comparable project id string from a task's `projectId`, which may be
|
|
62
|
+
* a bare id string or a populated object { _id, ... }.
|
|
63
|
+
*/
|
|
64
|
+
function projectIdOf(task) {
|
|
65
|
+
const pid = task && task.projectId;
|
|
66
|
+
if (pid && typeof pid === 'object') return pid._id != null ? String(pid._id) : '';
|
|
67
|
+
return pid != null ? String(pid) : '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Guard a by-id operation against the lock. When `lock` is null this is a no-op.
|
|
72
|
+
* When `lock` is set, throws unless the task belongs to the locked project.
|
|
73
|
+
* A missing/unknown projectId also throws (cannot prove it is in-scope).
|
|
74
|
+
*/
|
|
75
|
+
export function assertProjectAllowed(lock, task) {
|
|
76
|
+
if (!lock) return;
|
|
77
|
+
const pid = projectIdOf(task);
|
|
78
|
+
if (!pid || pid !== String(lock)) {
|
|
79
|
+
throw new Error('This agent is restricted to the "planko" project.');
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/sanitize.js
CHANGED
|
@@ -21,3 +21,17 @@ export function isBlankValue(value) {
|
|
|
21
21
|
if (Array.isArray(value)) return value.length === 0;
|
|
22
22
|
return false;
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* LLM callers often put the date in `datePlain` and leave `dueDate` empty.
|
|
27
|
+
* The backend derives datePlain FROM dueDate but not the reverse, so a
|
|
28
|
+
* datePlain-only task ends up with dueDate=null and never appears on day/agenda
|
|
29
|
+
* views (it looks "not created"). Mirror datePlain -> dueDate in that case.
|
|
30
|
+
* Mutates and returns `body`.
|
|
31
|
+
*/
|
|
32
|
+
export function applyDueDateFallback(body) {
|
|
33
|
+
if (isBlankValue(body.dueDate) && !isBlankValue(body.datePlain)) {
|
|
34
|
+
body.dueDate = body.datePlain;
|
|
35
|
+
}
|
|
36
|
+
return body;
|
|
37
|
+
}
|