import type { CollectionModel } from './collections.js' import { ComponentModel } from './components.js' import type { LibraryModel } from './libraries.js' import { SDKModel } from '../core/model.js' import SDKAdapter from '../core/adapter.js' import { nanoid } from '../core/transport.js' import { Atleast, Optional, RequiredKeys, SDK } from '../sdk.js' import { Version, VersionModel } from './versions.js' import * as Style from '../style/index.js' import { SDKModelSchema } from '../index.js' import versionBundleSchema from '../schemas/version_bundle.json' assert { type: 'json' } export interface VersionBundle { id: string libraryId: LibraryModel['id'] componentId: ComponentModel['id'] name: string description?: string breakpoints: { [key: string]: string } orderIdx?: string createdAt: Date modifiedAt: Date } export function getVersionBundleDefaults(sdk: SDK) { const now = new Date() return { id: nanoid(10), createdAt: now, modifiedAt: now, breakpoints: {} } } export type VersionBundleMinimal = RequiredKeys< VersionBundle, typeof getVersionBundleDefaults, T > export interface VersionBundleImplicit { /** Does version represent bundle? */ library?: LibraryModel libraryId: LibraryModel['id'] collectionId?: CollectionModel['id'] collection?: CollectionModel componentId: ComponentModel['id'] component?: ComponentModel } export interface VersionBundleParams extends VersionBundle, VersionBundleImplicit {} export interface VersionBundleModel extends VersionBundleParams {} export class VersionBundles extends SDKAdapter { fetch(version: Pick): Promise { const { libraryId, collectionId, componentId } = version return this.sdk.fetchJSON( `/libraries/${libraryId}/collections/${collectionId || 'unspecified'}/components/${componentId}/bundles?` + Math.random() ) } get(version: Pick): Promise { const { id, libraryId, collectionId, componentId } = version return this.sdk.fetchJSON( `/libraries/${libraryId}/collections/${collectionId || 'unspecified'}/components/${componentId}/bundles/${id}` ) } post( version: Optional & Atleast ) { const { libraryId, collectionId, componentId } = version return this.sdk.fetchJSON( `/libraries/${libraryId}/collections/${collectionId || 'unspecified'}/components/${componentId}/bundles`, { method: 'POST', body: JSON.stringify(version) } ) } put( version: Optional & Atleast ) { return this.post(version) } delete(version: Atleast) { const { libraryId, collectionId, componentId, id } = version return this.sdk.fetchJSON( `/libraries/${libraryId}/collections/${collectionId || 'unspecified'}/components/${componentId}/bundles/${id}`, { method: 'DELETE', headers: { 'Content-Type': 'text/plain' } } ) } } export class VersionBundleModel extends SDKModel implements VersionBundleParams { get adapter() { return this.sdk.VersionBundles } getDefaults(sdk: SDK) { return getVersionBundleDefaults(sdk) } getHiddenProps() { return ['component', 'library', 'collection'] } /* Create synthetic version of a component that has all the others combined */ aggregate( status: VersionModel['status'], versions = this.component.getBundlableVersions(this.component.getVersions(status)) ) { const data = ComponentModel.getAggregatedVersionData(versions) const map = this.getVersionMap(versions) const view = this.getHTML(versions, map) const version = this.component.versions.new({ id: this.id, name: this.name, status: status, isEmbeddable: true, modifiedAt: data.modifiedAt, modifiedBy: data.modifiedBy, datasourceIds: data.datasourceIds, createdAt: this.createdAt, bundle: this, bundleId: this.id, view: view }) version.getHTML = () => view return version } getHTML(versions: VersionModel[], map = this.getVersionMap(versions)) { return Object.keys(map) .reduce((bits, id) => { return bits.concat( versions.find((v) => v.id == id)?.getHTML(map[id].map((b) => `-breakpoint--${b.details.slug}`)) ) }, []) .join('\n\n') } getVersionMap(versions: VersionModel[]) { if (!this.library.stylesheet) throw new Error('Error: you are trying to access breakpoints, but you have not loaded any stylesheets') const map: Record[]> = {} const breakpoints = Style.Set.filterByType(this.library.stylesheet.rules, 'breakpoint') for (const breakpoint of breakpoints) { const matchedVersion = this.getVersionForBreakpoint(breakpoint, undefined, versions) if (matchedVersion) (map[matchedVersion.id] ||= []).push(breakpoint) } return map } getAvailableVersionsForBreakpoint( breakpoint: Style.Rule<'breakpoint'>, versions: VersionModel[] = this.component.versions ) { return versions.filter((v) => v .getBreakpoints() .find((b) => b.props.minWidth >= breakpoint.props.minWidth && b.props.maxWidth <= breakpoint.props.maxWidth) ) } getVersionForBreakpoint( breakpoint: Style.Rule<'breakpoint'>, status: Version['status'][] | Version['status'] = ['staged', 'published'], explicitVersions?: VersionModel[] ) { const bundleableVersions = explicitVersions || this.component.getBundlableVersions(this.component.getVersions([].concat(status))) const versions = VersionModel.getOrderedVersions(bundleableVersions) const version = this.component.getVersionForWidth(breakpoint.props.maxWidth, undefined, versions) return this.breakpoints?.[breakpoint.details.id] ? versions.find((v) => v.id === this.breakpoints[breakpoint.details.id]) || version : version } getUsedVersions() { const rules = this.library.stylesheet.rules const breakpoints = Style.Set.filterByType(rules, 'breakpoint') return [...new Set(breakpoints.map((breakpoint) => this.getVersionForBreakpoint(breakpoint)))] } static get schema(): SDKModelSchema { return new SDKModelSchema(VersionModel, versionBundleSchema) } getWarnings() { const rules = this.library.stylesheet.rules const breakpoints = Style.Set.filterByType(rules, 'breakpoint') const bundleStagedVersions = this.getVersionMap(this.component.getStagedVersions()) const bundlePublishedVersions = this.getVersionMap(this.component.getPublishedVersions()) const notPublishedVersionIds = Object.keys(bundleStagedVersions).filter( (id) => !Object.keys(bundlePublishedVersions).includes(id) ) const unstagedVersionIds = Object.keys(bundlePublishedVersions).filter( (id) => !Object.keys(bundleStagedVersions).includes(id) ) const breakpointWarnings = breakpoints.map((breakpoint) => { const version = this.getVersionForBreakpoint(breakpoint) const publishedVersion = this.getVersionForBreakpoint(breakpoint, 'published') const stagedVersion = this.getVersionForBreakpoint(breakpoint, 'staged') const warning = version ? notPublishedVersionIds.includes(version.id) ? { versionId: version.id, label: 'Selected version is not published', breakpoints, description: `Version is available in Pages. Until publishing, for all public-facing pages of the website, ${ publishedVersion ? 'version ' + publishedVersion.name + ' will be used instead.' : ' there will be no available version for this breakpoint' }` } : unstagedVersionIds.includes(version.id) ? { versionId: version.id, label: 'Selected version is unstaged', breakpoints, description: `Version is still visible on all public-facing pages of the website. When unpublishing, ${ stagedVersion ? 'version ' + stagedVersion.name + ' will be used instead.' : ' there will be no available version for this breakpoint' } ` } : !version.getBreakpoints().find((b) => b.details.id === breakpoint.details.id) ? { versionId: version.id, label: 'Best effort choice', breakpoints, description: `There are no compatible versions for the breakpoint, so the closest one was used instead.` } : null : null return { breakpoint, warning } }) return breakpointWarnings } }