@cooperco/cooper-component-library 0.1.71 → 0.1.72

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.
@@ -0,0 +1,231 @@
1
+ module.exports = {
2
+ // @ts-check
3
+ /** @type { import('contentful-migration').MigrationFunction } */
4
+ up: function (migration) {
5
+ const article = migration.createContentType('blog-article', {
6
+ name: 'Blog Article',
7
+ description: 'Article content',
8
+ displayField: 'slug',
9
+ })
10
+ // Add slug field
11
+ article.createField('slug', {
12
+ name: 'Slug',
13
+ type: 'Symbol',
14
+ required: true,
15
+ })
16
+
17
+ // Add heading field
18
+ article.createField('heading', {
19
+ name: 'Heading',
20
+ type: 'Symbol',
21
+ required: true,
22
+ })
23
+
24
+ // Add image field
25
+ article.createField('image',{
26
+ name: 'Image',
27
+ type: 'Link',
28
+ linkType: 'Asset',
29
+ required: false
30
+ })
31
+
32
+ // Add summary field
33
+ article.createField('summary', {
34
+ name: 'Summary',
35
+ type: 'RichText',
36
+ required: false,
37
+ validations: [
38
+ {
39
+ enabledMarks: [
40
+ 'bold',
41
+ 'italic',
42
+ 'underline',
43
+ 'code',
44
+ 'superscript',
45
+ 'subscript',
46
+ 'strikethrough',
47
+ ],
48
+ },
49
+ {
50
+ enabledNodeTypes: [
51
+ 'heading-1',
52
+ 'heading-2',
53
+ 'heading-3',
54
+ 'heading-4',
55
+ 'heading-5',
56
+ 'heading-6',
57
+ 'ordered-list',
58
+ 'unordered-list',
59
+ 'blockquote',
60
+ 'hr',
61
+ 'embedded-entry-block',
62
+ 'embedded-asset-block',
63
+ 'embedded-entry-inline',
64
+ 'hyperlink',
65
+ 'entry-hyperlink',
66
+ 'asset-hyperlink',
67
+ ],
68
+ },
69
+ ],
70
+ })
71
+
72
+ // Add content field
73
+ article.createField('content', {
74
+ name: 'Content',
75
+ type: 'RichText',
76
+ required: false,
77
+ validations: [
78
+ {
79
+ enabledMarks: [
80
+ 'bold',
81
+ 'italic',
82
+ 'underline',
83
+ 'code',
84
+ 'superscript',
85
+ 'subscript',
86
+ 'strikethrough',
87
+ ],
88
+ },
89
+ {
90
+ enabledNodeTypes: [
91
+ 'heading-1',
92
+ 'heading-2',
93
+ 'heading-3',
94
+ 'heading-4',
95
+ 'heading-5',
96
+ 'heading-6',
97
+ 'ordered-list',
98
+ 'unordered-list',
99
+ 'blockquote',
100
+ 'hr',
101
+ 'embedded-entry-block',
102
+ 'embedded-asset-block',
103
+ 'embedded-entry-inline',
104
+ 'hyperlink',
105
+ 'entry-hyperlink',
106
+ 'asset-hyperlink',
107
+ ],
108
+ },
109
+ ],
110
+ })
111
+
112
+ // Add footnote field
113
+ article.createField('footnote', {
114
+ name: 'Footnote',
115
+ type: 'RichText',
116
+ required: false,
117
+ validations: [
118
+ {
119
+ enabledMarks: [
120
+ 'bold',
121
+ 'italic',
122
+ 'underline',
123
+ 'code',
124
+ 'superscript',
125
+ 'subscript',
126
+ 'strikethrough',
127
+ ],
128
+ },
129
+ {
130
+ enabledNodeTypes: [
131
+ 'heading-1',
132
+ 'heading-2',
133
+ 'heading-3',
134
+ 'heading-4',
135
+ 'heading-5',
136
+ 'heading-6',
137
+ 'ordered-list',
138
+ 'unordered-list',
139
+ 'blockquote',
140
+ 'hr',
141
+ 'embedded-entry-block',
142
+ 'embedded-asset-block',
143
+ 'embedded-entry-inline',
144
+ 'hyperlink',
145
+ 'entry-hyperlink',
146
+ 'asset-hyperlink',
147
+ ],
148
+ },
149
+ ],
150
+ })
151
+
152
+ // Add heading field
153
+ article.createField('author', {
154
+ name: 'Author',
155
+ type: 'Symbol',
156
+ required: false,
157
+ })
158
+
159
+ //Add top field
160
+ article.createField('top', {
161
+ name: 'Top',
162
+ type: 'Boolean',
163
+ required: false,
164
+ })
165
+
166
+ // Add heading field
167
+ article.createField('instagram', {
168
+ name: 'Instagram',
169
+ type: 'Symbol',
170
+ required: false,
171
+ })
172
+
173
+ // Add categories field
174
+ article.createField('categories', {
175
+ name: 'Categories',
176
+ type: 'Array',
177
+ items: {
178
+ type: 'Symbol',
179
+ validations: [],
180
+ },
181
+ })
182
+
183
+ //Add top field
184
+ article.createField('active', {
185
+ name: 'Active',
186
+ type: 'Boolean',
187
+ required: false,
188
+ })
189
+
190
+ //Add date field
191
+ article.createField('date', {
192
+ name: 'Date',
193
+ type: 'Date',
194
+ required: false,
195
+ })
196
+ article.changeFieldControl('date', 'builtin', 'datePicker', {
197
+ format: 'dateAndTime',
198
+ ampm: '24',
199
+ })
200
+
201
+ //Add comments field
202
+ article.createField('comments', {
203
+ name: 'Comments',
204
+ type: 'Array',
205
+ items: {
206
+ type: 'Link',
207
+ linkType: 'Entry',
208
+ validations: [],
209
+ },
210
+ })
211
+
212
+ //Add article type field
213
+ article.createField('articleType', {
214
+ name: 'Article Type',
215
+ type: 'Symbol',
216
+ required: false,
217
+ validations: [
218
+ {
219
+ in: ['Default', 'HCP'],
220
+ },
221
+ ],
222
+ })
223
+ article.changeFieldControl('articleType', 'builtin', 'dropdown', {
224
+ helpText: 'Select the type of article',
225
+ })
226
+
227
+ },
228
+ down: function (migration) {
229
+ migration.deleteContentType('blog-article')
230
+ },
231
+ }
@@ -0,0 +1,275 @@
1
+ const { createClient } = require('contentful-management')
2
+ const readline = require('readline')
3
+ const dotenv = require('dotenv')
4
+ dotenv.config()
5
+
6
+ const args = process.argv.slice(2)
7
+ const getArg = (name) => {
8
+ const index = args.indexOf(`--${name}`)
9
+ return index !== -1 && args[index + 1] ? args[index + 1] : null
10
+ }
11
+ const hasFlag = (name) => args.includes(`--${name}`)
12
+
13
+ function confirm(message) {
14
+ const rl = readline.createInterface({
15
+ input: process.stdin,
16
+ output: process.stdout,
17
+ })
18
+
19
+ return new Promise((resolve) => {
20
+ rl.question(message, (answer) => {
21
+ rl.close()
22
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes')
23
+ })
24
+ })
25
+ }
26
+
27
+ const SPACE_ID = getArg('space-id') || process.env.CONTENTFUL_SPACE_ID
28
+ const ENVIRONMENT_ID = getArg('environment-id') || process.env.CONTENTFUL_ENVIRONMENT_ID || 'master'
29
+ const MANAGEMENT_TOKEN = getArg('management-token') || process.env.CONTENTFUL_MANAGEMENT_TOKEN
30
+
31
+ if (!SPACE_ID || !MANAGEMENT_TOKEN) {
32
+ process.stderr.write('Error: SPACE_ID and MANAGEMENT_TOKEN are required\n')
33
+ process.stderr.write('Use: node migrate-legacy-articles.cjs --space-id <ID> --management-token <TOKEN> [--environment-id <ENV>]\n')
34
+ process.exit(1)
35
+ }
36
+
37
+ const fieldMapping = {
38
+ slug: 'slug',
39
+ heading: 'heading',
40
+ image: 'image',
41
+ summary: 'summary',
42
+ content: 'content',
43
+ footnote: 'footnote',
44
+ author: 'author',
45
+ top: 'top',
46
+ instagram: 'instagram',
47
+ categories: 'categories',
48
+ active: 'active',
49
+ date: 'date',
50
+ comments: 'comments',
51
+ }
52
+
53
+ function mapLegacyEntryToArticle(legacyEntry, sourceContentType) {
54
+ const fields = legacyEntry.fields
55
+ const mappedFields = {}
56
+
57
+ Object.keys(fieldMapping).forEach(newField => {
58
+ const legacyField = fieldMapping[newField]
59
+
60
+ if (fields[legacyField]) {
61
+ mappedFields[newField] = fields[legacyField]
62
+ }
63
+ })
64
+
65
+ if (sourceContentType === 'hcpArticle') {
66
+ Object.keys(mappedFields.slug || {}).forEach(locale => {
67
+ if (!mappedFields.articleType) {
68
+ mappedFields.articleType = {}
69
+ }
70
+ mappedFields.articleType[locale] = 'HCP'
71
+ })
72
+ } else {
73
+ Object.keys(mappedFields.slug || {}).forEach(locale => {
74
+ if (!mappedFields.articleType) {
75
+ mappedFields.articleType = {}
76
+ }
77
+ mappedFields.articleType[locale] = 'Default'
78
+ })
79
+ }
80
+
81
+ return mappedFields
82
+ }
83
+
84
+ async function getAllEntries(environment, contentTypeId) {
85
+ const entries = []
86
+ let skip = 0
87
+ const limit = 1000
88
+
89
+ try {
90
+ let hasMore = true
91
+ while (hasMore) {
92
+ const response = await environment.getEntries({
93
+ content_type: contentTypeId,
94
+ skip,
95
+ limit,
96
+ })
97
+
98
+ entries.push(...response.items)
99
+
100
+ if (response.items.length < limit) {
101
+ hasMore = false
102
+ } else {
103
+ skip += limit
104
+ }
105
+ }
106
+ } catch (error) {
107
+ // If content type doesn't exist, return empty array
108
+ if (error.name === 'InvalidQuery' ||
109
+ (error.details && error.details.errors &&
110
+ error.details.errors.some(e => e.name === 'unknownContentType'))) {
111
+ return []
112
+ }
113
+ throw error
114
+ }
115
+
116
+ return entries
117
+ }
118
+
119
+ async function migrateContentType(environment, legacyContentTypeId) {
120
+ const legacyEntries = await getAllEntries(environment, legacyContentTypeId)
121
+
122
+ if (legacyEntries.length === 0) {
123
+ return { successCount: 0, errorCount: 0 }
124
+ }
125
+
126
+ let successCount = 0
127
+ let errorCount = 0
128
+
129
+ for (const legacyEntry of legacyEntries) {
130
+ const entryId = legacyEntry.sys.id
131
+ const slug = legacyEntry.fields.slug?.['en-US'] || entryId
132
+
133
+ try {
134
+ const mappedFields = mapLegacyEntryToArticle(legacyEntry, legacyContentTypeId)
135
+
136
+ if (!mappedFields.slug || Object.keys(mappedFields.slug).length === 0) {
137
+ process.stderr.write(` Skipped "${slug}" (${entryId}): missing required slug field\n`)
138
+ errorCount++
139
+ continue
140
+ }
141
+
142
+ const isPublished = legacyEntry.isPublished()
143
+ const isArchived = legacyEntry.isArchived()
144
+ const isUpdated = legacyEntry.isUpdated()
145
+ const isDraft = !isPublished && !isArchived
146
+
147
+ const newEntry = await environment.createEntry('blog-article', {
148
+ fields: mappedFields,
149
+ })
150
+
151
+ if (isPublished) {
152
+ await newEntry.publish()
153
+ }
154
+
155
+ if (isArchived) {
156
+ await newEntry.archive()
157
+ }
158
+
159
+ if (isPublished && isUpdated && !isArchived) {
160
+ const reloadedEntry = await environment.getEntry(newEntry.sys.id)
161
+
162
+ let fieldUpdated = false
163
+
164
+ if (reloadedEntry.fields.active) {
165
+ const firstLocale = Object.keys(reloadedEntry.fields.active)[0]
166
+ if (firstLocale !== undefined) {
167
+ const originalValue = reloadedEntry.fields.active[firstLocale]
168
+ reloadedEntry.fields.active[firstLocale] = !originalValue
169
+ await reloadedEntry.update()
170
+ const toggledEntry = await environment.getEntry(newEntry.sys.id)
171
+ toggledEntry.fields.active[firstLocale] = originalValue
172
+ await toggledEntry.update()
173
+ fieldUpdated = true
174
+ }
175
+ }
176
+
177
+ if (!fieldUpdated && reloadedEntry.fields.heading) {
178
+ const firstLocale = Object.keys(reloadedEntry.fields.heading)[0]
179
+ if (firstLocale) {
180
+ const currentHeading = reloadedEntry.fields.heading[firstLocale]
181
+ reloadedEntry.fields.heading[firstLocale] = currentHeading + ' '
182
+ await reloadedEntry.update()
183
+ const fixedEntry = await environment.getEntry(newEntry.sys.id)
184
+ fixedEntry.fields.heading[firstLocale] = currentHeading
185
+ await fixedEntry.update()
186
+ fieldUpdated = true
187
+ }
188
+ }
189
+
190
+ if (!fieldUpdated) {
191
+ await reloadedEntry.update()
192
+ }
193
+ }
194
+
195
+ const statusInfo = []
196
+ if (isDraft) statusInfo.push('draft')
197
+ if (isPublished && !isUpdated) statusInfo.push('published')
198
+ if (isPublished && isUpdated) statusInfo.push('changed')
199
+ if (isArchived) statusInfo.push('archived')
200
+ const statusText = statusInfo.length > 0 ? ` [${statusInfo.join(', ')}]` : ''
201
+
202
+ process.stdout.write(` Migrated: "${slug}" (${entryId})${statusText}\n`)
203
+ successCount++
204
+ } catch (error) {
205
+ const errorMessage = error.message || 'Unknown error'
206
+ const errorDetails = error.details?.errors?.[0]?.details || ''
207
+ process.stderr.write(` Failed "${slug}" (${entryId}): ${errorMessage} ${errorDetails}\n`)
208
+ errorCount++
209
+ }
210
+ }
211
+
212
+ return { successCount, errorCount }
213
+ }
214
+
215
+ async function main() {
216
+ const client = createClient({
217
+ accessToken: MANAGEMENT_TOKEN,
218
+ })
219
+
220
+ const space = await client.getSpace(SPACE_ID)
221
+ const environment = await space.getEnvironment(ENVIRONMENT_ID)
222
+
223
+ // Pre-check: show environment info and confirm
224
+ const articleCount = (await getAllEntries(environment, 'article')).length
225
+ const hcpArticleCount = (await getAllEntries(environment, 'hcpArticle')).length
226
+
227
+ process.stdout.write('\n=== Migration Preview ===\n')
228
+ process.stdout.write(`Space ID: ${SPACE_ID}\n`)
229
+ process.stdout.write(`Environment: ${ENVIRONMENT_ID}\n`)
230
+ process.stdout.write(`Space Name: ${space.name}\n`)
231
+ process.stdout.write(`\nEntries to migrate:\n`)
232
+ process.stdout.write(` - article: ${articleCount} entries\n`)
233
+ process.stdout.write(` - hcpArticle: ${hcpArticleCount} entries\n`)
234
+ process.stdout.write(` - Total: ${articleCount + hcpArticleCount} entries\n\n`)
235
+
236
+ if (!hasFlag('yes') && !hasFlag('y')) {
237
+ const confirmed = await confirm('Proceed with migration? (y/N): ')
238
+ if (!confirmed) {
239
+ process.stdout.write('Migration cancelled.\n')
240
+ process.exit(0)
241
+ }
242
+ }
243
+
244
+ try {
245
+ await environment.getContentType('blog-article')
246
+ } catch (error) {
247
+ process.stderr.write('Error: Content type "blog-article" does not exist.\n')
248
+ process.stderr.write(' Run migration 0061-create-new-article.cjs first.\n')
249
+ process.exit(1)
250
+ }
251
+
252
+ // Migrate entries from article
253
+ process.stdout.write('\nMigrating "article" entries...\n')
254
+ const articleResults = await migrateContentType(environment, 'article')
255
+ process.stdout.write(`Done: ${articleResults.successCount} success, ${articleResults.errorCount} errors\n`)
256
+
257
+ // Migrate entries from hcpArticle
258
+ process.stdout.write('\nMigrating "hcpArticle" entries...\n')
259
+ const hcpResults = await migrateContentType(environment, 'hcpArticle')
260
+ process.stdout.write(`Done: ${hcpResults.successCount} success, ${hcpResults.errorCount} errors\n`)
261
+
262
+ // Final summary
263
+ const totalSuccess = articleResults.successCount + hcpResults.successCount
264
+ const totalErrors = articleResults.errorCount + hcpResults.errorCount
265
+ process.stdout.write(`\nMigration complete: ${totalSuccess} migrated, ${totalErrors} failed\n`)
266
+ }
267
+
268
+ main().catch(error => {
269
+ process.stderr.write(`\n❌ Fatal error: ${error.message}\n`)
270
+ if (error.stack) {
271
+ process.stderr.write(`${error.stack}\n`)
272
+ }
273
+ process.exit(1)
274
+ })
275
+
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cooperco/cooper-component-library",
3
3
  "private": false,
4
- "version": "0.1.71",
4
+ "version": "0.1.72",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist"
@@ -32,7 +32,9 @@
32
32
  "@primevue/themes": "^4.0.0",
33
33
  "@vueuse/components": "^11.1.0",
34
34
  "@vueuse/core": "^11.1.0",
35
+ "contentful-management": "^11.68.0",
35
36
  "contentful-migration": "^4.23.2",
37
+ "dotenv": "^17.2.3",
36
38
  "graphql": "^16.9.0",
37
39
  "graphql-tag": "^2.12.6",
38
40
  "primeicons": "^7.0.0",