@salesforce/webapp-template-feature-graphql-experimental 1.27.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.
Files changed (100) hide show
  1. package/LICENSE.txt +82 -0
  2. package/README.md +104 -0
  3. package/dist/.a4drules/build-validation.md +81 -0
  4. package/dist/.a4drules/code-quality.md +150 -0
  5. package/dist/.a4drules/graphql/tools/knowledge/lds-explore-graphql-schema.md +227 -0
  6. package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-mutationquery.md +212 -0
  7. package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-readquery.md +185 -0
  8. package/dist/.a4drules/graphql/tools/knowledge/lds-guide-graphql.md +205 -0
  9. package/dist/.a4drules/graphql/tools/schemas/shared.graphqls +1150 -0
  10. package/dist/.a4drules/graphql.md +408 -0
  11. package/dist/.a4drules/images.md +13 -0
  12. package/dist/.a4drules/react.md +361 -0
  13. package/dist/.a4drules/react_image_processing.md +45 -0
  14. package/dist/.a4drules/skills/install-feature/SKILL.md +66 -0
  15. package/dist/.a4drules/skills/install-feature/scripts/copy-feature-assets.sh +36 -0
  16. package/dist/.a4drules/typescript.md +224 -0
  17. package/dist/.forceignore +15 -0
  18. package/dist/.husky/pre-commit +4 -0
  19. package/dist/.prettierignore +11 -0
  20. package/dist/.prettierrc +17 -0
  21. package/dist/CHANGELOG.md +324 -0
  22. package/dist/README.md +18 -0
  23. package/dist/config/project-scratch-def.json +13 -0
  24. package/dist/force-app/main/default/webapplications/feature-graphql/.graphqlrc.yml +6 -0
  25. package/dist/force-app/main/default/webapplications/feature-graphql/.prettierignore +9 -0
  26. package/dist/force-app/main/default/webapplications/feature-graphql/.prettierrc +11 -0
  27. package/dist/force-app/main/default/webapplications/feature-graphql/build/vite.config.d.ts +2 -0
  28. package/dist/force-app/main/default/webapplications/feature-graphql/build/vite.config.js +73 -0
  29. package/dist/force-app/main/default/webapplications/feature-graphql/e2e/app.spec.ts +24 -0
  30. package/dist/force-app/main/default/webapplications/feature-graphql/eslint.config.js +113 -0
  31. package/dist/force-app/main/default/webapplications/feature-graphql/feature-graphql.webapplication-meta.xml +7 -0
  32. package/dist/force-app/main/default/webapplications/feature-graphql/index.html +13 -0
  33. package/dist/force-app/main/default/webapplications/feature-graphql/package-lock.json +11134 -0
  34. package/dist/force-app/main/default/webapplications/feature-graphql/package.json +52 -0
  35. package/dist/force-app/main/default/webapplications/feature-graphql/playwright.config.ts +24 -0
  36. package/dist/force-app/main/default/webapplications/feature-graphql/scripts/rewrite-e2e-assets.mjs +23 -0
  37. package/dist/force-app/main/default/webapplications/feature-graphql/src/api/graphql-operations-types.ts +124 -0
  38. package/dist/force-app/main/default/webapplications/feature-graphql/src/api/graphql.test.ts +91 -0
  39. package/dist/force-app/main/default/webapplications/feature-graphql/src/api/graphql.ts +35 -0
  40. package/dist/force-app/main/default/webapplications/feature-graphql/src/api/utils/accounts.ts +35 -0
  41. package/dist/force-app/main/default/webapplications/feature-graphql/src/api/utils/query/highRevenueAccountsQuery.graphql +29 -0
  42. package/dist/force-app/main/default/webapplications/feature-graphql/src/api/utils/user.test.ts +124 -0
  43. package/dist/force-app/main/default/webapplications/feature-graphql/src/api/utils/user.ts +45 -0
  44. package/dist/force-app/main/default/webapplications/feature-graphql/src/app.tsx +13 -0
  45. package/dist/force-app/main/default/webapplications/feature-graphql/src/appLayout.tsx +11 -0
  46. package/dist/force-app/main/default/webapplications/feature-graphql/src/assets/icons/book.svg +3 -0
  47. package/dist/force-app/main/default/webapplications/feature-graphql/src/assets/icons/copy.svg +4 -0
  48. package/dist/force-app/main/default/webapplications/feature-graphql/src/assets/icons/rocket.svg +3 -0
  49. package/dist/force-app/main/default/webapplications/feature-graphql/src/assets/icons/star.svg +3 -0
  50. package/dist/force-app/main/default/webapplications/feature-graphql/src/assets/images/codey-1.png +0 -0
  51. package/dist/force-app/main/default/webapplications/feature-graphql/src/assets/images/codey-2.png +0 -0
  52. package/dist/force-app/main/default/webapplications/feature-graphql/src/assets/images/codey-3.png +0 -0
  53. package/dist/force-app/main/default/webapplications/feature-graphql/src/assets/images/vibe-codey.svg +194 -0
  54. package/dist/force-app/main/default/webapplications/feature-graphql/src/components/AccountsTable.tsx +121 -0
  55. package/dist/force-app/main/default/webapplications/feature-graphql/src/index.ts +7 -0
  56. package/dist/force-app/main/default/webapplications/feature-graphql/src/navigationMenu.tsx +81 -0
  57. package/dist/force-app/main/default/webapplications/feature-graphql/src/pages/About.tsx +12 -0
  58. package/dist/force-app/main/default/webapplications/feature-graphql/src/pages/AccountsPage.tsx +19 -0
  59. package/dist/force-app/main/default/webapplications/feature-graphql/src/pages/Home.tsx +12 -0
  60. package/dist/force-app/main/default/webapplications/feature-graphql/src/pages/NotFound.tsx +18 -0
  61. package/dist/force-app/main/default/webapplications/feature-graphql/src/pages/about.tsx +10 -0
  62. package/dist/force-app/main/default/webapplications/feature-graphql/src/pages/new.tsx +10 -0
  63. package/dist/force-app/main/default/webapplications/feature-graphql/src/router-utils.tsx +34 -0
  64. package/dist/force-app/main/default/webapplications/feature-graphql/src/routes.tsx +48 -0
  65. package/dist/force-app/main/default/webapplications/feature-graphql/src/styles/global.css +13 -0
  66. package/dist/force-app/main/default/webapplications/feature-graphql/tsconfig.json +36 -0
  67. package/dist/force-app/main/default/webapplications/feature-graphql/tsconfig.node.json +13 -0
  68. package/dist/force-app/main/default/webapplications/feature-graphql/vite-env.d.ts +1 -0
  69. package/dist/force-app/main/default/webapplications/feature-graphql/vite.config.ts +69 -0
  70. package/dist/force-app/main/default/webapplications/feature-graphql/vitest-env.d.ts +2 -0
  71. package/dist/force-app/main/default/webapplications/feature-graphql/vitest.config.ts +11 -0
  72. package/dist/force-app/main/default/webapplications/feature-graphql/vitest.setup.ts +1 -0
  73. package/dist/force-app/main/default/webapplications/feature-graphql/webapplication.json +7 -0
  74. package/dist/jest.config.js +6 -0
  75. package/dist/package.json +37 -0
  76. package/dist/scripts/apex/hello.apex +10 -0
  77. package/dist/scripts/soql/account.soql +6 -0
  78. package/dist/sfdx-project.json +12 -0
  79. package/package.json +48 -0
  80. package/rules/graphql-data-access-rule.md +411 -0
  81. package/skills/graphql-data-access/SKILL.md +167 -0
  82. package/skills/graphql-data-access/docs/explore-schema.md +256 -0
  83. package/skills/graphql-data-access/docs/generate-mutation-query.md +222 -0
  84. package/skills/graphql-data-access/docs/generate-read-query.md +195 -0
  85. package/skills/graphql-data-access/docs/shared-schema.graphqls +1150 -0
  86. package/skills/graphql-data-access/scripts/codegen.js +15 -0
  87. package/skills/graphql-data-access/scripts/get-graphql-schema.mjs +59 -0
  88. package/src/force-app/main/default/webapplications/feature-graphql/.graphqlrc.yml +6 -0
  89. package/src/force-app/main/default/webapplications/feature-graphql/src/api/graphql-operations-types.ts +124 -0
  90. package/src/force-app/main/default/webapplications/feature-graphql/src/api/graphql.test.ts +91 -0
  91. package/src/force-app/main/default/webapplications/feature-graphql/src/api/graphql.ts +35 -0
  92. package/src/force-app/main/default/webapplications/feature-graphql/src/api/utils/accounts.ts +35 -0
  93. package/src/force-app/main/default/webapplications/feature-graphql/src/api/utils/query/highRevenueAccountsQuery.graphql +29 -0
  94. package/src/force-app/main/default/webapplications/feature-graphql/src/api/utils/user.test.ts +124 -0
  95. package/src/force-app/main/default/webapplications/feature-graphql/src/api/utils/user.ts +45 -0
  96. package/src/force-app/main/default/webapplications/feature-graphql/src/components/AccountsTable.tsx +121 -0
  97. package/src/force-app/main/default/webapplications/feature-graphql/src/index.ts +7 -0
  98. package/src/force-app/main/default/webapplications/feature-graphql/src/pages/AccountsPage.tsx +19 -0
  99. package/src/force-app/main/default/webapplications/feature-graphql/src/routes.tsx +20 -0
  100. package/src/force-app/main/default/webapplications/feature-graphql/vite.config.ts +69 -0
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "base-react-app",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "build:e2e": "npm run build && node scripts/rewrite-e2e-assets.mjs",
10
+ "lint": "eslint .",
11
+ "preview": "vite preview",
12
+ "test": "vitest"
13
+ },
14
+ "dependencies": {
15
+ "@salesforce/sdk-data": "^1.22.0",
16
+ "@salesforce/webapp-experimental": "*",
17
+ "@tailwindcss/vite": "^4.1.17",
18
+ "react": "^19.2.0",
19
+ "react-dom": "^19.2.0",
20
+ "react-router": "^7.10.1",
21
+ "tailwindcss": "^4.1.17"
22
+ },
23
+ "devDependencies": {
24
+ "@eslint/js": "^9.39.1",
25
+ "@graphql-codegen/cli": "^6.1.1",
26
+ "@graphql-codegen/typescript": "^5.0.7",
27
+ "@graphql-codegen/typescript-operations": "^5.0.7",
28
+ "@playwright/test": "^1.49.0",
29
+ "@salesforce/vite-plugin-webapp-experimental": "*",
30
+ "@testing-library/jest-dom": "^6.6.3",
31
+ "@testing-library/react": "^16.1.0",
32
+ "@testing-library/user-event": "^14.5.2",
33
+ "@types/node": "^24.10.1",
34
+ "@types/react": "^19.2.5",
35
+ "@types/react-dom": "^19.2.3",
36
+ "@vitejs/plugin-react": "^5.1.1",
37
+ "@vitest/ui": "^4.0.17",
38
+ "eslint": "^9.39.1",
39
+ "eslint-plugin-react-hooks": "^7.0.1",
40
+ "eslint-plugin-react-refresh": "^0.4.24",
41
+ "globals": "^16.5.0",
42
+ "graphql": "^16.12.0",
43
+ "graphql-codegen-typescript-operation-types": "^2.0.2",
44
+ "jsdom": "^25.0.1",
45
+ "serve": "^14.2.5",
46
+ "typescript": "~5.9.3",
47
+ "typescript-eslint": "^8.46.4",
48
+ "vite": "^7.2.4",
49
+ "vite-plugin-graphql-codegen": "^3.8.0",
50
+ "vitest": "^4.0.17"
51
+ }
52
+ }
@@ -0,0 +1,24 @@
1
+ import { defineConfig, devices } from '@playwright/test';
2
+
3
+ const E2E_PORT = 5175;
4
+
5
+ export default defineConfig({
6
+ testDir: './e2e',
7
+ fullyParallel: true,
8
+ forbidOnly: !!process.env.CI,
9
+ retries: process.env.CI ? 2 : 0,
10
+ workers: process.env.CI ? 1 : undefined,
11
+ reporter: 'html',
12
+ use: {
13
+ baseURL: `http://localhost:${E2E_PORT}`,
14
+ trace: 'on-first-retry',
15
+ },
16
+ projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
17
+ webServer: {
18
+ // Serve built dist/ with static server so e2e works in CI without SF org (vite preview runs plugin and can fail)
19
+ command: `npx serve dist -l ${E2E_PORT}`,
20
+ url: `http://localhost:${E2E_PORT}`,
21
+ reuseExistingServer: !process.env.CI,
22
+ timeout: process.env.CI ? 120_000 : 60_000,
23
+ },
24
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Prepares dist/ for e2e: root-relative asset paths + SPA fallback for serve.
3
+ */
4
+ import { readFileSync, writeFileSync } from 'node:fs';
5
+ import { join, dirname } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const distDir = join(__dirname, '..', 'dist');
10
+
11
+ // Rewrite index.html so asset paths are root-relative (/assets/...)
12
+ const indexPath = join(distDir, 'index.html');
13
+ let html = readFileSync(indexPath, 'utf8');
14
+ html = html.replace(/(src|href)="[^"]*\/assets\//g, '$1="/assets/');
15
+ writeFileSync(indexPath, html);
16
+
17
+ // SPA fallback so /about, /non-existent-route etc. serve index.html
18
+ writeFileSync(
19
+ join(distDir, 'serve.json'),
20
+ JSON.stringify({
21
+ rewrites: [{ source: '**', destination: '/index.html' }],
22
+ })
23
+ );
@@ -0,0 +1,124 @@
1
+ export type Maybe<T> = T | null;
2
+ export type InputMaybe<T> = Maybe<T>;
3
+ export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
4
+ export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
5
+ export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
6
+ export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = {
7
+ [_ in K]?: never;
8
+ };
9
+ export type Incremental<T> =
10
+ | T
11
+ | { [P in keyof T]?: P extends " $fragmentName" | "__typename" ? T[P] : never };
12
+ /** All built-in and custom scalars, mapped to their actual values */
13
+ export type Scalars = {
14
+ ID: { input: string; output: string };
15
+ String: { input: string; output: string };
16
+ Boolean: { input: boolean; output: boolean };
17
+ Int: { input: number; output: number };
18
+ Float: { input: number; output: number };
19
+ Base64: { input: any; output: any };
20
+ /** An arbitrary precision signed decimal */
21
+ BigDecimal: { input: any; output: any };
22
+ /** An arbitrary precision signed integer */
23
+ BigInteger: { input: any; output: any };
24
+ /** An 8-bit signed integer */
25
+ Byte: { input: any; output: any };
26
+ /** A UTF-16 code unit; a character on Unicode's BMP */
27
+ Char: { input: any; output: any };
28
+ Currency: { input: any; output: any };
29
+ Date: { input: any; output: any };
30
+ DateTime: { input: any; output: any };
31
+ Double: { input: any; output: any };
32
+ Email: { input: any; output: any };
33
+ EncryptedString: { input: any; output: any };
34
+ /** Can be set to an ID or a Reference to the result of another mutation operation. */
35
+ IdOrRef: { input: any; output: any };
36
+ JSON: { input: any; output: any };
37
+ Latitude: { input: any; output: any };
38
+ /** A 64-bit signed integer */
39
+ Long: { input: any; output: any };
40
+ LongTextArea: { input: any; output: any };
41
+ Longitude: { input: any; output: any };
42
+ MultiPicklist: { input: any; output: any };
43
+ Percent: { input: any; output: any };
44
+ PhoneNumber: { input: any; output: any };
45
+ Picklist: { input: any; output: any };
46
+ RichTextArea: { input: any; output: any };
47
+ /** A 16-bit signed integer */
48
+ Short: { input: any; output: any };
49
+ TextArea: { input: any; output: any };
50
+ Time: { input: any; output: any };
51
+ Url: { input: any; output: any };
52
+ };
53
+
54
+ export enum DataType {
55
+ Address = "ADDRESS",
56
+ Anytype = "ANYTYPE",
57
+ Base64 = "BASE64",
58
+ Boolean = "BOOLEAN",
59
+ Combobox = "COMBOBOX",
60
+ Complexvalue = "COMPLEXVALUE",
61
+ Currency = "CURRENCY",
62
+ Date = "DATE",
63
+ Datetime = "DATETIME",
64
+ Double = "DOUBLE",
65
+ Email = "EMAIL",
66
+ Encryptedstring = "ENCRYPTEDSTRING",
67
+ Int = "INT",
68
+ Json = "JSON",
69
+ Junctionidlist = "JUNCTIONIDLIST",
70
+ Location = "LOCATION",
71
+ Long = "LONG",
72
+ Multipicklist = "MULTIPICKLIST",
73
+ Percent = "PERCENT",
74
+ Phone = "PHONE",
75
+ Picklist = "PICKLIST",
76
+ Reference = "REFERENCE",
77
+ String = "STRING",
78
+ Textarea = "TEXTAREA",
79
+ Time = "TIME",
80
+ Url = "URL",
81
+ }
82
+
83
+ export enum FieldExtraTypeInfo {
84
+ ExternalLookup = "EXTERNAL_LOOKUP",
85
+ ImageUrl = "IMAGE_URL",
86
+ IndirectLookup = "INDIRECT_LOOKUP",
87
+ Personname = "PERSONNAME",
88
+ Plaintextarea = "PLAINTEXTAREA",
89
+ Richtextarea = "RICHTEXTAREA",
90
+ SwitchablePersonname = "SWITCHABLE_PERSONNAME",
91
+ }
92
+
93
+ export enum ResultOrder {
94
+ Asc = "ASC",
95
+ Desc = "DESC",
96
+ }
97
+
98
+ export type GetHighRevenueAccountsQueryVariables = Exact<{
99
+ minRevenue?: InputMaybe<Scalars["Currency"]["input"]>;
100
+ }>;
101
+
102
+ export type GetHighRevenueAccountsQuery = {
103
+ uiapi: {
104
+ query: {
105
+ Account?: {
106
+ edges?: Array<{
107
+ node?: {
108
+ Id: string;
109
+ Name?: { value?: string | null } | null;
110
+ AnnualRevenue?: { value?: any | null } | null;
111
+ Industry?: { value?: any | null } | null;
112
+ Website?: { value?: any | null } | null;
113
+ } | null;
114
+ } | null> | null;
115
+ } | null;
116
+ };
117
+ };
118
+ };
119
+
120
+ export type CurrentUserQueryVariables = Exact<{ [key: string]: never }>;
121
+
122
+ export type CurrentUserQuery = {
123
+ uiapi: { currentUser?: { Id: string; Name?: { value?: string | null } | null } | null };
124
+ };
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { executeGraphQL } from "./graphql";
3
+ import { sdk } from "@salesforce/sdk-data";
4
+
5
+ vi.mock("@salesforce/sdk-data", () => ({
6
+ sdk: { graphql: vi.fn() },
7
+ }));
8
+
9
+ describe("executeGraphQL", () => {
10
+ beforeEach(() => {
11
+ vi.clearAllMocks();
12
+ });
13
+
14
+ it("should successfully execute a GraphQL query", async () => {
15
+ const mockData = {
16
+ uiapi: {
17
+ query: {
18
+ Account: {
19
+ edges: [
20
+ {
21
+ node: {
22
+ Id: "001",
23
+ Name: { value: "Test Account" },
24
+ },
25
+ },
26
+ ],
27
+ },
28
+ },
29
+ },
30
+ };
31
+
32
+ vi.mocked(sdk.graphql!).mockResolvedValue({ data: mockData });
33
+
34
+ const query = "query { uiapi { query { Account { edges { node { Id Name { value } } } } } } }";
35
+ const result = await executeGraphQL(query);
36
+
37
+ expect(sdk.graphql).toHaveBeenCalledWith(query, undefined);
38
+ expect(result).toEqual(mockData);
39
+ });
40
+
41
+ it("should handle GraphQL errors", async () => {
42
+ const mockErrors = [{ message: "Field not found" }, { message: "Invalid query" }];
43
+
44
+ vi.mocked(sdk.graphql!).mockResolvedValue({ errors: mockErrors } as any);
45
+
46
+ const query = "query { invalid }";
47
+
48
+ await expect(executeGraphQL(query)).rejects.toThrow(
49
+ "GraphQL Error: Field not found; Invalid query",
50
+ );
51
+ });
52
+
53
+ it("should handle network errors", async () => {
54
+ vi.mocked(sdk.graphql!).mockRejectedValue({
55
+ status: 500,
56
+ message: "Network error",
57
+ });
58
+
59
+ const query = "query { test }";
60
+
61
+ await expect(executeGraphQL(query)).rejects.toThrow("Network error");
62
+ });
63
+
64
+ it("should throw when data is undefined", async () => {
65
+ vi.mocked(sdk.graphql!).mockResolvedValue({} as any);
66
+
67
+ const query = "query { test }";
68
+
69
+ await expect(executeGraphQL(query)).rejects.toThrow("GraphQL Error: No data returned");
70
+ });
71
+
72
+ it("should throw when data is null", async () => {
73
+ vi.mocked(sdk.graphql!).mockResolvedValue({ data: null } as any);
74
+
75
+ const query = "query { test }";
76
+
77
+ await expect(executeGraphQL(query)).rejects.toThrow("GraphQL Error: No data returned");
78
+ });
79
+
80
+ it("should pass variables to the GraphQL query", async () => {
81
+ const mockData = { test: "data" };
82
+ vi.mocked(sdk.graphql!).mockResolvedValue({ data: mockData });
83
+
84
+ const query = "query GetAccount($id: ID!) { account(id: $id) { name } }";
85
+ const variables = { id: "001" };
86
+
87
+ await executeGraphQL(query, variables);
88
+
89
+ expect(sdk.graphql).toHaveBeenCalledWith(query, variables);
90
+ });
91
+ });
@@ -0,0 +1,35 @@
1
+ import { sdk } from "@salesforce/sdk-data";
2
+
3
+ export type NodeOfConnection<T> = T extends {
4
+ edges?: (infer E)[] | null;
5
+ } | null
6
+ ? E extends { node?: infer N } | null
7
+ ? N
8
+ : never
9
+ : never;
10
+
11
+ /**
12
+ * Execute a GraphQL query against the Salesforce GraphQL API
13
+ *
14
+ * @param query - The GraphQL query string
15
+ * @param variables - Optional variables for the query
16
+ * @returns The GraphQL response data
17
+ */
18
+ export async function executeGraphQL<T, InputVariables = Record<string, unknown>>(
19
+ query: string,
20
+ variables?: InputVariables,
21
+ ): Promise<T> {
22
+ const result = await sdk.graphql!<T, InputVariables>(query, variables);
23
+
24
+ if (result.errors?.length) {
25
+ const errorMessages = result.errors.map((e) => e.message).join("; ");
26
+ throw new Error(`GraphQL Error: ${errorMessages}`);
27
+ }
28
+
29
+ if (!result.data) {
30
+ throw new Error("GraphQL Error: No data returned");
31
+ }
32
+
33
+ return result.data;
34
+ }
35
+ export { gql } from "@salesforce/sdk-data";
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @attention agents
3
+ * This file serves as a reference example for accessing Salesforce data via GraphQL.
4
+ * Use this pattern when implementing new data fetching utilities:
5
+ * 1. Define TypeScript interfaces for the response shape
6
+ * 2. Write the GraphQL query using UIAPI syntax
7
+ * 3. Use executeGraphQL<T>() with proper typing
8
+ * 4. Extract and return the relevant data from the response
9
+ */
10
+
11
+ import { executeGraphQL, type NodeOfConnection } from "../graphql";
12
+ import HIGH_REVENUE_ACCOUNTS_QUERY from "./query/highRevenueAccountsQuery.graphql?raw";
13
+ import type {
14
+ GetHighRevenueAccountsQuery,
15
+ GetHighRevenueAccountsQueryVariables,
16
+ } from "../graphql-operations-types";
17
+
18
+ type AccountNode = NodeOfConnection<GetHighRevenueAccountsQuery["uiapi"]["query"]["Account"]>;
19
+
20
+ /**
21
+ * Fetch accounts with annual revenue greater than the specified amount
22
+ *
23
+ * @param minRevenue - Minimum annual revenue threshold (default: 100000)
24
+ * @returns Array of accounts matching the criteria
25
+ */
26
+ export async function getHighRevenueAccounts(
27
+ variables: GetHighRevenueAccountsQueryVariables,
28
+ ): Promise<AccountNode[]> {
29
+ const response = await executeGraphQL<GetHighRevenueAccountsQuery>(
30
+ HIGH_REVENUE_ACCOUNTS_QUERY,
31
+ variables,
32
+ );
33
+
34
+ return response.uiapi?.query?.Account?.edges?.map((edge) => edge?.node) || [];
35
+ }
@@ -0,0 +1,29 @@
1
+ query GetHighRevenueAccounts($minRevenue: Currency) {
2
+ uiapi {
3
+ query {
4
+ Account(
5
+ where: { AnnualRevenue: { gt: $minRevenue } }
6
+ orderBy: { AnnualRevenue: { order: DESC } }
7
+ first: 50
8
+ ) {
9
+ edges {
10
+ node {
11
+ Id
12
+ Name {
13
+ value
14
+ }
15
+ AnnualRevenue {
16
+ value
17
+ }
18
+ Industry {
19
+ value
20
+ }
21
+ Website {
22
+ value
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { getCurrentUser } from "./user";
3
+ import { executeGraphQL } from "../graphql";
4
+
5
+ vi.mock("../graphql", async () => {
6
+ return {
7
+ gql: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => {
8
+ return strings.reduce((prev, curr, i) => {
9
+ return prev + curr + (values[i] ?? "");
10
+ }, "");
11
+ }),
12
+ executeGraphQL: vi.fn(),
13
+ };
14
+ });
15
+
16
+ // Get reference to the mocked function for assertions
17
+ const mockGraphql = vi.mocked(executeGraphQL);
18
+
19
+ // Mock console.error to avoid noise in test output
20
+ const mockConsoleError = vi.spyOn(console, "error").mockImplementation(() => {});
21
+
22
+ describe("User API Utils", () => {
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ });
26
+
27
+ afterEach(() => {
28
+ mockConsoleError.mockClear();
29
+ });
30
+
31
+ describe("getCurrentUser", () => {
32
+ const mockResponse = {
33
+ uiapi: {
34
+ currentUser: {
35
+ Id: "005000000000001",
36
+ Name: { value: "John Doe" },
37
+ },
38
+ },
39
+ } as any;
40
+
41
+ it("successfully fetches current user", async () => {
42
+ mockGraphql.mockResolvedValue(mockResponse);
43
+
44
+ const result = await getCurrentUser();
45
+
46
+ expect(mockGraphql).toHaveBeenCalledWith(expect.any(String));
47
+ expect(result).toEqual({
48
+ id: "005000000000001",
49
+ name: "John Doe",
50
+ });
51
+ });
52
+
53
+ it('falls back to "User" when name is empty', async () => {
54
+ mockGraphql.mockResolvedValue({
55
+ uiapi: {
56
+ currentUser: {
57
+ Id: "005000000000003",
58
+ Name: { value: "" },
59
+ },
60
+ },
61
+ } as any);
62
+
63
+ const result = await getCurrentUser();
64
+
65
+ expect(result).toEqual({
66
+ id: "005000000000003",
67
+ name: "User",
68
+ });
69
+ });
70
+
71
+ it('falls back to "User" when name is null', async () => {
72
+ mockGraphql.mockResolvedValue({
73
+ uiapi: {
74
+ currentUser: {
75
+ Id: "005000000000004",
76
+ Name: { value: null },
77
+ },
78
+ },
79
+ } as any);
80
+
81
+ const result = await getCurrentUser();
82
+
83
+ expect(result).toEqual({
84
+ id: "005000000000004",
85
+ name: "User",
86
+ });
87
+ });
88
+
89
+ it('falls back to "User" when name is undefined', async () => {
90
+ mockGraphql.mockResolvedValue({
91
+ uiapi: {
92
+ currentUser: {
93
+ Id: "005000000000005",
94
+ Name: { value: undefined },
95
+ },
96
+ },
97
+ } as any);
98
+
99
+ const result = await getCurrentUser();
100
+
101
+ expect(result).toEqual({
102
+ id: "005000000000005",
103
+ name: "User",
104
+ });
105
+ });
106
+
107
+ it("throws error when request fails", async () => {
108
+ const apiError = new Error("Bad stuff");
109
+ mockGraphql.mockRejectedValue(apiError);
110
+
111
+ await expect(getCurrentUser()).rejects.toThrow("Bad stuff");
112
+ expect(mockConsoleError).toHaveBeenCalledWith("Error fetching user data:", apiError);
113
+ });
114
+
115
+ it("throws error when no user data is found", async () => {
116
+ mockGraphql.mockResolvedValue({
117
+ uiapi: {},
118
+ } as any);
119
+
120
+ await expect(getCurrentUser()).rejects.toThrow("No user data found");
121
+ expect(mockConsoleError).toHaveBeenCalled();
122
+ });
123
+ });
124
+ });
@@ -0,0 +1,45 @@
1
+ import { executeGraphQL, gql } from "../graphql";
2
+ import { type CurrentUserQuery } from "../graphql-operations-types";
3
+
4
+ interface User {
5
+ id: string;
6
+ name: string;
7
+ }
8
+
9
+ const CURRENT_USER_QUERY = gql`
10
+ query CurrentUser {
11
+ uiapi {
12
+ currentUser {
13
+ Id
14
+ Name {
15
+ value
16
+ }
17
+ }
18
+ }
19
+ }
20
+ `;
21
+
22
+ /**
23
+ * Fetch current user information from Salesforce
24
+ * Uses Chatter API to get current user details
25
+ * @returns User info or null if no session
26
+ */
27
+ export async function getCurrentUser(): Promise<User | null> {
28
+ try {
29
+ const response = await executeGraphQL<CurrentUserQuery>(CURRENT_USER_QUERY);
30
+
31
+ const userData = response.uiapi.currentUser;
32
+
33
+ if (!userData) {
34
+ throw new Error("No user data found");
35
+ }
36
+
37
+ return {
38
+ id: userData.Id,
39
+ name: userData.Name?.value || "User",
40
+ };
41
+ } catch (error) {
42
+ console.error("Error fetching user data:", error);
43
+ throw error;
44
+ }
45
+ }
@@ -0,0 +1,13 @@
1
+ import { createBrowserRouter, RouterProvider } from 'react-router';
2
+ import { routes } from '@/routes';
3
+ import { StrictMode } from 'react';
4
+ import { createRoot } from 'react-dom/client';
5
+ import './styles/global.css';
6
+
7
+ const router = createBrowserRouter(routes);
8
+
9
+ createRoot(document.getElementById('root')!).render(
10
+ <StrictMode>
11
+ <RouterProvider router={router} />
12
+ </StrictMode>
13
+ );
@@ -0,0 +1,11 @@
1
+ import { Outlet } from "react-router";
2
+ import NavigationMenu from "./navigationMenu";
3
+
4
+ export default function AppLayout() {
5
+ return (
6
+ <>
7
+ <NavigationMenu />
8
+ <Outlet />
9
+ </>
10
+ );
11
+ }
@@ -0,0 +1,3 @@
1
+ <svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M13.4935 0.000188828L13.9735 0.533522V10.5069L13.4935 11.0402H8.21348L7.36015 11.8935H6.61348L5.76015 11.0402H0.48015L0.000149727 10.5069V0.533522L0.48015 0.000188828H5.97348L6.34682 0.160189L6.98682 0.800188L7.62682 0.160189L8.00015 0.000188828H13.4935ZM6.50682 10.3469V1.76019L5.76015 1.01352H1.01348V10.0269H5.97348L6.29348 10.1869L6.50682 10.3469ZM13.0135 10.0269V1.01352H8.21348L7.52015 1.70685V10.2935L7.62682 10.1869L8.00015 10.0269H13.0135ZM5.01348 3.04019V4.00019H1.97348V3.04019H5.01348ZM5.01348 7.04019V8.00019H1.97348V7.04019H5.01348ZM1.97348 5.01352V6.02686H5.01348V5.01352H1.97348ZM12.0001 3.04019V4.00019H9.01348V3.04019H12.0001ZM9.01348 5.01352V6.02686H12.0001V5.01352H9.01348ZM9.01348 7.04019V8.00019H12.0001V7.04019H9.01348Z" fill="#0250D9"/>
3
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M5 5L6.25 3.75H13.0175L17.5 8.2325V17.5L16.25 18.75H6.25L5 17.5V5ZM16.25 8.75L12.5 5H6.25V17.5H16.25V8.75Z" fill="#2E2E2E"/>
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 1.25L2.5 2.5V15L3.75 16.25V2.5H11.7675L10.5175 1.25H3.75Z" fill="#2E2E2E"/>
4
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M13.4935 0.000149727L13.9735 0.480149C13.9735 2.15126 13.5646 3.80459 12.7468 5.44015C12.0713 6.7557 11.1468 8.05348 9.97348 9.33348V13.4935L9.49348 13.9735H6.50682L6.13348 13.8668L0.16015 7.84015L0.000149727 7.46682V4.48015L0.48015 4.00015H4.64015C5.92015 2.82682 7.21793 1.90237 8.53348 1.22682C10.169 0.409039 11.8224 0.000149727 13.4935 0.000149727ZM1.01348 4.96015V7.30682L1.38682 7.68015C2.06237 6.72015 2.80904 5.81348 3.62682 4.96015H1.01348ZM6.66682 12.9601H8.96015V10.3468C8.14237 11.1646 7.25348 11.9113 6.29348 12.5868L6.66682 12.9601ZM5.54682 11.8401C6.40015 11.2713 7.23571 10.6135 8.05348 9.86682C9.33348 8.62237 10.3824 7.36015 11.2002 6.08015C12.2668 4.37348 12.8535 2.68459 12.9601 1.01348C11.289 1.12015 9.60015 1.70682 7.89348 2.77348C6.61348 3.59126 5.35126 4.64015 4.10682 5.92015C3.36015 6.73793 2.70237 7.57348 2.13348 8.42682L5.54682 11.8401ZM2.98682 13.9735H0.000149727V10.9868H1.01348V12.9601H2.98682V13.9735ZM9.76015 6.29348C9.51126 6.64904 9.17348 6.88015 8.74682 6.98682C8.35571 7.05793 7.98237 6.98682 7.62682 6.77348C7.30682 6.52459 7.09348 6.20459 6.98682 5.81348C6.91571 5.38682 6.98682 5.01348 7.20015 4.69348C7.44904 4.33793 7.76904 4.12459 8.16015 4.05348C8.58682 3.94682 8.96015 4.01793 9.28015 4.26682C9.63571 4.48015 9.84904 4.80015 9.92015 5.22682C10.0268 5.61793 9.97348 5.97348 9.76015 6.29348Z" fill="#066AFE"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M5.70684 0.533125C5.8135 0.319791 5.9735 0.177569 6.18684 0.106458C6.40017 0.0353467 6.6135 0.0353467 6.82684 0.106458C7.04017 0.177569 7.20017 0.319791 7.30684 0.533125L8.8535 3.62646L12.2135 4.10646C12.4624 4.14201 12.6579 4.26646 12.8002 4.47979C12.9424 4.65757 13.0135 4.8709 13.0135 5.11979C13.0135 5.33312 12.9246 5.5109 12.7468 5.65312L10.2935 8.05312L10.8802 11.4665C10.9157 11.6798 10.8624 11.8931 10.7202 12.1065C10.5779 12.2842 10.4002 12.4087 10.1868 12.4798C9.9735 12.5509 9.76017 12.5331 9.54684 12.4265L6.50684 10.8265L3.46684 12.4265C3.2535 12.5331 3.04017 12.5509 2.82684 12.4798C2.6135 12.4087 2.43572 12.2842 2.2935 12.1065C2.15128 11.8931 2.09795 11.6798 2.1335 11.4665L2.72017 8.05312L0.266836 5.65312C0.0890582 5.5109 0.000169277 5.33312 0.000169277 5.11979C0.000169277 4.8709 0.0712804 4.65757 0.213503 4.47979C0.355725 4.26646 0.55128 4.14201 0.800169 4.10646L4.16017 3.62646L5.70684 0.533125ZM6.50684 1.17312L5.06684 4.10646C4.92461 4.3909 4.6935 4.56868 4.3735 4.63979L1.12017 5.06646L3.46684 7.35979C3.71572 7.60868 3.80461 7.87535 3.7335 8.15979L3.20017 11.4131L6.08017 9.86646C6.36461 9.72424 6.64906 9.72424 6.9335 9.86646L9.8135 11.4131L9.28017 8.15979C9.24461 7.87535 9.3335 7.60868 9.54684 7.35979L11.8935 5.06646L8.64017 4.63979C8.32017 4.56868 8.08906 4.3909 7.94684 4.10646L6.50684 1.17312Z" fill="#066AFE"/>
3
+ </svg>