From fdcffa1f7b6ebf0b4057f979432c86aaf24700b6 Mon Sep 17 00:00:00 2001 From: vaclavstencl Date: Thu, 19 Jun 2025 09:23:25 +0200 Subject: [PATCH 1/3] feat: update graphql guideliness --- .../standards/FE-Standards-graphql.md | 192 ++++++++++++------ 1 file changed, 135 insertions(+), 57 deletions(-) diff --git a/docs/Frontend_tutorials/standards/FE-Standards-graphql.md b/docs/Frontend_tutorials/standards/FE-Standards-graphql.md index a78b989..d9dd9a9 100644 --- a/docs/Frontend_tutorials/standards/FE-Standards-graphql.md +++ b/docs/Frontend_tutorials/standards/FE-Standards-graphql.md @@ -13,58 +13,86 @@ GraphQL API services abstract the complexity of communicating with external endp - **Reusability:** Share common query/mutation logic across multiple features. - **Separation:** Keep HTTP and GraphQL client details isolated from component code. -### Example: DataSourceConfluenceApi +### Example: ProjectStatusReportApi -The **dataSourceConfluenceApi.ts** file implements a GraphQL service to interact with the Confluence API. For example: +The **projectStatusReportApi.ts** file implements a GraphQL service to manage project status reports. It uses the Apollo Client to perform queries, mutations, and subscriptions, while also handling errors and notifications. ```ts -import apolloClient from "../../../../shared/plugins/apolloClient"; -import { ApolloClient } from "@apollo/client/core"; -import { WebDataSourceFragment } from "@/src/easy-ai/dataSourceWeb/shared/graphql/fragments/webDataSource.generated"; +import { ApolloClient, FetchResult } from "@apollo/client/core"; +import apolloClient from "@/src/shared/plugins/apolloClient"; import { notifyOnGraphqlValidationErrors } from "@/src/shared/api/utils"; -import { addConfluenceDataSource } from "@/src/easy-ai/dataSourceWeb/confluence/graphql/mutations/addConfluenceDataSource"; +import { ProjectStatusReportQuery } from "@/src/easy-ai/projectStatusReport/detail/graphql/queries/projectStatusReport.generated"; +import { projectStatusReportQuery } from "@/src/easy-ai/projectStatusReport/detail/graphql/queries/projectStatusReport"; +import { FullProjectStatusReportFragment } from "@/src/easy-ai/projectStatusReport/detail/graphql/fragments/fullProjectStatusReport.generated"; +import { useSubscription, UseSubscriptionReturn } from "@vue/apollo-composable"; +import { projectStatusReportUpdatedSubscription } from "@/src/easy-ai/projectStatusReport/detail/graphql/subscriptions/projectStatusReportUpdated"; +import { DeleteProjectStatusReportMutation } from "@/src/easy-ai/projectStatusReport/detail/graphql/mutation/deleteProjectStatusReport.generated"; +import { deleteProjectStatusReportMutation } from "@/src/easy-ai/projectStatusReport/detail/graphql/mutation/deleteProjectStatusReport"; import { - AddConfluenceDataSourceMutation, - AddConfluenceDataSourceMutationVariables, -} from "@/src/easy-ai/dataSourceWeb/confluence/graphql/mutations/addConfluenceDataSource.generated"; -import { CollectionConfluenceDataSourcesQuery } from "@/src/easy-ai/dataSourceWeb/confluence/graphql/queries/collectionConfluenceDataSources.generated"; -import { collectionConfluenceDataSourcesQuery } from "@/src/easy-ai/dataSourceWeb/confluence/graphql/queries/collectionConfluenceDataSources"; + ProjectStatusReportUpdatedSubscription, + ProjectStatusReportUpdatedSubscriptionVariables, +} from "@/src/easy-ai/projectStatusReport/detail/graphql/subscriptions/projectStatusReportUpdated.generated"; -export class DataSourceConfluenceApi { +export class ProjectStatusReportApi { constructor(private readonly apolloClient: ApolloClient) {} - public async addConfluenceDataSource( - variables: AddConfluenceDataSourceMutationVariables - ): Promise { - const { data } = await this.apolloClient.mutate({ - mutation: addConfluenceDataSource, - variables, + getProjectStatusReport = async (projectStatusReportId: string): Promise => { + const { data }: FetchResult = await this.apolloClient.query({ + query: projectStatusReportQuery, + variables: { + id: projectStatusReportId, + }, }); - notifyOnGraphqlValidationErrors(data?.addConfluenceDataSource); - - return data?.addConfluenceDataSource?.collection?.dataSourceConfluences || []; - } + return data?.easyAiProjectStatusReport || null; + }; - public async getConfluenceDataSources(collectionId: string): Promise { - const response = await this.apolloClient.query({ - query: collectionConfluenceDataSourcesQuery, + deleteProjectStatusReport = async (projectStatusReportId: string): Promise => { + const { data }: FetchResult = await this.apolloClient.mutate({ + mutation: deleteProjectStatusReportMutation, variables: { - collectionId, + id: projectStatusReportId, }, }); - return response.data.easyAiVectorDbCollection?.dataSourceConfluences || []; - } -} + notifyOnGraphqlValidationErrors(data?.deleteEasyAiProjectStatusReport); + }; -const dataSourceConfluenceApi = new DataSourceConfluenceApi(apolloClient); + useProjectStatusReportSubscription = ( + projectStatusReportId: string + ): UseSubscriptionReturn => { + return useSubscription(projectStatusReportUpdatedSubscription, { id: projectStatusReportId }); + }; +} -export default dataSourceConfluenceApi; +export default new ProjectStatusReportApi(apolloClient); ``` This service hides the details of setting up the GraphQL client and managing errors from the rest of the application. +### Example: Using of useProjectStatusReportSubscription method + +This example shows how to use the `useProjectStatusReportSubscription` method from the `ProjectStatusReportApi` service to subscribe to updates for a specific project status report. It also demonstrates how to handle the subscription lifecycle, including stopping the previous subscription if it exists. + +```ts +public initSubscription = (projectStatusReportId: string) => { + if (this.stopCurrentSubscription) { + this.stopCurrentSubscription(); + } + + if (this.shouldSubscribe()) { + const { onResult, stop } = projectStatusReportApi.useProjectStatusReportSubscription(projectStatusReportId); + this.stopCurrentSubscription = stop; + onResult(this.processProjectStatusReportSubscription); + } +}; + +private processProjectStatusReportSubscription = (response: FetchResult) => { + if (!response.data?.easyAiProjectStatusReportUpdated) return; + this.activeProjectStatusReport.value = response.data?.easyAiProjectStatusReportUpdated; +}; +``` + --- ## GraphQL Definitions @@ -78,9 +106,10 @@ GraphQL definitions—queries, mutations, and fragments—should be organized in - **File Structure:** Keep your GraphQL definitions organized in a dedicated [folder structure](https://easysoftware.stoplight.io/docs/developer-portal-devs/cfd453fb9e5be-frontend-project-structure-and-architecture#32-module-structure) and one graphql definition per file. - + - **Use of Fragments:** Define fragments for recurring field sets. This avoids redundancy and simplifies future updates. It is possible to nest fragments if suitable. + - **Descriptive Naming:** Use clear names for queries, mutations, and fragments. For instance, our query in **collectionConfluenceDataSources.ts** is named `collectionConfluenceDataSourcesQuery` and our reusable fragment defined in **webDataSource.ts** is called `webDataSourceFragment`. Name in the graphql definition is the same as const, only without `Fragment`, `Query`, `Mutation` or `Subscription` suffix. @@ -90,51 +119,100 @@ GraphQL definitions—queries, mutations, and fragments—should be organized in --- -### Example: Query and Fragment Definitions +### Example of definitions -#### GraphQL Fragment: webDataSourceFragment +#### GraphQL Fragment: fullProjectStatusReport -The **webDataSource.ts** file includes the fragment that defines the common fields for a web data source: +The **fullProjectStatusReport.ts** file defines a fragment that includes all necessary fields for a project status report. This fragment can be reused in queries and mutations to ensure consistency and reduce duplication. Inside the fragment we can use other fragments to avoid duplication of code. For example, we can use `projectStatusReportBlockDataFragment` and `repeatingTemplateFragment` to include specific fields related to project status report blocks and repeating templates. ```ts import { gql } from "@apollo/client"; - -export const webDataSourceFragment = gql` - fragment WebDataSource on DataSource { +import { projectStatusReportBlockDataFragment } from "@/src/easy-ai/projectStatusReport/detail/graphql/fragments/projectStatusReportBlockData"; +import { repeatingTemplateFragment } from "@/src/easy-ai/projectStatusReport/detail/graphql/fragments/repeatingTemplateFragment"; + +export const fullProjectStatusReportFragment = gql` + fragment FullProjectStatusReport on ProjectStatusReport { + blocksToReport + createdAt + blocksData { + ...ProjectStatusReportBlockData + } id - status - author { - id - name + interval + manageable + name + repeatingTemplate { + ...RepeatingTemplate + } + project { + identifier } - type - url - attachment { - contentUrl + } + + ${projectStatusReportBlockDataFragment} + ${repeatingTemplateFragment} +`; +``` + +#### GraphQL Query: projectStatusReport + +The **projectStatusReport.ts** file defines a query to fetch a project status report, using the `fullProjectStatusReportFragment`: + +```ts +import { gql } from "@apollo/client"; +import { fullProjectStatusReportFragment } from "@/src/easy-ai/projectStatusReport/detail/graphql/fragments/fullProjectStatusReport"; + +export const projectStatusReportQuery = gql` + query projectStatusReport($id: ID!) { + easyAiProjectStatusReport(id: $id) { + ...FullProjectStatusReport } - typeProperties } + + ${fullProjectStatusReportFragment} `; ``` -#### GraphQL Query: collectionConfluenceDataSourcesQuery +#### GraphQL Mutation: deleteProjectStatusReport -The **collectionConfluenceDataSources.ts** file contains a query that retrieves a collection of Confluence data sources while reusing the `webDataSourceFragment`: +The **deleteProjectStatusReport.ts** file defines a mutation to delete a project status report, reusing the `easyErrorFragment` for error handling: ```ts import { gql } from "@apollo/client"; -import { webDataSourceFragment } from "@/src/easy-ai/dataSourceWeb/shared/graphql/fragments/webDataSource"; +import { easyErrorFragment } from "@/src/shared/graphql/fragments/error"; -export const collectionConfluenceDataSourcesQuery = gql` - query collectionConfluenceDataSources($collectionId: ID!) { - easyAiVectorDbCollection(id: $collectionId) { - dataSourceConfluences { - ...WebDataSource +export const deleteProjectStatusReportMutation = gql` + mutation deleteProjectStatusReport($id: ID!) { + deleteEasyAiProjectStatusReport(id: $id) { + errors { + ...EasyError } } } - ${webDataSourceFragment} + ${easyErrorFragment} +`; +``` + + +> Note: Each mutation must return an `errors` field that contains the `EasyError` fragment. This is crucial for proper error handling. + +#### GraphQL Subscription: projectStatusReportUpdated + +The **projectStatusReportUpdated.ts** file defines a subscription to listen for updates to a project status report, using the `fullProjectStatusReportFragment`: + +```ts +import { gql } from "@apollo/client"; +import { fullProjectStatusReportFragment } from "@/src/easy-ai/projectStatusReport/detail/graphql/fragments/fullProjectStatusReport"; + +export const projectStatusReportUpdatedSubscription = gql` + subscription projectStatusReportUpdated($id: ID!) { + easyAiProjectStatusReportUpdated(id: $id) { + ...FullProjectStatusReport + } + } + + ${fullProjectStatusReportFragment} `; ``` @@ -142,7 +220,7 @@ export const collectionConfluenceDataSourcesQuery = gql` > Note: After each update of some graphql definition is needed to run generation of graphql types. [link](https://easysoftware.stoplight.io/docs/developer-portal-devs/a32e74fbf89d3-frontend-code-generator) -> Note: When defining GraphQL queries with the gql tag, use the tagged template literal syntax without enclosing it in parentheses gql\`...\`;. Do not write gql(\`...\`); instead, use it directly followed by a template literal as shown in the examples below. Using parentheses will cause a parsing error—such as Uncaught (in promise) GraphQLError: Syntax Error: Unexpected "["—because it interferes with the proper tag processing of the template literal. For further details on tagged template syntax, please see the MDN documentation on tagged templates. +> Note: When defining GraphQL with the gql tag, use the tagged template literal syntax without enclosing it in parentheses gql\`...\`;. Do not write gql(\`...\`); instead, use it directly followed by a template literal as shown in the examples below. Using parentheses will cause a parsing error—such as Uncaught (in promise) GraphQLError: Syntax Error: Unexpected "["—because it interferes with the proper tag processing of the template literal. For further details on tagged template syntax, please see the MDN documentation on tagged templates. ### GraphQL Definitions with fields from plugins From 4cd31197ca6d311ed043f8afe1411a0271e3bc37 Mon Sep 17 00:00:00 2001 From: vaclavstencl Date: Mon, 30 Jun 2025 14:51:36 +0200 Subject: [PATCH 2/3] ``` docs: update GraphQL standards for dynamic queries This update enhances the documentation on using GraphQL with directives and dynamic query construction. It clarifies how to conditionally include fields based on plugin activity and emphasizes the need for proper syntax when utilizing the gql tag. --- .../standards/FE-Standards-graphql.md | 96 ++++++++++++++++--- 1 file changed, 81 insertions(+), 15 deletions(-) diff --git a/docs/Frontend_tutorials/standards/FE-Standards-graphql.md b/docs/Frontend_tutorials/standards/FE-Standards-graphql.md index d9dd9a9..887da6c 100644 --- a/docs/Frontend_tutorials/standards/FE-Standards-graphql.md +++ b/docs/Frontend_tutorials/standards/FE-Standards-graphql.md @@ -222,28 +222,94 @@ export const projectStatusReportUpdatedSubscription = gql` > Note: When defining GraphQL with the gql tag, use the tagged template literal syntax without enclosing it in parentheses gql\`...\`;. Do not write gql(\`...\`); instead, use it directly followed by a template literal as shown in the examples below. Using parentheses will cause a parsing error—such as Uncaught (in promise) GraphQLError: Syntax Error: Unexpected "["—because it interferes with the proper tag processing of the template literal. For further details on tagged template syntax, please see the MDN documentation on tagged templates. +### GraphQL Definitions with directives + +In some cases, we can use [GraphQL directives](https://graphql.org/learn/queries/#directives) to conditionally include/skip fields in our queries. This allows us to write cleaner code without needing to dynamically construct the query string. + +```ts +export const easySprintBoardUpdateSwimlaneSubscription = gql` + subscription easySprintBoardUpdateSwimlaneAttributes($easySprintBoardId: ID!, $hasStickyNotes: Boolean!) { + easySprintBoardUpdateSwimlaneAttributes(easySprintBoardId: $easySprintBoardId) { + result { + mutationName + easySwimlane { + id + color + easyProductBacklogItem { + id + color + } + easyAgileSprint { + id + } + easySprintBoard @include(if: $hasStickyNotes) { + stickyNotesInheritColor + } + easyStickyNotes @include(if: $hasStickyNotes) { + ...StickyNote + } + } + } + } + } + ${stickyNoteFragment} +`; +``` + ### GraphQL Definitions with fields from plugins -Plugins in our system might be inactive at times. Therefore, before adding their fields to a GraphQL query, we must first check if the plugin is active. For example, the getAssignCoworkersQuery function checks whether the isR4aActive variable is true. If it is, the query includes fields related to R4A; otherwise, those fields are omitted because they are not part of the GraphQL schema. As a result, we cannot use [GraphQL directives](https://graphql.org/learn/queries/#directives) to conditionally include these fields. +Plugins in our system might be inactive at times. Therefore, before adding their fields to a GraphQL query, we must first check if the plugin is active. For example, the getValidateIssueMutation function checks whether the sprintOn variable is true. If it is, the query includes fields related to sprint; otherwise, those fields are omitted because they are not part of the GraphQL schema. As a result, we cannot use [GraphQL directives](https://graphql.org/learn/queries/#directives) to conditionally include these fields, and we must construct the query dynamically with js. + + +> Note: In this case, we cannot use template literals with the gql tag, and we must use a function to return the query. We need to ignore eslint rule with `// eslint-disable-next-line easy-rules/no-gql-function-call` in this case, because it is not possible to use the gql tag with template literals. + + +> Note: Using gql tag as a function is allowed only in cases where we need to dynamically construct the query, because of the plugin's. ```ts import { gql } from "@apollo/client"; - -export const getAssignCoworkersQuery = (isR4aActive: boolean) => { - return gql`query assignCoworker($id: ID!) { - easySprintBoard(id: $id) { - id - users { - someBaseUserFields - ${ - isR4aActive - ? ` - someR4aSpecificUserFields - ` - : "" +import { customValueFragment } from "@/src/shared/graphql/fragments/customValue"; + +export const getValidateIssueMutation = ( + sprintOn: boolean, + checklistOn: boolean, +) => { + const sprintFields = ` + easySprint { + capacity + closed + name + } + easyStoryPoints`; + + const projectChecklistsFields = ` + addableChecklists + addableChecklistItems + visibleChecklists + `; + + const validateIssueMutation = ` + mutation issueValidator ($id: ID!, $attributes: IssueAttributes!) { + validateModalIssue(attributes: $attributes, id: $id){ + issue { + id + category + subject + project { + id + name + ${checklistOn ? projectChecklistsFields : ""} + } + ${sprintOn ? sprintFields : ""} + } + errors { + messages } } } - }`; + ${customValueFragment}`; + + // eslint-disable-next-line easy-rules/no-gql-function-call + return gql(validateIssueMutation); }; ``` From d629567da600d7881b32f5b431ee2cacd5d8ba5d Mon Sep 17 00:00:00 2001 From: vaclavstencl Date: Mon, 30 Jun 2025 15:03:38 +0200 Subject: [PATCH 3/3] docs: update GraphQL naming examples Updated the naming examples for GraphQL queries and fragments. The example now references `projectStatusReport.ts` and `fullProjectStatusReport.ts` to provide clarity on naming conventions within the documentation. This ensures consistency with current project standards. --- docs/Frontend_tutorials/standards/FE-Standards-graphql.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Frontend_tutorials/standards/FE-Standards-graphql.md b/docs/Frontend_tutorials/standards/FE-Standards-graphql.md index 887da6c..6ee5322 100644 --- a/docs/Frontend_tutorials/standards/FE-Standards-graphql.md +++ b/docs/Frontend_tutorials/standards/FE-Standards-graphql.md @@ -111,7 +111,7 @@ GraphQL definitions—queries, mutations, and fragments—should be organized in Define fragments for recurring field sets. This avoids redundancy and simplifies future updates. It is possible to nest fragments if suitable. - **Descriptive Naming:** - Use clear names for queries, mutations, and fragments. For instance, our query in **collectionConfluenceDataSources.ts** is named `collectionConfluenceDataSourcesQuery` and our reusable fragment defined in **webDataSource.ts** is called `webDataSourceFragment`. + Use clear names for queries, mutations, and fragments. For instance, our query in **projectStatusReport.ts** is named `projectStatusReportQuery` and our reusable fragment defined in **fullProjectStatusReport.ts** is called `fullProjectStatusReportFragment`. Name in the graphql definition is the same as const, only without `Fragment`, `Query`, `Mutation` or `Subscription` suffix. - **Generate Types:**