Skip to content

Commit

Permalink
feat: components presets
Browse files Browse the repository at this point in the history
  • Loading branch information
alvarosabu committed Dec 12, 2024
1 parent 8b11e89 commit 775968d
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 26 deletions.
30 changes: 30 additions & 0 deletions src/commands/block/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import chalk from 'chalk'
import { colorPalette, commands } from '../../constants'
import { session } from '../../session'
import { getProgram } from '../../program'
import { CommandError, handleError, konsola } from '../../utils'

import type { PullComponentsOptions } from './constants'

const program = getProgram() // Get the shared singleton instance

export const blocksCommand = program
.command('block')
.option('-s, --space <space>', 'space ID')
.option('-p, --path <path>', 'path to where schemas are saved. Default is .storyblok/components')
.description(`Operations on Storyblok blocks`)

blocksCommand
.command('pull')
.description(`Download your space's blocks schema as json`)
.option('--sf, --separate-files [value]', 'Argument to create a single file for each component')
.action(async (options: PullComponentsOptions) => {
console.log('pulling blocks...', blocksCommand.opts())
})

blocksCommand
.command('push')
.description(`Push your space's blocks schema as json`)
.action(async (options: PullComponentsOptions) => {
console.log('pushing blocks...', options)
})
6 changes: 3 additions & 3 deletions src/commands/components/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { vol } from 'memfs'
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import { pullComponents, saveComponentsToFiles } from './actions'
import { fetchComponents, saveComponentsToFiles } from './actions'

const handlers = [
http.get('https://api.storyblok.com/v1/spaces/12345/components', async ({ request }) => {
Expand Down Expand Up @@ -70,12 +70,12 @@ describe('pull components actions', () => {
interntal_tags_ids: [1],
}]

const result = await pullComponents('12345', 'valid-token', 'eu')
const result = await fetchComponents('12345', 'valid-token', 'eu')
expect(result).toEqual(mockResponse)
})

it('should throw an masked error for invalid token', async () => {
await expect(pullComponents('12345', 'invalid-token', 'eu')).rejects.toThrow(
await expect(fetchComponents('12345', 'invalid-token', 'eu')).rejects.toThrow(
new Error(`The user is not authorized to access the API`),
)
})
Expand Down
137 changes: 119 additions & 18 deletions src/commands/components/actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ofetch } from 'ofetch'
import { handleAPIError, handleFileSystemError } from '../../utils'
import { handleAPIError, handleFileSystemError, slugify } from '../../utils'
import { regionsDomain } from '../../constants'
import { join } from 'node:path'
import { resolvePath, saveToFile } from '../../utils/filesystem'
Expand Down Expand Up @@ -27,14 +27,49 @@ export interface SpaceComponent {
content_type_asset_preview?: string
}

export interface SpaceComponentGroup {
name: string
id: number
uuid: string
parent_id: number
parent_uuid: string
}

export interface ComponentsSaveOptions {
path?: string
filename?: string
separateFiles?: boolean
suffix?: string
}

export const pullComponents = async (space: string, token: string, region: string): Promise<SpaceComponent[] | undefined> => {
export interface SpaceComponentPreset {
id: number
name: string
preset: Record<string, unknown>
component_id: number
space_id: number
created_at: string
updated_at: string
image: string
color: string
icon: string
description: string
}

/**
* Resolves the nested folder structure based on component group hierarchy.
* @param groupUuid - The UUID of the component group.
* @param groups - The list of all component groups.
* @returns The resolved path for the component group.
*/
const resolveGroupPath = (groupUuid: string, groups: SpaceComponentGroup[]): string => {
const group = groups.find(g => g.uuid === groupUuid)
if (!group) { return '' }
const parentPath = group.parent_uuid ? resolveGroupPath(group.parent_uuid, groups) : ''
return join(parentPath, slugify(group.name))
}

export const fetchComponents = async (space: string, token: string, region: string): Promise<SpaceComponent[] | undefined> => {
try {
const response = await ofetch(`https://${regionsDomain[region]}/v1/spaces/${space}/components`, {
headers: {
Expand All @@ -48,32 +83,98 @@ export const pullComponents = async (space: string, token: string, region: strin
}
}

export const saveComponentsToFiles = async (space: string, components: SpaceComponent[], options: PullComponentsOptions) => {
const { filename = 'components', suffix = space, path } = options
export const fetchComponentGroups = async (space: string, token: string, region: string): Promise<SpaceComponentGroup[] | undefined> => {
try {
const response = await ofetch(`https://${regionsDomain[region]}/v1/spaces/${space}/component_groups`, {
headers: {
Authorization: token,
},
})
return response.component_groups
}
catch (error) {
handleAPIError('pull_component_groups', error as Error)
}
}

export const fetchComponentPresets = async (space: string, token: string, region: string): Promise<SpaceComponentPreset[] | undefined> => {
try {
const data = JSON.stringify(components, null, 2)
const resolvedPath = resolvePath(path, 'components')
const response = await ofetch(`https://${regionsDomain[region]}/v1/spaces/${space}/presets`, {
headers: {
Authorization: token,
},
})
return response.presets
}
catch (error) {
handleAPIError('pull_component_presets', error as Error)
}
}

if (options.separateFiles) {
export const saveComponentsToFiles = async (
space: string,
components: SpaceComponent[],
groups: SpaceComponentGroup[],
presets: SpaceComponentPreset[],
options: PullComponentsOptions,
) => {
const { filename = 'components', suffix = space, path, separateFiles } = options
const resolvedPath = resolvePath(path, 'components')

try {
if (separateFiles) {
// Save in separate files with nested structure
for (const component of components) {
try {
const filePath = join(resolvedPath, `${component.name}.${suffix}.json`)
await saveToFile(filePath, JSON.stringify(component, null, 2))
}
catch (error) {
handleFileSystemError('write', error as Error)
const groupPath = component.component_group_uuid
? resolveGroupPath(component.component_group_uuid, groups)
: ''

const componentPath = join(resolvedPath, groupPath)

// Save component definition
const componentFilePath = join(componentPath, `${component.name}.${suffix}.json`)
await saveToFile(componentFilePath, JSON.stringify(component, null, 2))

// Find and save associated presets
const componentPresets = presets.filter(preset => preset.component_id === component.id)
if (componentPresets.length > 0) {
const presetsFilePath = join(componentPath, `${component.name}.presets.${suffix}.json`)
await saveToFile(presetsFilePath, JSON.stringify(componentPresets, null, 2))
}
}
return
}

// Default to saving all components to a single file
const name = `${filename}.${suffix}.json`
const filePath = join(resolvedPath, name)
// Default to saving consolidated files
const componentsFilePath = join(resolvedPath, `${filename}.${suffix}.json`)
await saveToFile(componentsFilePath, JSON.stringify(components, null, 2))

// Check if the path exists, and create it if it doesn't
await saveToFile(filePath, data)
if (groups.length > 0) {
const groupsFilePath = join(resolvedPath, `groups.${suffix}.json`)
await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2))
}

if (presets.length > 0) {
const presetsFilePath = join(resolvedPath, `presets.${suffix}.json`)
await saveToFile(presetsFilePath, JSON.stringify(presets, null, 2))
}
}
catch (error) {
handleFileSystemError('write', error as Error)
}
}

export const saveComponentPresetsToFiles = async (
space: string,
presets: SpaceComponentPreset[],
options: PullComponentsOptions,
) => {
const { filename = 'presets', suffix = space, path } = options

try {
const resolvedPath = resolvePath(path, 'components')
const filePath = join(resolvedPath, `${filename}.${suffix}.json`)
await saveToFile(filePath, JSON.stringify(presets, null, 2))
}
catch (error) {
handleFileSystemError('write', error as Error)
Expand Down
28 changes: 23 additions & 5 deletions src/commands/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { colorPalette, commands } from '../../constants'
import { session } from '../../session'
import { getProgram } from '../../program'
import { CommandError, handleError, konsola } from '../../utils'
import { pullComponents, saveComponentsToFiles } from './actions'
import { fetchComponentGroups, fetchComponentPresets, fetchComponents, saveComponentPresetsToFiles, saveComponentsToFiles } from './actions'
import type { PullComponentsOptions } from './constants'

const program = getProgram() // Get the shared singleton instance
Expand Down Expand Up @@ -41,23 +41,41 @@ componentsCommand
}

try {
const components = await pullComponents(space, state.password, state.region)
// Fetch all data first
const groups = await fetchComponentGroups(space, state.password, state.region)
const components = await fetchComponents(space, state.password, state.region)
const presets = await fetchComponentPresets(space, state.password, state.region)

if (!components || components.length === 0) {
konsola.warn(`No components found in the space ${space}`)
return
}
await saveComponentsToFiles(space, components, { ...options, path })
const msgFilename = `${filename}.${suffix}.json`

// Save everything using the new structure
await saveComponentsToFiles(space, components, groups || [], presets || [], { ...options, path })

if (separateFiles) {
if (filename !== 'components') {
konsola.warn(`The --filename option is ignored when using --separate-files`)
}
konsola.ok(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(path ? `${path}` : './storyblok/components')}`)
}
else {
const msgFilename = `${filename}.${suffix}.json`
konsola.ok(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(path ? `${path}/${msgFilename}` : `./storyblok/components/${msgFilename}`)}`)
}

// Fetch component groups
/* const componentGroups = await fetchComponentGroups(space, state.password, state.region)
if (!componentGroups || componentGroups.length === 0) {
konsola.warn(`No component groups found in the space ${space}`)
return
}
konsola.ok(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(path ? `${path}/${msgFilename}` : `./storyblok/components/${msgFilename}`)}`)
await saveComponentsToFiles(space, componentGroups, { ...options, path, filename: 'groups' })
konsola.ok(`Component groups downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(path ? `${path}/groups.${suffix}.json` : `./storyblok/components/groups.${suffix}.json`)}`) */
}
catch (error) {
handleError(error as Error, verbose)
Expand Down
2 changes: 2 additions & 0 deletions src/utils/error/api-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const API_ACTIONS = {
get_user: 'Failed to get user',
pull_languages: 'Failed to pull languages',
pull_components: 'Failed to pull components',
pull_component_groups: 'Failed to pull component groups',
pull_component_presets: 'Failed to pull component presets',
} as const

export const API_ERRORS = {
Expand Down
10 changes: 10 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@ export function maskToken(token: string): string {
const maskedPart = '*'.repeat(token.length - 4)
return `${visiblePart}${maskedPart}`
}

export const slugify = (text: string): string =>
text
.toString()
.toLowerCase()
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/[^\w-]+/g, '') // Remove all non-word chars
.replace(/-{2,}/g, '-') // Replace multiple - with single -
.replace(/^-+/, '') // Trim - from start of text
.replace(/-+$/, '')

0 comments on commit 775968d

Please sign in to comment.