Fix tasks page permissions handling

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-31 11:21:27 +01:00
committed by Bryan Frimin
parent e46a11001a
commit 21353f35e7
12 changed files with 643 additions and 409 deletions

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<f1100222ab7c7aeff0f7643640ecc23c>> * @generated SignedSource<<1ee2176710f8c100a3d16e65be7a5f74>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -16,25 +16,22 @@ export type MeasureTasksTabQuery$variables = {
}; };
export type MeasureTasksTabQuery$data = { export type MeasureTasksTabQuery$data = {
readonly node: { readonly node: {
readonly id?: string; readonly __typename: "Measure";
readonly tasks?: { readonly id: string;
readonly tasks: {
readonly __id: string; readonly __id: string;
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly node: { readonly node: {
readonly assignedTo: {
readonly fullName: string;
readonly id: string;
} | null | undefined;
readonly deadline: any | null | undefined;
readonly description: string | null | undefined;
readonly id: string; readonly id: string;
readonly name: string;
readonly state: TaskState; readonly state: TaskState;
readonly timeEstimate: any | null | undefined; readonly " $fragmentSpreads": FragmentRefs<"TaskFormDialogFragment" | "TasksCard_TaskRowFragment">;
readonly " $fragmentSpreads": FragmentRefs<"TaskFormDialogFragment">;
}; };
}>; }>;
}; };
} | {
// This will never be '%other', but we need some
// value in case none of the concrete values match.
readonly __typename: "%other";
}; };
}; };
export type MeasureTasksTabQuery = { export type MeasureTasksTabQuery = {
@@ -61,14 +58,14 @@ v2 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "id", "name": "__typename",
"storageKey": null "storageKey": null
}, },
v3 = { v3 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "name", "name": "id",
"storageKey": null "storageKey": null
}, },
v4 = { v4 = {
@@ -79,60 +76,13 @@ v4 = {
"storageKey": null "storageKey": null
}, },
v5 = { v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "timeEstimate",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deadline",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "assignedTo",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v10 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "cursor", "name": "cursor",
"storageKey": null "storageKey": null
}, },
v11 = { v6 = {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "PageInfo", "concreteType": "PageInfo",
@@ -157,7 +107,7 @@ v11 = {
], ],
"storageKey": null "storageKey": null
}, },
v12 = { v7 = {
"kind": "ClientExtension", "kind": "ClientExtension",
"selections": [ "selections": [
{ {
@@ -169,13 +119,20 @@ v12 = {
} }
] ]
}, },
v13 = [ v8 = [
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
"value": 100 "value": 100
} }
]; ],
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
};
return { return {
"fragment": { "fragment": {
"argumentDefinitions": (v0/*: any*/), "argumentDefinitions": (v0/*: any*/),
@@ -184,72 +141,85 @@ return {
"name": "MeasureTasksTabQuery", "name": "MeasureTasksTabQuery",
"selections": [ "selections": [
{ {
"alias": null, "kind": "RequiredField",
"args": (v1/*: any*/), "field": {
"concreteType": null, "alias": null,
"kind": "LinkedField", "args": (v1/*: any*/),
"name": "node", "concreteType": null,
"plural": false, "kind": "LinkedField",
"selections": [ "name": "node",
{ "plural": false,
"kind": "InlineFragment", "selections": [
"selections": [ (v2/*: any*/),
(v2/*: any*/), {
{ "kind": "InlineFragment",
"alias": "tasks", "selections": [
"args": null, (v3/*: any*/),
"concreteType": "TaskConnection", {
"kind": "LinkedField", "kind": "RequiredField",
"name": "__Measure__tasks_connection", "field": {
"plural": false, "alias": "tasks",
"selections": [
{
"alias": null,
"args": null, "args": null,
"concreteType": "TaskEdge", "concreteType": "TaskConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "edges", "name": "__Measure__tasks_connection",
"plural": true, "plural": false,
"selections": [ "selections": [
{ {
"alias": null, "kind": "RequiredField",
"args": null, "field": {
"concreteType": "Task", "alias": null,
"kind": "LinkedField", "args": null,
"name": "node", "concreteType": "TaskEdge",
"plural": false, "kind": "LinkedField",
"selections": [ "name": "edges",
(v2/*: any*/), "plural": true,
(v3/*: any*/), "selections": [
(v4/*: any*/), {
(v5/*: any*/), "alias": null,
(v6/*: any*/), "args": null,
(v7/*: any*/), "concreteType": "Task",
{ "kind": "LinkedField",
"args": null, "name": "node",
"kind": "FragmentSpread", "plural": false,
"name": "TaskFormDialogFragment" "selections": [
}, (v3/*: any*/),
(v8/*: any*/), (v4/*: any*/),
(v9/*: any*/) {
], "args": null,
"storageKey": null "kind": "FragmentSpread",
"name": "TaskFormDialogFragment"
},
{
"args": null,
"kind": "FragmentSpread",
"name": "TasksCard_TaskRowFragment"
},
(v2/*: any*/)
],
"storageKey": null
},
(v5/*: any*/)
],
"storageKey": null
},
"action": "THROW"
}, },
(v10/*: any*/) (v6/*: any*/),
(v7/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v11/*: any*/), "action": "THROW"
(v12/*: any*/) }
], ],
"storageKey": null "type": "Measure",
} "abstractKey": null
], }
"type": "Measure", ],
"abstractKey": null "storageKey": null
} },
], "action": "THROW"
"storageKey": null
} }
], ],
"type": "Query", "type": "Query",
@@ -269,14 +239,14 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v9/*: any*/),
(v2/*: any*/), (v2/*: any*/),
(v3/*: any*/),
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
{ {
"alias": null, "alias": null,
"args": (v13/*: any*/), "args": (v8/*: any*/),
"concreteType": "TaskConnection", "concreteType": "TaskConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "tasks", "name": "tasks",
@@ -298,13 +268,49 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v2/*: any*/),
(v3/*: any*/), (v3/*: any*/),
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/), {
(v6/*: any*/), "alias": null,
(v7/*: any*/), "args": null,
(v8/*: any*/), "kind": "ScalarField",
"name": "description",
"storageKey": null
},
(v9/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "timeEstimate",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deadline",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "assignedTo",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -313,26 +319,53 @@ return {
"name": "measure", "name": "measure",
"plural": false, "plural": false,
"selections": [ "selections": [
(v2/*: any*/) (v3/*: any*/),
(v9/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v9/*: any*/) {
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:task:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:task:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:task:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:task:delete\")"
},
(v2/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v10/*: any*/) (v5/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v11/*: any*/), (v6/*: any*/),
(v12/*: any*/) (v7/*: any*/)
], ],
"storageKey": "tasks(first:100)" "storageKey": "tasks(first:100)"
}, },
{ {
"alias": null, "alias": null,
"args": (v13/*: any*/), "args": (v8/*: any*/),
"filters": null, "filters": null,
"handle": "connection", "handle": "connection",
"key": "Measure__tasks", "key": "Measure__tasks",
@@ -349,7 +382,7 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "7988e2a18d5929fe99d9cef8de244627", "cacheID": "81a89b0ac44d316e2e580dd536be2812",
"id": null, "id": null,
"metadata": { "metadata": {
"connection": [ "connection": [
@@ -366,11 +399,11 @@ return {
}, },
"name": "MeasureTasksTabQuery", "name": "MeasureTasksTabQuery",
"operationKind": "query", "operationKind": "query",
"text": "query MeasureTasksTabQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n id\n tasks(first: 100) {\n edges {\n node {\n id\n name\n state\n description\n timeEstimate\n deadline\n ...TaskFormDialogFragment\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n" "text": "query MeasureTasksTabQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n id\n tasks(first: 100) {\n edges {\n node {\n id\n state\n ...TaskFormDialogFragment\n ...TasksCard_TaskRowFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n\nfragment TasksCard_TaskRowFragment on Task {\n id\n name\n state\n description\n timeEstimate\n deadline\n canUpdate: permission(action: \"core:task:update\")\n canDelete: permission(action: \"core:task:delete\")\n assignedTo {\n id\n fullName\n }\n measure {\n id\n name\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "358a07bb3e13ea9828d0703541caf894"; (node as any).hash = "0e886bdfa0bcba6ec98758098e1ec0b5";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<40ebf241dc89f4b4b1705e4a2fabb935>> * @generated SignedSource<<d646f8c938435fdb9e65ff696121ff3c>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -121,6 +121,19 @@ return {
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
{
"alias": "canCreateTask",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:task:create"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:task:create\")"
},
{ {
"alias": null, "alias": null,
"args": (v4/*: any*/), "args": (v4/*: any*/),
@@ -146,7 +159,6 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v2/*: any*/), (v2/*: any*/),
(v5/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -161,6 +173,7 @@ return {
"name": "description", "name": "description",
"storageKey": null "storageKey": null
}, },
(v5/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -207,6 +220,32 @@ return {
], ],
"storageKey": null "storageKey": null
}, },
{
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:task:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:task:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:task:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:task:delete\")"
},
(v3/*: any*/) (v3/*: any*/)
], ],
"storageKey": null "storageKey": null
@@ -296,12 +335,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "72cbeaa7667c477f85e5c3ecb09b7bf4", "cacheID": "9932d2887bfde054e02b14404a5ec7f8",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "TaskGraphQuery", "name": "TaskGraphQuery",
"operationKind": "query", "operationKind": "query",
"text": "query TaskGraphQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n ...TasksPageFragment\n }\n id\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n\nfragment TasksPageFragment on Organization {\n tasks(first: 500) {\n edges {\n node {\n id\n name\n state\n description\n timeEstimate\n deadline\n ...TaskFormDialogFragment\n measure {\n id\n name\n }\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" "text": "query TaskGraphQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n ...TasksPageFragment\n }\n id\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n\nfragment TasksCard_TaskRowFragment on Task {\n id\n name\n state\n description\n timeEstimate\n deadline\n canUpdate: permission(action: \"core:task:update\")\n canDelete: permission(action: \"core:task:delete\")\n assignedTo {\n id\n fullName\n }\n measure {\n id\n name\n }\n}\n\nfragment TasksPageFragment on Organization {\n canCreateTask: permission(action: \"core:task:create\")\n tasks(first: 500) {\n edges {\n node {\n id\n state\n ...TaskFormDialogFragment\n ...TasksCard_TaskRowFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
} }
}; };
})(); })();

View File

@@ -0,0 +1,155 @@
/**
* @generated SignedSource<<8baf10554098b6b6a3084311dda31fb3>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type TaskState = "DONE" | "TODO";
import { FragmentRefs } from "relay-runtime";
export type TasksCard_TaskRowFragment$data = {
readonly assignedTo: {
readonly fullName: string;
readonly id: string;
} | null | undefined;
readonly canDelete: boolean;
readonly canUpdate: boolean;
readonly deadline: any | null | undefined;
readonly description: string | null | undefined;
readonly id: string;
readonly measure: {
readonly id: string;
readonly name: string;
} | null | undefined;
readonly name: string;
readonly state: TaskState;
readonly timeEstimate: any | null | undefined;
readonly " $fragmentType": "TasksCard_TaskRowFragment";
};
export type TasksCard_TaskRowFragment$key = {
readonly " $data"?: TasksCard_TaskRowFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"TasksCard_TaskRowFragment">;
};
const node: ReaderFragment = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
};
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "TasksCard_TaskRowFragment",
"selections": [
(v0/*: any*/),
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "timeEstimate",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deadline",
"storageKey": null
},
{
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:task:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:task:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:task:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:task:delete\")"
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "assignedTo",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "Measure",
"kind": "LinkedField",
"name": "measure",
"plural": false,
"selections": [
(v0/*: any*/),
(v1/*: any*/)
],
"storageKey": null
}
],
"type": "Task",
"abstractKey": null
};
})();
(node as any).hash = "34df57ab7cc88c1fbf84f42b4024d50b";
export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<f6e71f80c25925eb372e360c1b9ec122>> * @generated SignedSource<<08f448295bd6530904bfbcfc19b1764d>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -12,26 +12,15 @@ import { ReaderFragment } from 'relay-runtime';
export type TaskState = "DONE" | "TODO"; export type TaskState = "DONE" | "TODO";
import { FragmentRefs } from "relay-runtime"; import { FragmentRefs } from "relay-runtime";
export type TasksPageFragment$data = { export type TasksPageFragment$data = {
readonly canCreateTask: boolean;
readonly id: string; readonly id: string;
readonly tasks: { readonly tasks: {
readonly __id: string; readonly __id: string;
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly node: { readonly node: {
readonly assignedTo: {
readonly fullName: string;
readonly id: string;
} | null | undefined;
readonly deadline: any | null | undefined;
readonly description: string | null | undefined;
readonly id: string; readonly id: string;
readonly measure: {
readonly id: string;
readonly name: string;
} | null | undefined;
readonly name: string;
readonly state: TaskState; readonly state: TaskState;
readonly timeEstimate: any | null | undefined; readonly " $fragmentSpreads": FragmentRefs<"TaskFormDialogFragment" | "TasksCard_TaskRowFragment">;
readonly " $fragmentSpreads": FragmentRefs<"TaskFormDialogFragment">;
}; };
}>; }>;
}; };
@@ -54,13 +43,6 @@ v1 = {
"kind": "ScalarField", "kind": "ScalarField",
"name": "id", "name": "id",
"storageKey": null "storageKey": null
},
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
}; };
return { return {
"argumentDefinitions": [ "argumentDefinitions": [
@@ -125,89 +107,50 @@ return {
"name": "TasksPageFragment", "name": "TasksPageFragment",
"selections": [ "selections": [
{ {
"alias": "tasks", "alias": "canCreateTask",
"args": [ "args": [
{ {
"kind": "Variable", "kind": "Literal",
"name": "orderBy", "name": "action",
"variableName": "order" "value": "core:task:create"
} }
], ],
"concreteType": "TaskConnection", "kind": "ScalarField",
"kind": "LinkedField", "name": "permission",
"name": "__TasksPageFragment_tasks_connection", "storageKey": "permission(action:\"core:task:create\")"
"plural": false, },
"selections": [ {
{ "kind": "RequiredField",
"alias": null, "field": {
"args": null, "alias": "tasks",
"concreteType": "TaskEdge", "args": [
"kind": "LinkedField", {
"name": "edges", "kind": "Variable",
"plural": true, "name": "orderBy",
"selections": [ "variableName": "order"
{ }
],
"concreteType": "TaskConnection",
"kind": "LinkedField",
"name": "__TasksPageFragment_tasks_connection",
"plural": false,
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "Task", "concreteType": "TaskEdge",
"kind": "LinkedField", "kind": "LinkedField",
"name": "node", "name": "edges",
"plural": false, "plural": true,
"selections": [ "selections": [
(v1/*: any*/),
(v2/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "concreteType": "Task",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "timeEstimate",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deadline",
"storageKey": null
},
{
"args": null,
"kind": "FragmentSpread",
"name": "TaskFormDialogFragment"
},
{
"alias": null,
"args": null,
"concreteType": "Measure",
"kind": "LinkedField", "kind": "LinkedField",
"name": "measure", "name": "node",
"plural": false,
"selections": [
(v1/*: any*/),
(v2/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "assignedTo",
"plural": false, "plural": false,
"selections": [ "selections": [
(v1/*: any*/), (v1/*: any*/),
@@ -215,7 +158,24 @@ return {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "fullName", "name": "state",
"storageKey": null
},
{
"args": null,
"kind": "FragmentSpread",
"name": "TaskFormDialogFragment"
},
{
"args": null,
"kind": "FragmentSpread",
"name": "TasksCard_TaskRowFragment"
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null "storageKey": null
} }
], ],
@@ -225,75 +185,69 @@ return {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "__typename", "name": "cursor",
"storageKey": null "storageKey": null
} }
], ],
"storageKey": null "storageKey": null
}, },
{ "action": "THROW"
"alias": null, },
"args": null, {
"kind": "ScalarField", "alias": null,
"name": "cursor", "args": null,
"storageKey": null "concreteType": "PageInfo",
} "kind": "LinkedField",
], "name": "pageInfo",
"storageKey": null "plural": false,
}, "selections": [
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "PageInfo", "kind": "ScalarField",
"kind": "LinkedField", "name": "endCursor",
"name": "pageInfo", "storageKey": null
"plural": false, },
"selections": [ {
{ "alias": null,
"alias": null, "args": null,
"args": null, "kind": "ScalarField",
"kind": "ScalarField", "name": "hasNextPage",
"name": "endCursor", "storageKey": null
"storageKey": null },
}, {
{ "alias": null,
"alias": null, "args": null,
"args": null, "kind": "ScalarField",
"kind": "ScalarField", "name": "hasPreviousPage",
"name": "hasNextPage", "storageKey": null
"storageKey": null },
}, {
{ "alias": null,
"alias": null, "args": null,
"args": null, "kind": "ScalarField",
"kind": "ScalarField", "name": "startCursor",
"name": "hasPreviousPage", "storageKey": null
"storageKey": null }
}, ],
{ "storageKey": null
"alias": null, },
"args": null, {
"kind": "ScalarField", "kind": "ClientExtension",
"name": "startCursor", "selections": [
"storageKey": null {
} "alias": null,
], "args": null,
"storageKey": null "kind": "ScalarField",
}, "name": "__id",
{ "storageKey": null
"kind": "ClientExtension", }
"selections": [ ]
{ }
"alias": null, ],
"args": null, "storageKey": null
"kind": "ScalarField", },
"name": "__id", "action": "THROW"
"storageKey": null
}
]
}
],
"storageKey": null
}, },
(v1/*: any*/) (v1/*: any*/)
], ],
@@ -302,6 +256,6 @@ return {
}; };
})(); })();
(node as any).hash = "466d3b82119334991a0714bb0b7cd6d0"; (node as any).hash = "6c9522af3e0782b9f919b76572ba6225";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<e765e52f0386640e282459c13a3e9769>> * @generated SignedSource<<9c51ce30a6a5b894c53064c5e9248fd9>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -194,6 +194,19 @@ return {
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
{
"alias": "canCreateTask",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:task:create"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:task:create\")"
},
{ {
"alias": null, "alias": null,
"args": (v13/*: any*/), "args": (v13/*: any*/),
@@ -219,7 +232,6 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v12/*: any*/), (v12/*: any*/),
(v14/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -234,6 +246,7 @@ return {
"name": "description", "name": "description",
"storageKey": null "storageKey": null
}, },
(v14/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -280,6 +293,32 @@ return {
], ],
"storageKey": null "storageKey": null
}, },
{
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:task:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:task:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:task:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:task:delete\")"
},
(v11/*: any*/) (v11/*: any*/)
], ],
"storageKey": null "storageKey": null
@@ -369,16 +408,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "d31450ee0ddd09b0e08b43c9f7728f5f", "cacheID": "8fadc4428353d6ef3444f63b913dc695",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "TasksPageFragment_query", "name": "TasksPageFragment_query",
"operationKind": "query", "operationKind": "query",
"text": "query TasksPageFragment_query(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 500\n $last: Int = null\n $order: TaskOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...TasksPageFragment_16fISc\n id\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n\nfragment TasksPageFragment_16fISc on Organization {\n tasks(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n name\n state\n description\n timeEstimate\n deadline\n ...TaskFormDialogFragment\n measure {\n id\n name\n }\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" "text": "query TasksPageFragment_query(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 500\n $last: Int = null\n $order: TaskOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...TasksPageFragment_16fISc\n id\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n\nfragment TasksCard_TaskRowFragment on Task {\n id\n name\n state\n description\n timeEstimate\n deadline\n canUpdate: permission(action: \"core:task:update\")\n canDelete: permission(action: \"core:task:delete\")\n assignedTo {\n id\n fullName\n }\n measure {\n id\n name\n }\n}\n\nfragment TasksPageFragment_16fISc on Organization {\n canCreateTask: permission(action: \"core:task:create\")\n tasks(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n state\n ...TaskFormDialogFragment\n ...TasksCard_TaskRowFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
} }
}; };
})(); })();
(node as any).hash = "466d3b82119334991a0714bb0b7cd6d0"; (node as any).hash = "6c9522af3e0782b9f919b76572ba6225";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<bfffdd69a2b1f565b60beee5e96d430a>> * @generated SignedSource<<f52778bf4020037a3a76f29c2a7c6b39>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -50,6 +50,7 @@ export type TrustCenterGraphQuery$data = {
}; };
readonly trustCenter?: { readonly trustCenter?: {
readonly active: boolean; readonly active: boolean;
readonly canUpdate: boolean;
readonly createdAt: any; readonly createdAt: any;
readonly id: string; readonly id: string;
readonly ndaFileName: string | null | undefined; readonly ndaFileName: string | null | undefined;
@@ -199,6 +200,19 @@ v9 = {
}, },
(v5/*: any*/), (v5/*: any*/),
(v6/*: any*/), (v6/*: any*/),
{
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:trust-center:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:trust-center:update\")"
},
{ {
"alias": null, "alias": null,
"args": [ "args": [
@@ -881,7 +895,7 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "45a68d6b8fc3128a695529daa40eba3a", "cacheID": "77cfcb8a51459702b07cd2295688ad07",
"id": null, "id": null,
"metadata": { "metadata": {
"connection": [ "connection": [
@@ -898,11 +912,11 @@ return {
}, },
"name": "TrustCenterGraphQuery", "name": "TrustCenterGraphQuery",
"operationKind": "query", "operationKind": "query",
"text": "query TrustCenterGraphQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n customDomain {\n id\n domain\n }\n trustCenter {\n id\n active\n ndaFileName\n ndaFileUrl\n createdAt\n updatedAt\n references(first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n name\n description\n websiteUrl\n logoUrl\n createdAt\n updatedAt\n }\n }\n }\n }\n documents(first: 100) {\n edges {\n node {\n id\n ...TrustCenterDocumentsCardFragment\n }\n }\n }\n audits(first: 100) {\n edges {\n node {\n id\n ...TrustCenterAuditsCardFragment\n }\n }\n }\n vendors(first: 100) {\n edges {\n node {\n id\n ...TrustCenterVendorsCardFragment\n }\n }\n }\n trustCenterFiles(first: 100) {\n edges {\n node {\n id\n ...TrustCenterFilesCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n slackConnections(first: 100) {\n edges {\n node {\n id\n channel\n channelId\n createdAt\n updatedAt\n }\n }\n }\n }\n id\n }\n}\n\nfragment TrustCenterAuditsCardFragment on Audit {\n id\n name\n framework {\n name\n id\n }\n validFrom\n validUntil\n state\n trustCenterVisibility\n createdAt\n}\n\nfragment TrustCenterDocumentsCardFragment on Document {\n id\n title\n createdAt\n documentType\n trustCenterVisibility\n versions(first: 1) {\n edges {\n node {\n id\n status\n }\n }\n }\n}\n\nfragment TrustCenterFilesCardFragment on TrustCenterFile {\n id\n name\n category\n fileUrl\n trustCenterVisibility\n createdAt\n updatedAt\n}\n\nfragment TrustCenterVendorsCardFragment on Vendor {\n id\n name\n category\n description\n showOnTrustCenter\n createdAt\n}\n" "text": "query TrustCenterGraphQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n customDomain {\n id\n domain\n }\n trustCenter {\n id\n active\n ndaFileName\n ndaFileUrl\n createdAt\n updatedAt\n canUpdate: permission(action: \"core:trust-center:update\")\n references(first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n name\n description\n websiteUrl\n logoUrl\n createdAt\n updatedAt\n }\n }\n }\n }\n documents(first: 100) {\n edges {\n node {\n id\n ...TrustCenterDocumentsCardFragment\n }\n }\n }\n audits(first: 100) {\n edges {\n node {\n id\n ...TrustCenterAuditsCardFragment\n }\n }\n }\n vendors(first: 100) {\n edges {\n node {\n id\n ...TrustCenterVendorsCardFragment\n }\n }\n }\n trustCenterFiles(first: 100) {\n edges {\n node {\n id\n ...TrustCenterFilesCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n slackConnections(first: 100) {\n edges {\n node {\n id\n channel\n channelId\n createdAt\n updatedAt\n }\n }\n }\n }\n id\n }\n}\n\nfragment TrustCenterAuditsCardFragment on Audit {\n id\n name\n framework {\n name\n id\n }\n validFrom\n validUntil\n state\n trustCenterVisibility\n createdAt\n}\n\nfragment TrustCenterDocumentsCardFragment on Document {\n id\n title\n createdAt\n documentType\n trustCenterVisibility\n versions(first: 1) {\n edges {\n node {\n id\n status\n }\n }\n }\n}\n\nfragment TrustCenterFilesCardFragment on TrustCenterFile {\n id\n name\n category\n fileUrl\n trustCenterVisibility\n createdAt\n updatedAt\n}\n\nfragment TrustCenterVendorsCardFragment on Vendor {\n id\n name\n category\n description\n showOnTrustCenter\n createdAt\n}\n"
} }
}; };
})(); })();
(node as any).hash = "1ee50612da47b187cb55b2e228ba5945"; (node as any).hash = "8c59eb08a0ec74662a01a3b8bb8ab56f";
export default node; export default node;

View File

@@ -2,10 +2,9 @@ import { Badge, Button, Card } from "@probo/ui";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { sprintf } from "@probo/helpers"; import { sprintf } from "@probo/helpers";
import type { TrustCenterGraphQuery$data } from "/__generated__/core/TrustCenterGraphQuery.graphql"; import type { TrustCenterGraphQuery$data } from "/__generated__/core/TrustCenterGraphQuery.graphql";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = { type Props = {
canUpdate: boolean;
organizationId: string; organizationId: string;
slackConnections: NonNullable< slackConnections: NonNullable<
TrustCenterGraphQuery$data["organization"]["slackConnections"] TrustCenterGraphQuery$data["organization"]["slackConnections"]
@@ -13,11 +12,11 @@ type Props = {
}; };
export function SlackConnections({ export function SlackConnections({
canUpdate,
organizationId, organizationId,
slackConnections: connectedSlackConnections, slackConnections: connectedSlackConnections,
}: Props) { }: Props) {
const { __, dateTimeFormat } = useTranslate(); const { __, dateTimeFormat } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const slackConnectionDefinitions = [ const slackConnectionDefinitions = [
{ {
id: "SLACK", id: "SLACK",
@@ -87,7 +86,7 @@ export function SlackConnections({
</Badge> </Badge>
</div> </div>
) : ( ) : (
isAuthorized("TrustCenter", "updateTrustCenter") && ( canUpdate && (
<Button variant="secondary" asChild> <Button variant="secondary" asChild>
<a href={getUrl(slackConnection.id)}>{__("Connect")}</a> <a href={getUrl(slackConnection.id)}>{__("Connect")}</a>
</Button> </Button>

View File

@@ -18,38 +18,35 @@ import {
useDialogRef, useDialogRef,
} from "@probo/ui"; } from "@probo/ui";
import { Fragment, use } from "react"; import { Fragment, use } from "react";
import { graphql, useMutation, useRelayEnvironment } from "react-relay"; import {
graphql,
useFragment,
useMutation,
useRelayEnvironment,
} from "react-relay";
import { Link, useLocation, useParams } from "react-router"; import { Link, useLocation, useParams } from "react-router";
import type { TaskFormDialogFragment$key } from "/__generated__/core/TaskFormDialogFragment.graphql";
import TaskFormDialog, { import TaskFormDialog, {
taskUpdateMutation, taskUpdateMutation,
} from "/components/tasks/TaskFormDialog"; } from "/components/tasks/TaskFormDialog";
import { updateStoreCounter } from "/hooks/useMutationWithIncrement"; import { updateStoreCounter } from "/hooks/useMutationWithIncrement";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import { PermissionsContext } from "/providers/PermissionsContext"; import { PermissionsContext } from "/providers/PermissionsContext";
import type { ItemOf } from "/types"; import type { TasksCard_TaskRowFragment$key } from "/__generated__/core/TasksCard_TaskRowFragment.graphql";
import type { TasksPageFragment$data } from "/__generated__/core/TasksPageFragment.graphql";
import type { MeasureTasksTabQuery$data } from "/__generated__/core/MeasureTasksTabQuery.graphql";
import type { TaskFormDialogFragment$key } from "/__generated__/core/TaskFormDialogFragment.graphql";
type Props = { type Props = {
tasks: ({ tasks:
assignedTo?: { | TasksPageFragment$data["tasks"]["edges"]
id: string; | Extract<
fullName: string; MeasureTasksTabQuery$data["node"],
} | null; { __typename: "Measure" }
id: string; >["tasks"]["edges"];
name: string;
state: "TODO" | "DONE";
description?: string | null;
timeEstimate?: string | null;
deadline?: string | null;
measure?: {
id: string;
name: string;
} | null;
} & TaskFormDialogFragment$key)[];
connectionId: string; connectionId: string;
}; };
export default function TasksCard({ tasks, connectionId }: Props) { export function TasksCard({ tasks, connectionId }: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const hash = useLocation().hash.replace("#", ""); const hash = useLocation().hash.replace("#", "");
@@ -60,8 +57,8 @@ export default function TasksCard({ tasks, connectionId }: Props) {
] as const; ] as const;
const tasksPerHash = new Map([ const tasksPerHash = new Map([
["", tasks?.filter((t) => t.state === "TODO")], ["", tasks?.filter(({ node }) => node.state === "TODO")],
["done", tasks?.filter((t) => t.state === "DONE")], ["done", tasks?.filter(({ node }) => node.state === "DONE")],
["all", tasks], ["all", tasks],
]); ]);
@@ -69,14 +66,9 @@ export default function TasksCard({ tasks, connectionId }: Props) {
usePageTitle(__("Tasks")); usePageTitle(__("Tasks"));
const { isAuthorized } = use(PermissionsContext);
const hasAnyAction =
isAuthorized("Task", "updateTask") || isAuthorized("Task", "deleteTask");
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{tasks?.length === 0 ? ( {tasks.length === 0 ? (
<p className="text-center py-6 text-txt-secondary">{__("No tasks")}</p> <p className="text-center py-6 text-txt-secondary">{__("No tasks")}</p>
) : ( ) : (
<Card> <Card>
@@ -102,23 +94,21 @@ export default function TasksCard({ tasks, connectionId }: Props) {
<TaskStateIcon state={h.state!} /> <TaskStateIcon state={h.state!} />
{h.label} {h.label}
</h2> </h2>
{tasksPerHash.get(h.hash)?.map((task) => ( {tasksPerHash.get(h.hash)?.map(({ node: task }) => (
<TaskRow <TaskRow
key={task.id} key={task.id}
task={task} fKey={task}
connectionId={connectionId} connectionId={connectionId}
hasAnyAction={hasAnyAction}
/> />
))} ))}
</Fragment> </Fragment>
)) ))
: // Todo and Done tab simply list todos : // Todo and Done tab simply list todos
filteredTasks?.map((task) => ( filteredTasks?.map(({ node: task }) => (
<TaskRow <TaskRow
key={task.id} key={task.id}
task={task} fKey={task}
connectionId={connectionId} connectionId={connectionId}
hasAnyAction={hasAnyAction}
/> />
))} ))}
</div> </div>
@@ -129,11 +119,31 @@ export default function TasksCard({ tasks, connectionId }: Props) {
} }
type TaskRowProps = { type TaskRowProps = {
task: ItemOf<Props["tasks"]>; fKey: TasksCard_TaskRowFragment$key | TaskFormDialogFragment$key;
connectionId: string; connectionId: string;
hasAnyAction: boolean;
}; };
const fragment = graphql`
fragment TasksCard_TaskRowFragment on Task {
id
name
state
description
timeEstimate
deadline
canUpdate: permission(action: "core:task:update")
canDelete: permission(action: "core:task:delete")
assignedTo {
id
fullName
}
measure {
id
name
}
}
`;
const deleteMutation = graphql` const deleteMutation = graphql`
mutation TasksCardDeleteMutation( mutation TasksCardDeleteMutation(
$input: DeleteTaskInput! $input: DeleteTaskInput!
@@ -155,14 +165,19 @@ function TaskRow(props: TaskRowProps) {
const { isAuthorized } = use(PermissionsContext); const { isAuthorized } = use(PermissionsContext);
const relayEnv = useRelayEnvironment(); const relayEnv = useRelayEnvironment();
const { canUpdate, canDelete, ...task } =
useFragment<TasksCard_TaskRowFragment$key>(
fragment,
props.fKey as TasksCard_TaskRowFragment$key,
);
const [updateTask, isUpdating] = useMutation(taskUpdateMutation); const [updateTask, isUpdating] = useMutation(taskUpdateMutation);
const onToggle = () => { const onToggle = () => {
promisifyMutation(updateTask)({ promisifyMutation(updateTask)({
variables: { variables: {
input: { input: {
taskId: props.task.id, taskId: task.id,
state: props.task.state === "TODO" ? "DONE" : "TODO", state: task.state === "TODO" ? "DONE" : "TODO",
}, },
}, },
}); });
@@ -173,7 +188,7 @@ function TaskRow(props: TaskRowProps) {
() => () =>
promisifyMutation(deleteTask)({ promisifyMutation(deleteTask)({
variables: { variables: {
input: { taskId: props.task.id }, input: { taskId: task.id },
connections: [props.connectionId], connections: [props.connectionId],
}, },
onCompleted: (_response, errors) => { onCompleted: (_response, errors) => {
@@ -195,7 +210,10 @@ function TaskRow(props: TaskRowProps) {
return ( return (
<> <>
<TaskFormDialog task={props.task} ref={dialogRef} /> <TaskFormDialog
task={props.fKey as TaskFormDialogFragment$key}
ref={dialogRef}
/>
<div className="flex items-center justify-between py-3 px-6"> <div className="flex items-center justify-between py-3 px-6">
<div className="flex gap-2 items-start"> <div className="flex gap-2 items-start">
<div className="flex items-center gap-2 pt-[2px]"> <div className="flex items-center gap-2 pt-[2px]">
@@ -205,52 +223,53 @@ function TaskRow(props: TaskRowProps) {
className="cursor-pointer -m-1 p-1 disabled:opacity-60" className="cursor-pointer -m-1 p-1 disabled:opacity-60"
disabled={isUpdating} disabled={isUpdating}
> >
<TaskStateIcon state={props.task.state} /> <TaskStateIcon state={task.state} />
</button> </button>
</div> </div>
<div className="text-sm space-y-1 flex-1"> <div className="text-sm space-y-1 flex-1">
<h2 className="font-medium">{props.task.name}</h2> <h2 className="font-medium">{task.name}</h2>
{props.task.description && ( {task.description && (
<p className="text-txt-secondary whitespace-pre-wrap break-words"> <p className="text-txt-secondary whitespace-pre-wrap break-words">
{props.task.description} {task.description}
</p> </p>
)} )}
<div className="flex flex-wrap items-center gap-3 text-txt-secondary text-xs"> <div className="flex flex-wrap items-center gap-3 text-txt-secondary text-xs">
{props.task.measure && ( {task.measure && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<IconArrowCornerDownLeft className="scale-x-[-1]" size={14} /> <IconArrowCornerDownLeft className="scale-x-[-1]" size={14} />
<Link <Link
className="hover:underline" className="hover:underline"
to={`/organizations/${organizationId}/measures/${props.task.measure?.id}`} to={`/organizations/${organizationId}/measures/${task.measure?.id}`}
> >
{props.task.measure?.name} {task.measure?.name}
</Link> </Link>
</span> </span>
)} )}
{props.task.timeEstimate && ( {task.timeEstimate && (
<span>{formatDuration(props.task.timeEstimate, __)}</span> <span>{formatDuration(task.timeEstimate, __)}</span>
)} )}
{props.task.deadline && ( {task.deadline && (
<time dateTime={props.task.deadline}> <time dateTime={task.deadline}>
{formatDate(props.task.deadline)} {formatDate(task.deadline)}
</time> </time>
)} )}
</div> </div>
</div> </div>
</div> </div>
{props.task.assignedTo?.fullName && ( {task.assignedTo?.fullName && (
<div className="text-sm text-txt-secondary ml-auto mr-8"> <div className="text-sm text-txt-secondary ml-auto mr-8">
<Link <Link
className="hover:underline" className="hover:underline"
to={`/organizations/${organizationId}/people/${props.task.assignedTo.id}`} to={`/organizations/${organizationId}/people/${task.assignedTo.id}`}
> >
{props.task.assignedTo.fullName} {task.assignedTo.fullName}
</Link> </Link>
</div> </div>
)} )}
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
{isUpdating && <Spinner size={16} />} {isUpdating && <Spinner size={16} />}
{props.hasAnyAction && ( {(canUpdate || canDelete) && (
<ActionDropdown> <ActionDropdown>
{isAuthorized("Task", "updateTask") && ( {isAuthorized("Task", "updateTask") && (
<DropdownItem <DropdownItem

View File

@@ -19,6 +19,7 @@ export const trustCenterQuery = graphql`
ndaFileUrl ndaFileUrl
createdAt createdAt
updatedAt updatedAt
canUpdate: permission(action: "core:trust-center:update")
references( references(
first: 100 first: 100
orderBy: { field: CREATED_AT, direction: DESC } orderBy: { field: CREATED_AT, direction: DESC }

View File

@@ -2,31 +2,27 @@ import { graphql } from "relay-runtime";
import type { MeasureTasksTabQuery } from "/__generated__/core/MeasureTasksTabQuery.graphql"; import type { MeasureTasksTabQuery } from "/__generated__/core/MeasureTasksTabQuery.graphql";
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import { useLazyLoadQuery } from "react-relay"; import { useLazyLoadQuery } from "react-relay";
import TasksCard from "/components/tasks/TasksCard"; import { TasksCard } from "/components/tasks/TasksCard";
import { Button, IconPlusLarge } from "@probo/ui"; import { Button, IconPlusLarge } from "@probo/ui";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import TaskFormDialog from "/components/tasks/TaskFormDialog"; import TaskFormDialog from "/components/tasks/TaskFormDialog";
const tasksQuery = graphql` const tasksQuery = graphql`
query MeasureTasksTabQuery($measureId: ID!) { query MeasureTasksTabQuery($measureId: ID!) {
node(id: $measureId) { node(id: $measureId) @required(action: THROW) {
__typename
... on Measure { ... on Measure {
id id
tasks(first: 100) @connection(key: "Measure__tasks") { tasks(first: 100)
@connection(key: "Measure__tasks")
@required(action: THROW) {
__id __id
edges { edges @required(action: THROW) {
node { node {
id id
name
state state
description
timeEstimate
deadline
...TaskFormDialogFragment ...TaskFormDialogFragment
assignedTo { ...TasksCard_TaskRowFragment
id
fullName
}
} }
} }
} }
@@ -40,19 +36,17 @@ export default function MeasureTasksTab() {
const { measure } = useOutletContext<{ const { measure } = useOutletContext<{
measure: { id: string }; measure: { id: string };
}>(); }>();
const data = useLazyLoadQuery<MeasureTasksTabQuery>(tasksQuery, { const { node } = useLazyLoadQuery<MeasureTasksTabQuery>(tasksQuery, {
measureId: measure.id, measureId: measure.id,
}); });
const node = data.node; if (node.__typename !== "Measure") {
if (!node || !node.tasks) { throw new Error("invalid node type");
return null;
} }
const connectionId = node.tasks.__id; const connectionId = node.tasks.__id;
const tasks = node.tasks.edges?.map((edge) => edge.node) ?? [];
return ( return (
<div className="relative"> <div className="relative">
<TasksCard connectionId={connectionId} tasks={tasks} /> <TasksCard connectionId={connectionId} tasks={node.tasks.edges} />
<TaskFormDialog connection={connectionId} measureId={measure.id}> <TaskFormDialog connection={connectionId} measureId={measure.id}>
<Button <Button
variant="secondary" variant="secondary"

View File

@@ -10,10 +10,8 @@ import { useTranslate } from "@probo/i18n";
import type { TasksPageFragment$key } from "/__generated__/core/TasksPageFragment.graphql"; import type { TasksPageFragment$key } from "/__generated__/core/TasksPageFragment.graphql";
import { tasksQuery } from "/hooks/graph/TaskGraph"; import { tasksQuery } from "/hooks/graph/TaskGraph";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import TasksCard from "/components/tasks/TasksCard"; import { TasksCard } from "/components/tasks/TasksCard";
import TaskFormDialog from "/components/tasks/TaskFormDialog"; import TaskFormDialog from "/components/tasks/TaskFormDialog";
import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
const tasksFragment = graphql` const tasksFragment = graphql`
fragment TasksPageFragment on Organization fragment TasksPageFragment on Organization
@@ -25,31 +23,21 @@ const tasksFragment = graphql`
before: { type: "CursorKey", defaultValue: null } before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null } last: { type: "Int", defaultValue: null }
) { ) {
canCreateTask: permission(action: "core:task:create")
tasks( tasks(
first: $first first: $first
after: $after after: $after
last: $last last: $last
before: $before before: $before
orderBy: $order orderBy: $order
) @connection(key: "TasksPageFragment_tasks") { ) @connection(key: "TasksPageFragment_tasks") @required(action: THROW) {
__id __id
edges { edges @required(action: THROW) {
node { node {
id id
name
state state
description
timeEstimate
deadline
...TaskFormDialogFragment ...TaskFormDialogFragment
measure { ...TasksCard_TaskRowFragment
id
name
}
assignedTo {
id
fullName
}
} }
} }
} }
@@ -67,9 +55,7 @@ export default function TasksPage({ queryRef }: Props) {
tasksFragment, tasksFragment,
query.organization as TasksPageFragment$key, query.organization as TasksPageFragment$key,
); );
const tasks = data.tasks?.edges.map((edge) => edge.node);
const connectionId = data.tasks.__id; const connectionId = data.tasks.__id;
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Tasks")); usePageTitle(__("Tasks"));
return ( return (
@@ -80,13 +66,13 @@ export default function TasksPage({ queryRef }: Props) {
"Track your assigned compliance tasks and keep progress on track.", "Track your assigned compliance tasks and keep progress on track.",
)} )}
> >
{isAuthorized("Organization", "createTask") && ( {data.canCreateTask && (
<TaskFormDialog connection={connectionId}> <TaskFormDialog connection={connectionId}>
<Button icon={IconPlusLarge}>{__("New task")}</Button> <Button icon={IconPlusLarge}>{__("New task")}</Button>
</TaskFormDialog> </TaskFormDialog>
)} )}
</PageHeader> </PageHeader>
<TasksCard connectionId={connectionId} tasks={tasks ?? []} /> <TasksCard connectionId={connectionId} tasks={data.tasks.edges} />
</div> </div>
); );
} }

View File

@@ -277,6 +277,7 @@ export default function TrustCenterOverviewTab() {
<h2 className="text-base font-medium">{__("Integrations")}</h2> <h2 className="text-base font-medium">{__("Integrations")}</h2>
<Card padded> <Card padded>
<SlackConnections <SlackConnections
canUpdate={!!organization.trustCenter?.canUpdate}
organizationId={organization.id!} organizationId={organization.id!}
slackConnections={ slackConnections={
organization.slackConnections?.edges?.map((edge) => edge.node) ?? organization.slackConnections?.edges?.map((edge) => edge.node) ??