@salesforce/templates 66.10.3 → 66.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/generators/uiBundleGenerator.d.ts +1 -0
- package/lib/generators/uiBundleGenerator.js +13 -0
- package/lib/generators/uiBundleGenerator.js.map +1 -1
- package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package-lock.json +257 -257
- package/lib/templates/project/reactinternalapp/CHANGELOG.md +65 -0
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package-lock.json +261 -261
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package.json +4 -4
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/AgentforceConversationClient.tsx +13 -1
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx +4 -2
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/types/conversation.ts +1 -0
- package/lib/templates/project/reactinternalapp/package.json +5 -1
- package/lib/templates/project/reactinternalapp/scripts/org-setup-config-schema.mjs +96 -0
- package/lib/templates/project/reactinternalapp/scripts/org-setup.mjs +408 -178
- package/lib/templates/project/reactinternalapp/scripts/validate-org-setup-config.mjs +38 -0
- package/lib/templates/uiBundles/angularbasic/.forceignore +15 -0
- package/lib/templates/uiBundles/angularbasic/.postcssrc.json +5 -0
- package/lib/templates/uiBundles/angularbasic/.prettierignore +8 -0
- package/lib/templates/uiBundles/angularbasic/.prettierrc +12 -0
- package/lib/templates/uiBundles/angularbasic/CHANGELOG.md +4 -0
- package/lib/templates/uiBundles/angularbasic/README.md +85 -0
- package/lib/templates/uiBundles/angularbasic/_uibundle.uibundle-meta.xml +7 -0
- package/lib/templates/uiBundles/angularbasic/angular.json +94 -0
- package/lib/templates/uiBundles/angularbasic/esbuild/api-version.mjs +17 -0
- package/lib/templates/uiBundles/angularbasic/eslint.config.js +56 -0
- package/lib/templates/uiBundles/angularbasic/middleware/html.mjs +3 -0
- package/lib/templates/uiBundles/angularbasic/middleware/proxy.mjs +12 -0
- package/lib/templates/uiBundles/angularbasic/package-lock.json +14070 -0
- package/lib/templates/uiBundles/angularbasic/package.json +52 -0
- package/lib/templates/uiBundles/angularbasic/playwright.config.ts +25 -0
- package/lib/templates/uiBundles/angularbasic/public/favicon.ico +0 -0
- package/lib/templates/uiBundles/angularbasic/scripts/rewrite-e2e-assets.mjs +30 -0
- package/lib/templates/uiBundles/angularbasic/src/app/api/graphql-client.service.spec.ts +154 -0
- package/lib/templates/uiBundles/angularbasic/src/app/api/graphql-client.service.ts +51 -0
- package/lib/templates/uiBundles/angularbasic/src/app/app.config.ts +8 -0
- package/lib/templates/uiBundles/angularbasic/src/app/app.css +0 -0
- package/lib/templates/uiBundles/angularbasic/src/app/app.html +1 -0
- package/lib/templates/uiBundles/angularbasic/src/app/app.routes.ts +21 -0
- package/lib/templates/uiBundles/angularbasic/src/app/app.spec.ts +18 -0
- package/lib/templates/uiBundles/angularbasic/src/app/app.ts +10 -0
- package/lib/templates/uiBundles/angularbasic/src/app/layout/app-layout.component.html +50 -0
- package/lib/templates/uiBundles/angularbasic/src/app/layout/app-layout.component.spec.ts +63 -0
- package/lib/templates/uiBundles/angularbasic/src/app/layout/app-layout.component.ts +21 -0
- package/lib/templates/uiBundles/angularbasic/src/app/pages/home/home.component.html +6 -0
- package/lib/templates/uiBundles/angularbasic/src/app/pages/home/home.component.spec.ts +23 -0
- package/lib/templates/uiBundles/angularbasic/src/app/pages/home/home.component.ts +7 -0
- package/lib/templates/uiBundles/angularbasic/src/app/pages/not-found/not-found.component.html +12 -0
- package/lib/templates/uiBundles/angularbasic/src/app/pages/not-found/not-found.component.spec.ts +33 -0
- package/lib/templates/uiBundles/angularbasic/src/app/pages/not-found/not-found.component.ts +9 -0
- package/lib/templates/uiBundles/angularbasic/src/index.html +13 -0
- package/lib/templates/uiBundles/angularbasic/src/main.ts +5 -0
- package/lib/templates/uiBundles/angularbasic/src/styles.css +12 -0
- package/lib/templates/uiBundles/angularbasic/src/types/sf-globals.d.ts +9 -0
- package/lib/templates/uiBundles/angularbasic/tsconfig.app.json +11 -0
- package/lib/templates/uiBundles/angularbasic/tsconfig.json +33 -0
- package/lib/templates/uiBundles/angularbasic/tsconfig.spec.json +10 -0
- package/lib/templates/uiBundles/angularbasic/ui-bundle.json +7 -0
- package/lib/templates/uiBundles/reactbasic/package-lock.json +261 -261
- package/lib/templates/uiBundles/reactbasic/package.json +4 -4
- package/package.json +5 -4
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "base-angular-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "sf-angular-serve",
|
|
11
|
+
"build": "ng build",
|
|
12
|
+
"watch": "ng build --watch --configuration development",
|
|
13
|
+
"test": "ng test",
|
|
14
|
+
"lint": "ng lint",
|
|
15
|
+
"build:e2e": "ng build && node scripts/rewrite-e2e-assets.mjs",
|
|
16
|
+
"e2e": "playwright test"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@angular/common": "^21.2.0",
|
|
20
|
+
"@angular/compiler": "^21.2.0",
|
|
21
|
+
"@angular/core": "^21.2.0",
|
|
22
|
+
"@angular/forms": "^21.2.0",
|
|
23
|
+
"@angular/platform-browser": "^21.2.0",
|
|
24
|
+
"@angular/router": "^21.2.0",
|
|
25
|
+
"@salesforce/angular-plugin-ui-bundle": "^10.24.0",
|
|
26
|
+
"@salesforce/platform-sdk": "^10.24.0",
|
|
27
|
+
"rxjs": "~7.8.0",
|
|
28
|
+
"tslib": "^2.3.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@angular-builders/custom-esbuild": "^21.0.0",
|
|
32
|
+
"@angular-eslint/builder": "^21.0.0",
|
|
33
|
+
"@angular/build": "^21.2.11",
|
|
34
|
+
"@angular/cli": "^21.2.11",
|
|
35
|
+
"@angular/compiler-cli": "^21.2.0",
|
|
36
|
+
"@eslint/js": "^9.0.0",
|
|
37
|
+
"@playwright/test": "^1.49.0",
|
|
38
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
39
|
+
"angular-eslint": "^21.0.0",
|
|
40
|
+
"eslint": "^9.0.0",
|
|
41
|
+
"jsdom": "^28.0.0",
|
|
42
|
+
"postcss": "^8.5.0",
|
|
43
|
+
"serve": "^14.2.5",
|
|
44
|
+
"tailwindcss": "^4.0.0",
|
|
45
|
+
"typescript": "~5.9.2",
|
|
46
|
+
"typescript-eslint": "^8.0.0",
|
|
47
|
+
"vitest": "^4.1.0"
|
|
48
|
+
},
|
|
49
|
+
"overrides": {
|
|
50
|
+
"undici": "^7.28.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
const E2E_PORT = 5176;
|
|
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 a static server so e2e works in CI without an SF org
|
|
19
|
+
// (ng serve runs the SF plugin/proxy middleware and can fail without an org).
|
|
20
|
+
command: `npx serve dist -l ${E2E_PORT}`,
|
|
21
|
+
url: `http://localhost:${E2E_PORT}`,
|
|
22
|
+
reuseExistingServer: !process.env.CI,
|
|
23
|
+
timeout: process.env.CI ? 120_000 : 60_000,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepares dist/ for e2e: root-relative asset paths + SPA fallback for serve.
|
|
3
|
+
*
|
|
4
|
+
* angular.json sets deployUrl: "./", so the built index.html references
|
|
5
|
+
* scripts/styles with a relative "./" prefix (e.g. ./main-HASH.js). When the
|
|
6
|
+
* SPA fallback serves index.html for a deep route like /non-existent-route,
|
|
7
|
+
* those "./" paths resolve against the wrong base and 404, so the app never
|
|
8
|
+
* boots. Rewrite them to root-relative "/" paths.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { join, dirname } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
// angular.json outputPath: { base: "dist", browser: "" } -> files land in dist/
|
|
16
|
+
const distDir = join(__dirname, '..', 'dist');
|
|
17
|
+
|
|
18
|
+
// Rewrite index.html so asset paths are root-relative (./main-x.js -> /main-x.js)
|
|
19
|
+
const indexPath = join(distDir, 'index.html');
|
|
20
|
+
let html = readFileSync(indexPath, 'utf8');
|
|
21
|
+
html = html.replace(/(src|href)="\.\//g, '$1="/');
|
|
22
|
+
writeFileSync(indexPath, html);
|
|
23
|
+
|
|
24
|
+
// SPA fallback so /non-existent-route etc. serve index.html
|
|
25
|
+
writeFileSync(
|
|
26
|
+
join(distDir, 'serve.json'),
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
rewrites: [{ source: '**', destination: '/index.html' }],
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { GraphqlClient } from './graphql-client.service';
|
|
3
|
+
import { createDataSDK } from '@salesforce/platform-sdk';
|
|
4
|
+
|
|
5
|
+
vi.mock('@salesforce/platform-sdk', () => ({
|
|
6
|
+
createDataSDK: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe('GraphqlClient', () => {
|
|
10
|
+
let service: GraphqlClient;
|
|
11
|
+
let mockGraphqlQuery: ReturnType<typeof vi.fn>;
|
|
12
|
+
let mockGraphqlMutate: ReturnType<typeof vi.fn>;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
mockGraphqlQuery = vi.fn();
|
|
16
|
+
mockGraphqlMutate = vi.fn();
|
|
17
|
+
|
|
18
|
+
vi.mocked(createDataSDK).mockResolvedValue({
|
|
19
|
+
graphql: {
|
|
20
|
+
query: mockGraphqlQuery,
|
|
21
|
+
mutate: mockGraphqlMutate,
|
|
22
|
+
},
|
|
23
|
+
} as any);
|
|
24
|
+
|
|
25
|
+
await TestBed.configureTestingModule({
|
|
26
|
+
providers: [GraphqlClient],
|
|
27
|
+
}).compileComponents();
|
|
28
|
+
|
|
29
|
+
service = TestBed.inject(GraphqlClient);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should create the service', () => {
|
|
37
|
+
expect(service).toBeTruthy();
|
|
38
|
+
expect(service).toBeInstanceOf(GraphqlClient);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should route plain query operations to graphql.query and return result.data', async () => {
|
|
42
|
+
const operation = 'query { user { id name } }';
|
|
43
|
+
const expectedData = { user: { id: '123', name: 'Test User' } };
|
|
44
|
+
mockGraphqlQuery.mockResolvedValue({ data: expectedData });
|
|
45
|
+
|
|
46
|
+
const result = await service.execute(operation);
|
|
47
|
+
|
|
48
|
+
expect(mockGraphqlQuery).toHaveBeenCalledWith({ query: operation, variables: undefined });
|
|
49
|
+
expect(mockGraphqlMutate).not.toHaveBeenCalled();
|
|
50
|
+
expect(result).toEqual(expectedData);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should route anonymous query operations to graphql.query', async () => {
|
|
54
|
+
const operation = '{ user { id name } }';
|
|
55
|
+
const expectedData = { user: { id: '456', name: 'Anonymous User' } };
|
|
56
|
+
mockGraphqlQuery.mockResolvedValue({ data: expectedData });
|
|
57
|
+
|
|
58
|
+
const result = await service.execute(operation);
|
|
59
|
+
|
|
60
|
+
expect(mockGraphqlQuery).toHaveBeenCalledWith({ query: operation, variables: undefined });
|
|
61
|
+
expect(mockGraphqlMutate).not.toHaveBeenCalled();
|
|
62
|
+
expect(result).toEqual(expectedData);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should route mutation operations to graphql.mutate', async () => {
|
|
66
|
+
const operation = 'mutation { updateUser(id: "123", name: "New Name") { id name } }';
|
|
67
|
+
const expectedData = { updateUser: { id: '123', name: 'New Name' } };
|
|
68
|
+
mockGraphqlMutate.mockResolvedValue({ data: expectedData });
|
|
69
|
+
|
|
70
|
+
const result = await service.execute(operation);
|
|
71
|
+
|
|
72
|
+
expect(mockGraphqlMutate).toHaveBeenCalledWith({ mutation: operation, variables: undefined });
|
|
73
|
+
expect(mockGraphqlQuery).not.toHaveBeenCalled();
|
|
74
|
+
expect(result).toEqual(expectedData);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should detect mutation with leading GraphQL comment and route to graphql.mutate', async () => {
|
|
78
|
+
const operation = '# This is a comment\nmutation { deleteUser(id: "123") { success } }';
|
|
79
|
+
const expectedData = { deleteUser: { success: true } };
|
|
80
|
+
mockGraphqlMutate.mockResolvedValue({ data: expectedData });
|
|
81
|
+
|
|
82
|
+
const result = await service.execute(operation);
|
|
83
|
+
|
|
84
|
+
expect(mockGraphqlMutate).toHaveBeenCalledWith({ mutation: operation, variables: undefined });
|
|
85
|
+
expect(mockGraphqlQuery).not.toHaveBeenCalled();
|
|
86
|
+
expect(result).toEqual(expectedData);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should not treat query as mutation when comment contains "mutation" word', async () => {
|
|
90
|
+
const operation = '# this is not a mutation, just a query\nquery { user { id } }';
|
|
91
|
+
const expectedData = { user: { id: '789' } };
|
|
92
|
+
mockGraphqlQuery.mockResolvedValue({ data: expectedData });
|
|
93
|
+
|
|
94
|
+
const result = await service.execute(operation);
|
|
95
|
+
|
|
96
|
+
expect(mockGraphqlQuery).toHaveBeenCalledWith({ query: operation, variables: undefined });
|
|
97
|
+
expect(mockGraphqlMutate).not.toHaveBeenCalled();
|
|
98
|
+
expect(result).toEqual(expectedData);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should throw error when result.errors contains errors', async () => {
|
|
102
|
+
const operation = 'query { user { id } }';
|
|
103
|
+
mockGraphqlQuery.mockResolvedValue({
|
|
104
|
+
data: null,
|
|
105
|
+
errors: [{ message: 'Field not found' }, { message: 'Permission denied' }],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await expect(service.execute(operation)).rejects.toThrow(
|
|
109
|
+
'GraphQL Error: Field not found; Permission denied',
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should throw error when result.errors contains a single error', async () => {
|
|
114
|
+
const operation = 'query { user { id } }';
|
|
115
|
+
mockGraphqlQuery.mockResolvedValue({
|
|
116
|
+
data: null,
|
|
117
|
+
errors: [{ message: 'Unauthorized' }],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await expect(service.execute(operation)).rejects.toThrow('GraphQL Error: Unauthorized');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should throw error when result.data is null with no errors', async () => {
|
|
124
|
+
const operation = 'query { user { id } }';
|
|
125
|
+
mockGraphqlQuery.mockResolvedValue({ data: null });
|
|
126
|
+
|
|
127
|
+
await expect(service.execute(operation)).rejects.toThrow('GraphQL response data is null');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should pass variables through to graphql.query', async () => {
|
|
131
|
+
const operation = 'query GetUser($id: ID!) { user(id: $id) { id name } }';
|
|
132
|
+
const variables = { id: '999' };
|
|
133
|
+
const expectedData = { user: { id: '999', name: 'Variable User' } };
|
|
134
|
+
mockGraphqlQuery.mockResolvedValue({ data: expectedData });
|
|
135
|
+
|
|
136
|
+
const result = await service.execute(operation, variables);
|
|
137
|
+
|
|
138
|
+
expect(mockGraphqlQuery).toHaveBeenCalledWith({ query: operation, variables });
|
|
139
|
+
expect(result).toEqual(expectedData);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should pass variables through to graphql.mutate', async () => {
|
|
143
|
+
const operation =
|
|
144
|
+
'mutation UpdateUser($id: ID!, $name: String!) { updateUser(id: $id, name: $name) { id name } }';
|
|
145
|
+
const variables = { id: '888', name: 'Updated Name' };
|
|
146
|
+
const expectedData = { updateUser: { id: '888', name: 'Updated Name' } };
|
|
147
|
+
mockGraphqlMutate.mockResolvedValue({ data: expectedData });
|
|
148
|
+
|
|
149
|
+
const result = await service.execute(operation, variables);
|
|
150
|
+
|
|
151
|
+
expect(mockGraphqlMutate).toHaveBeenCalledWith({ mutation: operation, variables });
|
|
152
|
+
expect(result).toEqual(expectedData);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { createDataSDK } from '@salesforce/platform-sdk';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* True when the operation's first definition is a `mutation`. Strips GraphQL
|
|
6
|
+
* comments first so a leading `# ...` line can't mask the keyword. Queries
|
|
7
|
+
* (named or anonymous `{ ... }` shorthand) and subscriptions fall through to
|
|
8
|
+
* query().
|
|
9
|
+
*/
|
|
10
|
+
function isMutation(operation: string): boolean {
|
|
11
|
+
return /^\s*mutation\b/.test(operation.replace(/#[^\n\r]*/g, ''));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Thin GraphQL client: createDataSDK + sdk.graphql with centralized error
|
|
16
|
+
* handling. Mutations are routed to sdk.graphql.mutate and everything else to
|
|
17
|
+
* sdk.graphql.query (the SDK rejects an operation sent to the wrong method).
|
|
18
|
+
* Use with gql-tagged queries and generated operation types for type-safe calls.
|
|
19
|
+
*
|
|
20
|
+
* Injectable mirror of the React template's src/api/graphqlClient.ts so both
|
|
21
|
+
* frameworks share the same platform-sdk data-fetch contract.
|
|
22
|
+
*/
|
|
23
|
+
@Injectable({ providedIn: 'root' })
|
|
24
|
+
export class GraphqlClient {
|
|
25
|
+
async execute<TData, TVariables = Record<string, unknown>>(
|
|
26
|
+
operation: string,
|
|
27
|
+
variables?: TVariables,
|
|
28
|
+
): Promise<TData> {
|
|
29
|
+
const data = await createDataSDK();
|
|
30
|
+
const result = isMutation(operation)
|
|
31
|
+
? await data.graphql!.mutate<TData, TVariables>({
|
|
32
|
+
mutation: operation,
|
|
33
|
+
variables,
|
|
34
|
+
})
|
|
35
|
+
: await data.graphql!.query<TData, TVariables>({
|
|
36
|
+
query: operation,
|
|
37
|
+
variables,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (result.errors?.length) {
|
|
41
|
+
const msg = result.errors.map((e) => e.message).join('; ');
|
|
42
|
+
throw new Error(`GraphQL Error: ${msg}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (result.data == null) {
|
|
46
|
+
throw new Error('GraphQL response data is null');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result.data;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
|
2
|
+
import { provideRouter } from '@angular/router';
|
|
3
|
+
|
|
4
|
+
import { routes } from './app.routes';
|
|
5
|
+
|
|
6
|
+
export const appConfig: ApplicationConfig = {
|
|
7
|
+
providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)],
|
|
8
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<router-outlet />
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Routes } from '@angular/router';
|
|
2
|
+
import { AppLayoutComponent } from './layout/app-layout.component';
|
|
3
|
+
import { HomeComponent } from './pages/home/home.component';
|
|
4
|
+
import { NotFoundComponent } from './pages/not-found/not-found.component';
|
|
5
|
+
|
|
6
|
+
export const routes: Routes = [
|
|
7
|
+
{
|
|
8
|
+
path: '',
|
|
9
|
+
component: AppLayoutComponent,
|
|
10
|
+
children: [
|
|
11
|
+
{
|
|
12
|
+
path: '',
|
|
13
|
+
component: HomeComponent,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
path: '**',
|
|
17
|
+
component: NotFoundComponent,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { provideRouter } from '@angular/router';
|
|
3
|
+
import { App } from './app';
|
|
4
|
+
|
|
5
|
+
describe('App', () => {
|
|
6
|
+
beforeEach(async () => {
|
|
7
|
+
await TestBed.configureTestingModule({
|
|
8
|
+
imports: [App],
|
|
9
|
+
providers: [provideRouter([])],
|
|
10
|
+
}).compileComponents();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should create the app', () => {
|
|
14
|
+
const fixture = TestBed.createComponent(App);
|
|
15
|
+
const app = fixture.componentInstance;
|
|
16
|
+
expect(app).toBeTruthy();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<nav class="bg-white border-b border-gray-200">
|
|
2
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
3
|
+
<div class="flex justify-between items-center h-16">
|
|
4
|
+
<a routerLink="/" class="text-xl font-semibold text-gray-900"> Angular App </a>
|
|
5
|
+
<button
|
|
6
|
+
(click)="toggleMenu()"
|
|
7
|
+
class="p-2 rounded-md text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
8
|
+
aria-label="Toggle menu"
|
|
9
|
+
[attr.aria-expanded]="isOpen()"
|
|
10
|
+
>
|
|
11
|
+
<div class="w-6 h-6 flex flex-col justify-center space-y-1.5">
|
|
12
|
+
<span
|
|
13
|
+
class="block h-0.5 w-6 bg-current transition-all"
|
|
14
|
+
[class.rotate-45]="isOpen()"
|
|
15
|
+
[class.translate-y-2]="isOpen()"
|
|
16
|
+
></span>
|
|
17
|
+
<span
|
|
18
|
+
class="block h-0.5 w-6 bg-current transition-all"
|
|
19
|
+
[class.opacity-0]="isOpen()"
|
|
20
|
+
></span>
|
|
21
|
+
<span
|
|
22
|
+
class="block h-0.5 w-6 bg-current transition-all"
|
|
23
|
+
[class.-rotate-45]="isOpen()"
|
|
24
|
+
[class.-translate-y-2]="isOpen()"
|
|
25
|
+
></span>
|
|
26
|
+
</div>
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
@if (isOpen()) {
|
|
30
|
+
<div class="pb-4">
|
|
31
|
+
<ul class="flex flex-col space-y-2 list-none">
|
|
32
|
+
@for (item of navigationItems; track item.path) {
|
|
33
|
+
<li>
|
|
34
|
+
<a
|
|
35
|
+
[routerLink]="item.path"
|
|
36
|
+
routerLinkActive="bg-blue-100 text-blue-700"
|
|
37
|
+
[routerLinkActiveOptions]="{ exact: true }"
|
|
38
|
+
(click)="closeMenu()"
|
|
39
|
+
class="block px-3 py-2 rounded-md text-sm font-medium transition-colors text-gray-700 hover:bg-gray-100"
|
|
40
|
+
>
|
|
41
|
+
{{ item.label }}
|
|
42
|
+
</a>
|
|
43
|
+
</li>
|
|
44
|
+
}
|
|
45
|
+
</ul>
|
|
46
|
+
</div>
|
|
47
|
+
}
|
|
48
|
+
</div>
|
|
49
|
+
</nav>
|
|
50
|
+
<router-outlet />
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { provideRouter } from '@angular/router';
|
|
3
|
+
import { AppLayoutComponent } from './app-layout.component';
|
|
4
|
+
|
|
5
|
+
describe('AppLayoutComponent', () => {
|
|
6
|
+
beforeEach(async () => {
|
|
7
|
+
await TestBed.configureTestingModule({
|
|
8
|
+
imports: [AppLayoutComponent],
|
|
9
|
+
providers: [provideRouter([])],
|
|
10
|
+
}).compileComponents();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should create the component', () => {
|
|
14
|
+
const fixture = TestBed.createComponent(AppLayoutComponent);
|
|
15
|
+
const component = fixture.componentInstance;
|
|
16
|
+
expect(component).toBeTruthy();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should toggle menu', () => {
|
|
20
|
+
const fixture = TestBed.createComponent(AppLayoutComponent);
|
|
21
|
+
const component = fixture.componentInstance;
|
|
22
|
+
expect(component.isOpen()).toBe(false);
|
|
23
|
+
component.toggleMenu();
|
|
24
|
+
expect(component.isOpen()).toBe(true);
|
|
25
|
+
component.toggleMenu();
|
|
26
|
+
expect(component.isOpen()).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should close menu', () => {
|
|
30
|
+
const fixture = TestBed.createComponent(AppLayoutComponent);
|
|
31
|
+
const component = fixture.componentInstance;
|
|
32
|
+
component.toggleMenu();
|
|
33
|
+
expect(component.isOpen()).toBe(true);
|
|
34
|
+
component.closeMenu();
|
|
35
|
+
expect(component.isOpen()).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should have navigation items', () => {
|
|
39
|
+
const fixture = TestBed.createComponent(AppLayoutComponent);
|
|
40
|
+
const component = fixture.componentInstance;
|
|
41
|
+
expect(component.navigationItems.length).toBeGreaterThan(0);
|
|
42
|
+
expect(component.navigationItems[0].path).toBe('/');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should render nav element', async () => {
|
|
46
|
+
const fixture = TestBed.createComponent(AppLayoutComponent);
|
|
47
|
+
await fixture.whenStable();
|
|
48
|
+
const compiled = fixture.nativeElement as HTMLElement;
|
|
49
|
+
expect(compiled.querySelector('nav')).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should set aria-expanded on toggle button', async () => {
|
|
53
|
+
const fixture = TestBed.createComponent(AppLayoutComponent);
|
|
54
|
+
await fixture.whenStable();
|
|
55
|
+
const compiled = fixture.nativeElement as HTMLElement;
|
|
56
|
+
const button = compiled.querySelector('button');
|
|
57
|
+
expect(button?.getAttribute('aria-expanded')).toBe('false');
|
|
58
|
+
|
|
59
|
+
fixture.componentInstance.toggleMenu();
|
|
60
|
+
fixture.detectChanges();
|
|
61
|
+
expect(button?.getAttribute('aria-expanded')).toBe('true');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
|
3
|
+
|
|
4
|
+
@Component({
|
|
5
|
+
selector: 'app-layout',
|
|
6
|
+
imports: [RouterLink, RouterLinkActive, RouterOutlet],
|
|
7
|
+
templateUrl: './app-layout.component.html',
|
|
8
|
+
})
|
|
9
|
+
export class AppLayoutComponent {
|
|
10
|
+
isOpen = signal(false);
|
|
11
|
+
|
|
12
|
+
navigationItems = [{ path: '/', label: 'Home' }];
|
|
13
|
+
|
|
14
|
+
toggleMenu(): void {
|
|
15
|
+
this.isOpen.update((v) => !v);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
closeMenu(): void {
|
|
19
|
+
this.isOpen.set(false);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { HomeComponent } from './home.component';
|
|
3
|
+
|
|
4
|
+
describe('HomeComponent', () => {
|
|
5
|
+
beforeEach(async () => {
|
|
6
|
+
await TestBed.configureTestingModule({
|
|
7
|
+
imports: [HomeComponent],
|
|
8
|
+
}).compileComponents();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should create the component', () => {
|
|
12
|
+
const fixture = TestBed.createComponent(HomeComponent);
|
|
13
|
+
const component = fixture.componentInstance;
|
|
14
|
+
expect(component).toBeTruthy();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should render main landmark', async () => {
|
|
18
|
+
const fixture = TestBed.createComponent(HomeComponent);
|
|
19
|
+
await fixture.whenStable();
|
|
20
|
+
const compiled = fixture.nativeElement as HTMLElement;
|
|
21
|
+
expect(compiled.querySelector('main')).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
2
|
+
<div class="text-center">
|
|
3
|
+
<h1 class="text-4xl font-bold text-gray-900 mb-4">404</h1>
|
|
4
|
+
<p class="text-lg text-gray-600 mb-8">Page not found</p>
|
|
5
|
+
<a
|
|
6
|
+
routerLink="/"
|
|
7
|
+
class="inline-block px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
|
8
|
+
>
|
|
9
|
+
Go to Home
|
|
10
|
+
</a>
|
|
11
|
+
</div>
|
|
12
|
+
</main>
|
package/lib/templates/uiBundles/angularbasic/src/app/pages/not-found/not-found.component.spec.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { provideRouter } from '@angular/router';
|
|
3
|
+
import { NotFoundComponent } from './not-found.component';
|
|
4
|
+
|
|
5
|
+
describe('NotFoundComponent', () => {
|
|
6
|
+
beforeEach(async () => {
|
|
7
|
+
await TestBed.configureTestingModule({
|
|
8
|
+
imports: [NotFoundComponent],
|
|
9
|
+
providers: [provideRouter([])],
|
|
10
|
+
}).compileComponents();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should create the component', () => {
|
|
14
|
+
const fixture = TestBed.createComponent(NotFoundComponent);
|
|
15
|
+
const component = fixture.componentInstance;
|
|
16
|
+
expect(component).toBeTruthy();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should render main landmark', async () => {
|
|
20
|
+
const fixture = TestBed.createComponent(NotFoundComponent);
|
|
21
|
+
await fixture.whenStable();
|
|
22
|
+
const compiled = fixture.nativeElement as HTMLElement;
|
|
23
|
+
expect(compiled.querySelector('main')).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should have a link to home', async () => {
|
|
27
|
+
const fixture = TestBed.createComponent(NotFoundComponent);
|
|
28
|
+
await fixture.whenStable();
|
|
29
|
+
const compiled = fixture.nativeElement as HTMLElement;
|
|
30
|
+
const link = compiled.querySelector('a');
|
|
31
|
+
expect(link?.getAttribute('href')).toBe('/');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>MyAngularApp</title>
|
|
6
|
+
<base href="/" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
|
+
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<app-root></app-root>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time constants injected via esbuild `define`. Substituted by
|
|
3
|
+
* @salesforce/angular-plugin-ui-bundle during build (see esbuild/api-version.mjs
|
|
4
|
+
* and angular.json plugins[]).
|
|
5
|
+
*
|
|
6
|
+
* Without substitution, `@salesforce/platform-sdk` and `@salesforce/ui-bundle`
|
|
7
|
+
* fall back to "65.0" — the published packages contain the literal token.
|
|
8
|
+
*/
|
|
9
|
+
declare const __SF_API_VERSION__: string;
|