@inizioevoke/veeva-astroclm-core 1.0.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 (91) hide show
  1. package/.editorconfig +12 -0
  2. package/build.ts +38 -0
  3. package/dist/apps/index.d.ts +2 -0
  4. package/dist/apps/index.js +2 -0
  5. package/dist/apps/page-manager/bulk-create.d.ts +1 -0
  6. package/dist/apps/page-manager/bulk-create.js +120 -0
  7. package/dist/apps/page-manager/index.d.ts +5 -0
  8. package/dist/apps/page-manager/index.js +24 -0
  9. package/dist/apps/page-manager/single-create.d.ts +1 -0
  10. package/dist/apps/page-manager/single-create.js +78 -0
  11. package/dist/apps/page-manager/templates/contents.astro.txt +12 -0
  12. package/dist/apps/page-manager/templates/page.astro.txt +4 -0
  13. package/dist/apps/page-manager/utils.d.ts +10 -0
  14. package/dist/apps/page-manager/utils.js +57 -0
  15. package/dist/apps/utils.d.ts +5 -0
  16. package/dist/apps/utils.js +43 -0
  17. package/dist/apps/veeva-config-manager/create.d.ts +1 -0
  18. package/dist/apps/veeva-config-manager/create.js +136 -0
  19. package/dist/apps/veeva-config-manager/index.d.ts +3 -0
  20. package/dist/apps/veeva-config-manager/index.js +16 -0
  21. package/dist/apps/veeva-config-manager/templates/config.ts.txt +60 -0
  22. package/dist/env/schema.d.ts +15 -0
  23. package/dist/env/schema.js +28 -0
  24. package/dist/lib/const.d.ts +15 -0
  25. package/dist/lib/const.js +14 -0
  26. package/dist/lib/env.d.ts +8 -0
  27. package/dist/lib/env.js +17 -0
  28. package/dist/lib/index.d.ts +5 -0
  29. package/dist/lib/index.js +5 -0
  30. package/dist/lib/logger.d.ts +9 -0
  31. package/dist/lib/logger.js +72 -0
  32. package/dist/lib/parse-argv.d.ts +31 -0
  33. package/dist/lib/parse-argv.js +109 -0
  34. package/dist/lib/parse-env.d.ts +45 -0
  35. package/dist/lib/parse-env.js +124 -0
  36. package/dist/lib/utils.d.ts +8 -0
  37. package/dist/lib/utils.js +37 -0
  38. package/dist/tasks/copy-files.d.ts +9 -0
  39. package/dist/tasks/copy-files.js +15 -0
  40. package/dist/tasks/create-csv.d.ts +15 -0
  41. package/dist/tasks/create-csv.js +186 -0
  42. package/dist/tasks/create-zips.d.ts +5 -0
  43. package/dist/tasks/create-zips.js +16 -0
  44. package/dist/tasks/deploy.d.ts +10 -0
  45. package/dist/tasks/deploy.js +49 -0
  46. package/dist/tasks/generate-thumbs.d.ts +12 -0
  47. package/dist/tasks/generate-thumbs.js +152 -0
  48. package/dist/tasks/index.d.ts +1 -0
  49. package/dist/tasks/index.js +1 -0
  50. package/dist/tasks/update-shared.d.ts +5 -0
  51. package/dist/tasks/update-shared.js +41 -0
  52. package/dist/types/config.d.ts +52 -0
  53. package/dist/types/env.d.ts +7 -0
  54. package/dist/types/index.d.ts +9 -0
  55. package/dist/types/veeva.d.ts +58 -0
  56. package/dist/veeva/slideManager.d.ts +33 -0
  57. package/dist/veeva/slideManager.js +120 -0
  58. package/package.json +39 -0
  59. package/src/apps/index.ts +2 -0
  60. package/src/apps/page-manager/bulk-create.ts +131 -0
  61. package/src/apps/page-manager/index.ts +31 -0
  62. package/src/apps/page-manager/single-create.ts +97 -0
  63. package/src/apps/page-manager/templates/contents.astro.txt +12 -0
  64. package/src/apps/page-manager/templates/page.astro.txt +4 -0
  65. package/src/apps/page-manager/utils.ts +70 -0
  66. package/src/apps/utils.ts +47 -0
  67. package/src/apps/veeva-config-manager/create.ts +153 -0
  68. package/src/apps/veeva-config-manager/index.ts +20 -0
  69. package/src/apps/veeva-config-manager/templates/config.ts.txt +60 -0
  70. package/src/env/schema.ts +43 -0
  71. package/src/lib/const.ts +17 -0
  72. package/src/lib/env.ts +27 -0
  73. package/src/lib/index.ts +5 -0
  74. package/src/lib/logger.ts +84 -0
  75. package/src/lib/parse-argv.ts +125 -0
  76. package/src/lib/parse-env.ts +147 -0
  77. package/src/lib/utils.ts +37 -0
  78. package/src/tasks/copy-files.ts +29 -0
  79. package/src/tasks/create-csv.ts +259 -0
  80. package/src/tasks/create-zips.ts +21 -0
  81. package/src/tasks/deploy.ts +72 -0
  82. package/src/tasks/generate-thumbs.ts +179 -0
  83. package/src/tasks/index.ts +1 -0
  84. package/src/tasks/update-shared.ts +49 -0
  85. package/src/types/config.d.ts +52 -0
  86. package/src/types/env.d.ts +7 -0
  87. package/src/types/index.d.ts +9 -0
  88. package/src/types/veeva.d.ts +58 -0
  89. package/src/veeva/readme.md +77 -0
  90. package/src/veeva/slideManager.ts +139 -0
  91. package/tsconfig.json +27 -0
@@ -0,0 +1,37 @@
1
+ import { DataEvotrk } from '../types';
2
+
3
+ export function formatExtId(...ids: string[]) {
4
+ return ids.map((id) => { return id.toUpperCase(); }).join('_');
5
+ }
6
+
7
+ export function pathToSlideZip(path: string) {
8
+ return `${flattenPath(path, { slashes: false })}.zip`;
9
+ }
10
+
11
+ export function flattenPath(path: string, { slashes = true }: { slashes?: boolean } = {}) {
12
+ return `${slashes ? '/' : ''}${path.toLowerCase().split('/').filter(s => s !== '').join('-')}${slashes ? '/' : ''}`;
13
+ }
14
+
15
+ export function urlToId(url: string | URL, separator = '_' ) {
16
+ return (url instanceof URL ? url.pathname : url).split('/').filter(s => s !== '').map((s) => { return s.toLowerCase().replace(/[^0-9a-z_]/g, separator)}).join(separator);
17
+ }
18
+
19
+ export function dataEvotrkToString(data: DataEvotrk): string {
20
+ data.slide = data.slide ?? '';
21
+ data.type = data.type ?? '';
22
+ data.name = data.name ?? '';
23
+ return Object.entries((data))
24
+ .sort(([key1], [key2]) => {
25
+ if (key1 === 'slide') return -1;
26
+ if (key2 === 'slide') return 1;
27
+ if (key1 === 'type') return -1;
28
+ if (key2 === 'type') return 1;
29
+ if (key1 === 'name') return -1;
30
+ if (key2 === 'name') return 1;
31
+ return 0;
32
+ })
33
+ .map(([key, value]) => {
34
+ return value;
35
+ })
36
+ .join('|')
37
+ }
@@ -0,0 +1,29 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { rm, cp, readdir, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import logger from '../lib/logger.js';
7
+ import type { IVeevaConfig } from '../types';
8
+
9
+ interface Params {
10
+ config: IVeevaConfig
11
+ rootDir: string;
12
+ astroDistDir: string;
13
+ deployDir: string;
14
+ }
15
+ export default async function({ config, rootDir, astroDistDir, deployDir }: Params) {
16
+
17
+ logger.info(`Copying files to ${deployDir.replace(rootDir, '')}`);
18
+
19
+ const dirContent = await readdir(astroDistDir, { withFileTypes: true });
20
+ for(const c of dirContent) {
21
+ if (c.isDirectory() && (c.name === 'shared' || config.presentation.slides.find(s => s.path === c.name))) {
22
+ await cp(join(c.parentPath, c.name), join(deployDir, c.name), { recursive: true });
23
+
24
+ if (c.name === 'shared') {
25
+ writeFile(join(deployDir, c.name, 'index.html'), `<html><head><meta charset="UTF-8"><title>${config.presentation.name} - Shared Resource</title></head><body><h1>${config.presentation.name} - Shared Resource</h1></body></html>`);
26
+ }
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,259 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import type { IVeevaConfig, IVeevaConfigSlide, Actions, IVeevaClmBinderFields } from '../types';
4
+ import logger from '../lib/logger.js';
5
+ import { pathToSlideZip } from '../lib/utils.js';
6
+
7
+ interface Params {
8
+ config: IVeevaConfig
9
+ deployDir: string;
10
+ buildName: string;
11
+ fieldsOnly?: boolean;
12
+ additional?: {
13
+ headers: string[];
14
+ presentation: string[];
15
+ slides: string[][];
16
+ shared: string[];
17
+ }
18
+ }
19
+ export default async function({ config, deployDir, buildName, fieldsOnly = false, additional }: Params) {
20
+ logger.info('Creating multichannel loader CSV');
21
+ const headers: string[] = (fieldsOnly ? CSV_HEADERS_FIELDS_ONLY : CSV_HEADERS).concat(additional?.headers ?? []);
22
+ const presentation = config.presentation;
23
+
24
+ for (const [key, header] of Object.entries(BINDER_PRES_HEADERS)) {
25
+ if (presentation[key as keyof IVeevaClmBinderFields]) {
26
+ headers.push(header);
27
+ }
28
+ }
29
+ for (const [key, header] of Object.entries(BINDER_SLIDE_HEADERS)) {
30
+ if (presentation.slideSettings?.[key as keyof IVeevaClmBinderFields] || config.slideSettings?.[key as keyof IVeevaClmBinderFields]) {
31
+ headers.push(header);
32
+ }
33
+ }
34
+
35
+ const rows: string[][] = [headers];
36
+ rows.push(createPresentationRow(headers, config).concat(additional?.presentation ?? []));
37
+ for (let i=0; i<config.presentation.slides.length; i++) {
38
+ rows.push(createSlideRow(headers, config, config.presentation.slides[i]).concat(additional?.slides?.[i] ?? []));
39
+ }
40
+ const shared = createSharedRow(headers, config);
41
+ shared && rows.push(shared.concat(additional?.shared ?? []));
42
+
43
+ await writeFile(join(deployDir, `_${config.presentation.presentationId}_${buildName}.csv`), rows.map((r)=>{return r.join(',');}).join('\n'), { encoding: 'utf8' });
44
+ }
45
+
46
+
47
+ const
48
+ VEEVA_YES = 'YES',
49
+ VEEVA_NO = 'NO',
50
+ VEEVA_TYPE_PRESENTATION = 'Presentation',
51
+ VEEVA_TYPE_SLIDE = 'Slide',
52
+ VEEVA_TYPE_SHARED = 'Shared',
53
+ VEEVA_LIFECYCLE_BINDER = 'Binder Lifecycle',
54
+ VEEVA_LIFECYCLE_CRM_CONTENT = 'CRM Content Lifecycle',
55
+ VEEVA_IOS_RESOLUTION_DEFAULT = 'Default For Device',
56
+ VEEVA_MEDIATYPE_HTML = 'HTML',
57
+
58
+ // HEADERS
59
+ HEADER_FIELDS_ONLY = 'Fields Only',
60
+ HEADER_EXTERNAL_ID = 'external_id__v',
61
+ HEADER_NAME = 'name__v',
62
+ HEADER_TYPE = 'Type',
63
+ HEADER_LIFECYCLE = 'lifecycle__v',
64
+
65
+ HEADER_PRES_ID = 'pres.crm_presentation_id__v',
66
+ HEADER_PRES_TITLE = 'pres.title__v',
67
+ HEADER_PRES_PRODUCT = 'pres.product__v.name__v',
68
+ HEADER_PRES_COUNTRY = 'pres.country__v.name__v',
69
+ HEADER_PRES_LANGUAGE = 'pres.language__v',
70
+ HEADER_PRES_CLM_CONTENT = 'pres.clm_content__v',
71
+ HEADER_PRES_CLM_HIDDEN = 'pres.crm_hidden__v',
72
+ HEADER_PRES_TRAINING = 'pres.crm_training__v',
73
+ HEADER_PRES_CRM_TARGET = 'pres.crm_target_platform__v',
74
+ HEADER_PRES_CRM_ORG = 'pres.crm_org__v.crm_org_id__v',
75
+ HEADER_PRES_CRM_PRODUCT = 'pres.crm_product__v.id', // Get the Veeva ID from the URL
76
+ HEADER_PRES_CRM_CONTENT_GROUP = 'pres.crm_content_group__v.id', // Get the Veeva ID from the URL
77
+ HEADER_PRES_CRM_DETAIL_GROUP = 'pres.crm_detail_group__v.id', // Get the Veeva ID from the URL
78
+ HEADER_PRES_DETAIL_GROUP = 'pres.detail_group__v',
79
+
80
+ HEADER_PRESENTATION = 'Presentation Link',
81
+ HEADER_SLIDE_TITLE = 'slide.title__v',
82
+ HEADER_SLIDE_PRODUCT = 'slide.product__v.name__v',
83
+ HEADER_SLIDE_COUNTRY = 'slide.country__v.name__v',
84
+ HEADER_SLIDE_LANGUAGE = 'slide.language__v',
85
+ HEADER_SLIDE_MEDIA_TYPE = 'slide.crm_media_type__v',
86
+ HEADER_SLIDE_IOS_RESOLUTION = 'slide.ios_resolution__v',
87
+ HEADER_SLIDE_RELATED_SHARED = 'slide.related_shared_resource__v',
88
+ HEADER_SLIDE_CLM_CONTENT = 'slide.clm_content__v',
89
+ HEADER_SLIDE_FILE_NAME = 'slide.filename',
90
+ HEADER_SLIDE_REACTIONS = 'slide.crm_custom_reaction__v',
91
+ HEADER_SLIDE_DISABLE_ACTIONS = 'slide.crm_disable_actions__v',
92
+ HEADER_SLIDE_CRM_TARGET = 'slide.crm_target_platform__v',
93
+ HEADER_SLIDE_CRM_ORG = 'slide.crm_org__v.crm_org_id__v',
94
+ HEADER_SLIDE_CRM_PRODUCT = 'slide.crm_product__v.id', // Get the Veeva ID from the URL
95
+ HEADER_SLIDE_CRM_CONTENT_GROUP = 'slide.crm_content_group__v.id', // Get the Veeva ID from the URL
96
+ HEADER_SLIDE_CRM_DETAIL_GROUP = 'slide.crm_detail_group__v.id', // Get the Veeva ID from the URL
97
+ HEADER_SLIDE_CRM_SHARED = 'slide.crm_shared_resource__v',
98
+ HEADER_SLIDE_DETAIL_GROUP = 'slide.detail_group__v';
99
+
100
+ const CSV_HEADERS = [
101
+ HEADER_EXTERNAL_ID,
102
+ HEADER_TYPE,
103
+ HEADER_NAME,
104
+ HEADER_PRES_ID,
105
+ HEADER_PRES_TITLE,
106
+ HEADER_PRES_PRODUCT,
107
+ HEADER_PRES_COUNTRY,
108
+ HEADER_PRES_TRAINING,
109
+ HEADER_PRES_CLM_CONTENT,
110
+ HEADER_PRES_CLM_HIDDEN,
111
+ HEADER_SLIDE_TITLE,
112
+ HEADER_SLIDE_PRODUCT,
113
+ HEADER_SLIDE_COUNTRY,
114
+ HEADER_SLIDE_MEDIA_TYPE,
115
+ HEADER_SLIDE_IOS_RESOLUTION,
116
+ HEADER_SLIDE_DISABLE_ACTIONS,
117
+ HEADER_SLIDE_REACTIONS,
118
+ HEADER_SLIDE_FILE_NAME,
119
+ HEADER_SLIDE_RELATED_SHARED,
120
+ HEADER_SLIDE_CLM_CONTENT,
121
+ HEADER_SLIDE_CRM_SHARED,
122
+ HEADER_PRESENTATION,
123
+ HEADER_LIFECYCLE
124
+ ];
125
+
126
+ const CSV_HEADERS_FIELDS_ONLY = [
127
+ HEADER_FIELDS_ONLY,
128
+ ...CSV_HEADERS.slice(0, CSV_HEADERS.indexOf(HEADER_SLIDE_FILE_NAME)),
129
+ ...CSV_HEADERS.slice(CSV_HEADERS.indexOf(HEADER_SLIDE_FILE_NAME) + 1)
130
+ ];
131
+
132
+ const BINDER_PRES_HEADERS: Record<keyof IVeevaClmBinderFields, string> = {
133
+ language: HEADER_PRES_LANGUAGE,
134
+ crmTarget: HEADER_PRES_CRM_TARGET,
135
+ crmOrg: HEADER_PRES_CRM_ORG,
136
+ crmProduct: HEADER_PRES_CRM_PRODUCT,
137
+ crmContentGroup: HEADER_PRES_CRM_CONTENT_GROUP,
138
+ crmDetailGroup: HEADER_PRES_CRM_DETAIL_GROUP,
139
+ detailGroup: HEADER_PRES_DETAIL_GROUP
140
+ };
141
+ const BINDER_SLIDE_HEADERS: Record<keyof IVeevaClmBinderFields, string> = {
142
+ language: HEADER_SLIDE_LANGUAGE,
143
+ crmTarget: HEADER_SLIDE_CRM_TARGET,
144
+ crmOrg: HEADER_SLIDE_CRM_ORG,
145
+ crmProduct: HEADER_SLIDE_CRM_PRODUCT,
146
+ crmContentGroup: HEADER_SLIDE_CRM_CONTENT_GROUP,
147
+ crmDetailGroup: HEADER_SLIDE_CRM_DETAIL_GROUP,
148
+ detailGroup: HEADER_SLIDE_DETAIL_GROUP
149
+ };
150
+
151
+
152
+ function createPresentationRow(headers: string[], config: IVeevaConfig): string[] {
153
+ const row: string[] = new Array(headers.length).fill('');
154
+ const presentation = config.presentation;
155
+ addColumn(headers, row, HEADER_FIELDS_ONLY, VEEVA_YES);
156
+ addColumn(headers, row, HEADER_EXTERNAL_ID, presentation.externalId);
157
+ addColumn(headers, row, HEADER_TYPE, VEEVA_TYPE_PRESENTATION);
158
+ addColumn(headers, row, HEADER_NAME, presentation.name);
159
+ addColumn(headers, row, HEADER_PRES_ID, presentation.presentationId);
160
+ addColumn(headers, row, HEADER_PRES_TITLE, presentation.title ?? presentation.name);
161
+ addColumn(headers, row, HEADER_PRES_PRODUCT, presentation.product);
162
+ addColumn(headers, row, HEADER_PRES_COUNTRY, presentation.country);
163
+ addColumn(headers, row, HEADER_PRES_TRAINING, presentation.isTraining);
164
+ addColumn(headers, row, HEADER_PRES_CLM_CONTENT, VEEVA_YES);
165
+ addColumn(headers, row, HEADER_PRES_CLM_HIDDEN, presentation.isHidden ? VEEVA_YES : VEEVA_NO);
166
+ addColumn(headers, row, HEADER_LIFECYCLE, VEEVA_LIFECYCLE_BINDER);
167
+ // Binder fields
168
+ for (const [key, header] of Object.entries(BINDER_PRES_HEADERS)) {
169
+ addColumn(headers, row, header, presentation[key as keyof IVeevaClmBinderFields]);
170
+ }
171
+ return row;
172
+ }
173
+
174
+ function createSlideRow(headers: string[], config: IVeevaConfig, slide: IVeevaConfigSlide): string[] {
175
+ const row: string[] = new Array(headers.length).fill('');
176
+ addColumn(headers, row, HEADER_FIELDS_ONLY, VEEVA_YES);
177
+ addColumn(headers, row, HEADER_EXTERNAL_ID, slide.externalId);
178
+ addColumn(headers, row, HEADER_TYPE, VEEVA_TYPE_SLIDE);
179
+ addColumn(headers, row, HEADER_NAME, slide.name ?? `${config.presentation.name} - ${slide.title ?? slide.path}`);
180
+ addColumn(headers, row, HEADER_SLIDE_TITLE, slide.title ?? slide.path);
181
+ addColumn(headers, row, HEADER_SLIDE_PRODUCT, config.presentation.product);
182
+ addColumn(headers, row, HEADER_SLIDE_COUNTRY, config.presentation.country);
183
+ addColumn(headers, row, HEADER_SLIDE_MEDIA_TYPE, slide.mediaType ?? config.presentation.slideSettings?.mediaType ?? config.slideSettings?.mediaType ?? VEEVA_MEDIATYPE_HTML);
184
+ addColumn(headers, row, HEADER_SLIDE_IOS_RESOLUTION, slide.iosResolution ?? config.presentation.slideSettings?.iosResolution ?? config.slideSettings?.iosResolution ?? VEEVA_IOS_RESOLUTION_DEFAULT);
185
+
186
+ const disable: Set<Actions> = new Set<Actions>(config.presentation.slideSettings?.disableActions ?? config.slideSettings?.disableActions ?? []);
187
+ slide.disableActions?.forEach((action) => {
188
+ disable.add(action);
189
+ });
190
+ slide.enableActions?.forEach((action) => {
191
+ disable.delete(action);
192
+ });
193
+
194
+ addColumn(headers, row, HEADER_SLIDE_REACTIONS, disable.has('Reactions') ? ',a,b,c' : '');
195
+ disable.delete('Reactions');
196
+ addColumn(headers, row, HEADER_SLIDE_DISABLE_ACTIONS, [...disable].join(','));
197
+
198
+ addColumn(headers, row, HEADER_SLIDE_FILE_NAME, pathToSlideZip(slide.path));
199
+ addColumn(headers, row, HEADER_SLIDE_RELATED_SHARED, config.presentation.shared?.externalId ?? '');
200
+ addColumn(headers, row, HEADER_SLIDE_CLM_CONTENT, VEEVA_YES);
201
+ addColumn(headers, row, HEADER_PRESENTATION, config.presentation.externalId);
202
+
203
+ addColumn(headers, row, HEADER_LIFECYCLE, VEEVA_LIFECYCLE_CRM_CONTENT);
204
+
205
+ // Binder fields
206
+ for (const [key, header] of Object.entries(BINDER_SLIDE_HEADERS)) {
207
+ addColumn(headers, row, header, slide[key as keyof IVeevaClmBinderFields] ?? config.presentation.slideSettings?.[key as keyof IVeevaClmBinderFields] ?? config.slideSettings?.[key as keyof IVeevaClmBinderFields]);
208
+ }
209
+ return row;
210
+ }
211
+
212
+ function createSharedRow(headers: string[], config: IVeevaConfig): string[] | null {
213
+ const shared = config.presentation.shared;
214
+ if (!shared) {
215
+ return null;
216
+ }
217
+ const row: string[] = new Array(headers.length).fill('');
218
+ addColumn(headers, row, HEADER_FIELDS_ONLY, VEEVA_YES);
219
+ addColumn(headers, row, HEADER_TYPE, VEEVA_TYPE_SHARED);
220
+ addColumn(headers, row, HEADER_EXTERNAL_ID, shared.externalId);
221
+ addColumn(headers, row, HEADER_NAME, shared.name ?? `${config.presentation.name} - ${shared.title ?? 'Shared Resource'}`);
222
+ addColumn(headers, row, HEADER_SLIDE_TITLE, shared.title ?? 'Shared Resource');
223
+ addColumn(headers, row, HEADER_SLIDE_PRODUCT, config.presentation.product);
224
+ addColumn(headers, row, HEADER_SLIDE_COUNTRY, config.presentation.country);
225
+ addColumn(headers, row, HEADER_SLIDE_MEDIA_TYPE, shared.mediaType ?? VEEVA_MEDIATYPE_HTML);
226
+ addColumn(headers, row, HEADER_SLIDE_FILE_NAME, pathToSlideZip(shared.path ?? 'shared'));
227
+ addColumn(headers, row, HEADER_SLIDE_CLM_CONTENT, VEEVA_YES);
228
+ addColumn(headers, row, HEADER_SLIDE_CRM_SHARED, VEEVA_YES);
229
+ addColumn(headers, row, HEADER_LIFECYCLE, VEEVA_LIFECYCLE_CRM_CONTENT);
230
+ // Binder fields
231
+ for (const [key, header] of Object.entries(BINDER_SLIDE_HEADERS)) {
232
+ addColumn(headers, row, header, shared[key as keyof IVeevaClmBinderFields] ?? config.presentation.slideSettings?.[key as keyof IVeevaClmBinderFields] ?? config.slideSettings?.[key as keyof IVeevaClmBinderFields]);
233
+ }
234
+ return row;
235
+ }
236
+
237
+ function addColumn(headers: string[], row: string[], header: string, value?: any) { // boolean | string | string[]) {
238
+ const index = headers.findIndex(h => h === header);
239
+ if (index !== -1) {
240
+ row[index] = formatValue(value);
241
+ }
242
+ }
243
+ function formatValue(value?: any): string {
244
+ if (value !== undefined && value !== null) {
245
+ if (typeof value == 'object' && !Array.isArray(value)) {
246
+ value = formatValue(Object.entries(value)?.[0]?.[1]) ?? '';
247
+ } else {
248
+ if (Array.isArray(value)) {
249
+ value = value.length > 0 ? value.join(',') : '';
250
+ } else if (typeof value == 'boolean') {
251
+ value = value ? VEEVA_YES : VEEVA_NO;
252
+ }
253
+ if (/("|,)/.test(value)) {
254
+ value = `"${value.replace(/"/g, '""')}"`;
255
+ }
256
+ }
257
+ }
258
+ return value ?? '';
259
+ }
@@ -0,0 +1,21 @@
1
+ import { rm, readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import Zip from 'adm-zip';
4
+ import logger from '../lib/logger.js';
5
+ import { pathToSlideZip } from '../lib/utils.js';
6
+
7
+ interface Params {
8
+ deployDir: string;
9
+ }
10
+ export default async function({ deployDir }: Params) {
11
+ logger.info('Creating ZIP files');
12
+ const dirs = await readdir(deployDir);
13
+
14
+ for (const dir of dirs) {
15
+ const dirPath = join(deployDir, dir);
16
+ const zip = new Zip();
17
+ zip.addLocalFolder(dirPath);
18
+ await zip.writeZipPromise(join(deployDir, pathToSlideZip(dir)));
19
+ await rm(dirPath, { recursive: true });
20
+ }
21
+ }
@@ -0,0 +1,72 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, rm, readdir } from 'node:fs/promises';
3
+ import { join, resolve as resolvePath } from "node:path"
4
+ import { preview } from "astro";
5
+ import { IVeevaConfig } from '../types';
6
+ import loadEnv from '../lib/env.js';
7
+ import logger from '../lib/logger.js';
8
+ import parseArgv from '../lib/parse-argv.js';
9
+ import copyFiles from './copy-files.js';
10
+ import generateThumbs from './generate-thumbs.js';
11
+ import updateShared from './update-shared.js';
12
+ import createZips from './create-zips.js';
13
+ import createCsv from './create-csv.js';
14
+
15
+ const argv = parseArgv({
16
+ mode: 'string',
17
+ fieldsOnly: 'boolean'
18
+ });
19
+
20
+ // const { BUILD_TARGET, VEEVA_ENV, VEEVA_TRAINING } = await loadEnv(argv.mode);
21
+
22
+ interface Opts {
23
+ root: string;
24
+ config: IVeevaConfig;
25
+ env: string;
26
+ csvOnly?: boolean;
27
+ serverPort?: number;
28
+ }
29
+ export default async function({ root, config, env, csvOnly = false, serverPort = 4320 }: Opts) {
30
+ const rootDir = root ?? process.cwd();
31
+ // we could import the astro config and see if the outDir was set but right now it's not necessary
32
+ const astroDistDir = join(rootDir, 'dist');
33
+ const buildName = `${env}${config.presentation.isTraining === true ? '-training' : ''}`;
34
+ const buildType = argv.mode ?? buildName;
35
+ const deployDir = join(rootDir, 'deploy', buildType);
36
+
37
+ if (existsSync(deployDir)) {
38
+ const items = await readdir(deployDir);
39
+ for (const item of items) {
40
+ await rm(join(deployDir, item), { recursive: true });
41
+ }
42
+ } else {
43
+ await mkdir(deployDir, { recursive: true });
44
+ }
45
+
46
+ if (!csvOnly) {
47
+ await copyFiles({ config, rootDir, astroDistDir, deployDir });
48
+ await updateShared({ deployDir });
49
+
50
+ const server = await preview({
51
+ root: rootDir,
52
+ server: {
53
+ port: serverPort
54
+ }
55
+ });
56
+
57
+ await generateThumbs({
58
+ server: { host: server.host ?? 'localhost', port: server.port },
59
+ config,
60
+ deployDir,
61
+ });
62
+
63
+ await server.stop();
64
+
65
+ await createZips({ deployDir });
66
+ }
67
+
68
+ await createCsv({ config, deployDir, buildName, fieldsOnly: csvOnly })
69
+ }
70
+
71
+
72
+
@@ -0,0 +1,179 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import puppeteer, { type Viewport } from "puppeteer";
4
+ import sharp from 'sharp';
5
+
6
+ import logger from '../lib/logger.js';
7
+ import type { IVeevaConfig } from "../types";
8
+
9
+ interface Params {
10
+ config: IVeevaConfig;
11
+ deployDir: string;
12
+ server: {
13
+ protocol?: 'http' | 'https';
14
+ host: string;
15
+ port: number;
16
+ };
17
+ }
18
+
19
+ export default async function({ config, deployDir, server }: Params) {
20
+ logger.info('Generating thumbnails');
21
+ initThumbSettings(config);
22
+ await generateSharedThumb(config, deployDir);
23
+
24
+ const browser = await puppeteer.launch();
25
+ const page = await browser.newPage();
26
+ await page.setViewport({
27
+ width: config.dimensions.width,
28
+ height: config.dimensions.height,
29
+ hasTouch: true,
30
+ isLandscape: true,
31
+ isMobile: true
32
+ });
33
+
34
+ for (const slide of config.presentation.slides) {
35
+ for (const settings of slide.thumbnails!.settings!) {
36
+ logger.info(`Generating /${slide.path}/`);
37
+ const url = `${server.protocol ?? 'http'}://${server.host}:${server.port}/${slide.path}/${settings.queryStringParams ? `?${objToQS(settings.queryStringParams)}` : ''}`
38
+
39
+ try {
40
+ await page.goto(url, {
41
+ waitUntil: 'networkidle0'
42
+ });
43
+ } catch (err) {
44
+ throw err;
45
+ }
46
+ if (settings.delay && settings.delay > 0) {
47
+ logger.info(`Delaying thumbnail generation for ${settings.delay}ms`);
48
+ await sleep(settings.delay);
49
+ }
50
+ let screenshot = await page.screenshot();
51
+ let thumb = await (
52
+ sharp(screenshot)
53
+ .resize({
54
+ width: settings.width,
55
+ height: settings.height
56
+ })
57
+ .png({
58
+ palette: true,
59
+ compressionLevel: 9,
60
+ quality: 80,
61
+ effort: 10
62
+ })
63
+ ).toBuffer();
64
+
65
+ const fileName = typeof settings.filename == 'function' ? settings.filename(config, slide, settings) : settings.filename ?? 'thumb.png';
66
+ await writeFile(join(deployDir, slide.path, fileName), thumb);
67
+ }
68
+ }
69
+
70
+ await page.close();
71
+ await browser.close();
72
+ }
73
+
74
+ async function generateSharedThumb(config: IVeevaConfig, deployDir: string) {
75
+ let settings = config.thumbnails?.settings?.find(s => s.filename === 'thumb.png');
76
+ const width = settings?.width ?? 600;
77
+ const height = settings?.height ?? width / 4 * 3;
78
+ const svgImage = `
79
+ <svg width="${width}" height="${height}">
80
+ <style>
81
+ .title { fill: #fff; font-size: 32px; font-weight: bold; font-family: sans-serif; }
82
+ </style>
83
+ <text x="50%" y="40%" text-anchor="middle" class="title">
84
+ <tspan x="50%" dy="1.2em">${config.presentation.name}</tspan>
85
+ <tspan x="50%" dy="1.2em">Shared Resource</tspan>
86
+ </text>
87
+ </svg>`;
88
+
89
+ const buffer = await sharp({
90
+ create: {
91
+ width,
92
+ height: height,
93
+ channels: 4,
94
+ background: { r: 0, g: 0, b: 0, alpha: 1 } // Blue background
95
+ }
96
+ })
97
+ .composite([{
98
+ input: Buffer.from(svgImage),
99
+ gravity: 'center'
100
+ }])
101
+ .png()
102
+ .toBuffer();
103
+
104
+ await writeFile(join(deployDir, 'shared', 'thumb.png'), buffer);
105
+ }
106
+
107
+ function objToQS(data: Record<string, any>) {
108
+ return Object.entries(data)
109
+ .map(([key, val]) => {
110
+ return `${encodeURIComponent(key)}=${encodeURIComponent(val?.toString() ?? '')}`;
111
+ })
112
+ .join('&');
113
+ }
114
+
115
+ function initThumbSettings(config: IVeevaConfig) {
116
+ if (!config.thumbnails) {
117
+ config.thumbnails = {};
118
+ }
119
+ if (!config.thumbnails.default) {
120
+ config.thumbnails.default = {};
121
+ }
122
+ if (!config.thumbnails.default.width) {
123
+ config.thumbnails.default.width = 600;
124
+ }
125
+ if (!config.thumbnails.default.height) {
126
+ config.thumbnails.default.height = config.thumbnails.default.width / 4 * 3;
127
+ }
128
+ if (!config.thumbnails.default.filename) {
129
+ config.thumbnails.default.filename = 'thumb.png';
130
+ }
131
+ if (!config.thumbnails.settings) {
132
+ config.thumbnails.settings = [];
133
+ }
134
+ if (config.thumbnails.settings.length === 0) {
135
+ config.thumbnails.settings.push(config.thumbnails.default);
136
+ } else {
137
+ for (let i=0; i<config.thumbnails.settings.length; i++) {
138
+ config.thumbnails.settings[i] = {...config.thumbnails.default, ...config.thumbnails.settings[i]};
139
+ }
140
+ }
141
+
142
+ for (const slide of config.presentation.slides) {
143
+ if (!slide.thumbnails) {
144
+ slide.thumbnails = config.thumbnails;
145
+ }
146
+ if (!slide.thumbnails.settings) {
147
+ slide.thumbnails.settings = config.thumbnails.settings;
148
+ } else {
149
+ switch (slide.thumbnails.strategy) {
150
+ case 'append':
151
+ slide.thumbnails.settings = config.thumbnails.settings.concat(
152
+ slide.thumbnails.settings.map((sets) => {
153
+ return {...config.thumbnails!.default, ...slide.thumbnails?.default ?? {}, ...sets}
154
+ })
155
+ );
156
+ break;
157
+ case 'replace':
158
+ break;
159
+ case 'merge':
160
+ default:
161
+ for (let i=0; i<slide.thumbnails.settings.length; i++) {
162
+ slide.thumbnails.settings[i] = {
163
+ ...(config.thumbnails.settings[i] ?? config.thumbnails.default),
164
+ ...(slide.thumbnails.default ?? {}),
165
+ ...slide.thumbnails.settings[i]
166
+ };
167
+ }
168
+ break;
169
+ }
170
+ }
171
+ }
172
+
173
+ }
174
+
175
+ function sleep(timeout = 0) {
176
+ return new Promise((resolve) => {
177
+ setTimeout(resolve, timeout);
178
+ });
179
+ }
@@ -0,0 +1 @@
1
+ export { default as deploy } from './deploy.js';
@@ -0,0 +1,49 @@
1
+ import { readdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { join, extname } from 'node:path';
3
+
4
+ import logger from '../lib/logger.js';
5
+
6
+ interface Params {
7
+ deployDir: string;
8
+ }
9
+ export default async function({ deployDir }: Params) {
10
+ logger.info('Updating /shared/ to ./shared/');
11
+
12
+ const dirs = await readdir(deployDir);
13
+ for (const dir of dirs) {
14
+ if (dir === 'shared') {
15
+ await handleShared(deployDir, dir);
16
+ } else {
17
+ await handleHTML(deployDir, dir)
18
+ }
19
+ }
20
+ }
21
+
22
+ async function handleShared(deployDir: string, dir: string) {
23
+ const files = (await readdir(join(deployDir, dir)))
24
+ .filter((f) => {
25
+ switch (extname(f)) {
26
+ case '.css':
27
+ case '.js':
28
+ return true;
29
+ default:
30
+ return false;
31
+ }
32
+ });
33
+ for (const file of files) {
34
+ await updateSharedPath(join(deployDir, dir, file));
35
+ }
36
+ }
37
+
38
+ async function handleHTML(deployDir: string, dir: string) {
39
+ await updateSharedPath(join(deployDir, dir, 'index.html'));
40
+ }
41
+
42
+ async function updateSharedPath(filepath: string) {
43
+ const str = await readFile(filepath, 'utf8');
44
+ let shared = './shared/';
45
+ if (extname(filepath) === '.css') { // relative paths inside a css
46
+ shared = './';
47
+ }
48
+ await writeFile(filepath, str.replace(/(?:\.*\/)*shared\//g, shared));
49
+ }