refactor(export): derive export format lists from the IO registry (#755)
The Export panel, SDK helpers, app menus, and CLI each kept their own hand-written format lists, so new formats such as PPTX reached some surfaces and not others. Scene Graph now owns the persisted export-setting format ids, Core IO adapters carry literal ids so that list is checked against real adapters, and the panel labels, scale handling, app format types, and CLI format validation/help are derived from the registry. PPTX joins the Export panel as a result.
This commit is contained in:
parent
99925e4c25
commit
2d9e83d58a
|
|
@ -6,6 +6,10 @@
|
|||
|
||||
- The desktop app now requires macOS 13 or later; the web app supports Chrome 111, Edge 111, Firefox 128, and Safari 16.4 or later.
|
||||
|
||||
### Added
|
||||
|
||||
- Choose PPTX in the Export panel's format list, alongside PNG, JPG, WEBP, SVG, and PDF.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Show what to update instead of a blank window when the browser or system WebView is too old, naming the detected macOS, Safari, Chrome, Edge, Firefox, WebKitGTK, or WebView2 version and linking a prefilled bug report, and explain a failed start the same way (#744).
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { defineCommand } from 'citty'
|
|||
|
||||
import { decodeBase64 } from '@open-pencil/core/bytes'
|
||||
import { BUILTIN_IO_FORMATS, IORegistry } from '@open-pencil/core/io'
|
||||
import type { RasterExportFormat } from '@open-pencil/core/io'
|
||||
import {
|
||||
exportHTMLBundle,
|
||||
sceneGraphToDesignDocument,
|
||||
|
|
@ -19,8 +18,14 @@ import { ok, printError } from '#cli/format'
|
|||
import { loadDocument, populateDocumentPage, populateWholeDocument } from '#cli/headless'
|
||||
|
||||
const io = new IORegistry(BUILTIN_IO_FORMATS)
|
||||
const RASTER_FORMATS = ['PNG', 'JPG', 'WEBP']
|
||||
const ALL_FORMATS = new Set([...RASTER_FORMATS, 'SVG', 'PDF', 'PPTX', 'JSX', 'FIG', 'HTML'])
|
||||
// HTML goes through the DOM/CSS bundle exporter; every other format is a Core IO adapter.
|
||||
const FORMAT_IDS = [...io.listExportFormats('node').map((format) => format.id), 'html']
|
||||
const ALL_FORMATS = new Set(FORMAT_IDS.map((id) => id.toUpperCase()))
|
||||
const FORMAT_LIST = `${FORMAT_IDS.slice(0, -1).join(', ')}, or ${FORMAT_IDS.at(-1)}`
|
||||
|
||||
function formatSupportsScale(format: string): boolean {
|
||||
return io.getFormat(format.toLowerCase())?.exportOptions?.scale ?? false
|
||||
}
|
||||
const JSX_STYLES = new Set(['openpencil', 'tailwind'])
|
||||
const HTML_STYLES = new Set(['inline', 'tailwind'])
|
||||
const HTML_MODES = new Set(['fragment', 'standalone'])
|
||||
|
|
@ -242,7 +247,7 @@ async function exportFromFile(format: string, args: ExportArgs) {
|
|||
options = { format: args.style }
|
||||
} else if (format === 'FIG') {
|
||||
options = { renderThumbnail: true }
|
||||
} else if (format === 'PNG' || format === 'JPG' || format === 'WEBP') {
|
||||
} else if (formatSupportsScale(format)) {
|
||||
options = {
|
||||
format,
|
||||
scale: Number(args.scale),
|
||||
|
|
@ -256,7 +261,7 @@ async function exportFromFile(format: string, args: ExportArgs) {
|
|||
exportFileName(
|
||||
defaultName,
|
||||
result.extension,
|
||||
format === 'PNG' || format === 'JPG' || format === 'WEBP' ? Number(args.scale) : undefined
|
||||
formatSupportsScale(format) ? Number(args.scale) : undefined
|
||||
)
|
||||
)
|
||||
await writeAndLog(output, result.data as string | Uint8Array)
|
||||
|
|
@ -264,7 +269,7 @@ async function exportFromFile(format: string, args: ExportArgs) {
|
|||
}
|
||||
|
||||
export default defineCommand({
|
||||
meta: { description: 'Export a document to PNG, JPG, WEBP, SVG, PDF, PPTX, JSX, HTML, or .fig' },
|
||||
meta: { description: `Export a document to ${FORMAT_LIST}` },
|
||||
args: {
|
||||
file: {
|
||||
type: 'positional',
|
||||
|
|
@ -280,7 +285,7 @@ export default defineCommand({
|
|||
format: {
|
||||
type: 'string',
|
||||
alias: 'f',
|
||||
description: 'Export format: png, jpg, webp, svg, pdf, pptx, jsx, html, fig (default: png)',
|
||||
description: `Export format: ${FORMAT_IDS.join(', ')} (default: png)`,
|
||||
default: 'png'
|
||||
},
|
||||
scale: { type: 'string', alias: 's', description: 'Export scale (default: 1)', default: '1' },
|
||||
|
|
@ -336,11 +341,9 @@ export default defineCommand({
|
|||
...appTargetOptions
|
||||
},
|
||||
async run({ args }) {
|
||||
const format = args.format.toUpperCase() as RasterExportFormat | 'SVG' | 'JSX' | 'FIG' | 'HTML'
|
||||
const format = args.format.toUpperCase()
|
||||
if (!ALL_FORMATS.has(format)) {
|
||||
printError(
|
||||
`Invalid format "${args.format}". Use png, jpg, webp, svg, pdf, pptx, jsx, html, or fig.`
|
||||
)
|
||||
printError(`Invalid format "${args.format}". Use ${FORMAT_LIST}.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ async function renderRaster(
|
|||
})
|
||||
}
|
||||
|
||||
function rasterFormat(format: RasterExportFormat): IOFormatAdapter {
|
||||
const extension = format === 'JPG' ? 'jpg' : format.toLowerCase()
|
||||
function rasterFormat<F extends RasterExportFormat>(format: F): IOFormatAdapter<Lowercase<F>> {
|
||||
const extension = format.toLowerCase() as Lowercase<F>
|
||||
let mimeType = 'image/png'
|
||||
if (format === 'JPG') mimeType = 'image/jpeg'
|
||||
else if (format === 'WEBP') mimeType = 'image/webp'
|
||||
|
|
@ -138,7 +138,7 @@ function rasterFormat(format: RasterExportFormat): IOFormatAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
export const figFormat: IOFormatAdapter = {
|
||||
export const figFormat: IOFormatAdapter<'fig'> = {
|
||||
id: 'fig',
|
||||
label: 'OpenPencil Document',
|
||||
role: 'native-document',
|
||||
|
|
@ -198,7 +198,7 @@ export const figFormat: IOFormatAdapter = {
|
|||
}
|
||||
}
|
||||
|
||||
export const penFormat: IOFormatAdapter = {
|
||||
export const penFormat: IOFormatAdapter<'pen'> = {
|
||||
id: 'pen',
|
||||
label: 'Pencil Document',
|
||||
role: 'interchange-document',
|
||||
|
|
@ -222,7 +222,7 @@ export const pngFormat = rasterFormat('PNG')
|
|||
export const jpgFormat = rasterFormat('JPG')
|
||||
export const webpFormat = rasterFormat('WEBP')
|
||||
|
||||
export const svgFormat: IOFormatAdapter = {
|
||||
export const svgFormat: IOFormatAdapter<'svg'> = {
|
||||
id: 'svg',
|
||||
label: 'SVG',
|
||||
role: 'derived-export',
|
||||
|
|
@ -255,7 +255,7 @@ export const svgFormat: IOFormatAdapter = {
|
|||
}
|
||||
}
|
||||
|
||||
export const pdfFormat: IOFormatAdapter = {
|
||||
export const pdfFormat: IOFormatAdapter<'pdf'> = {
|
||||
id: 'pdf',
|
||||
label: 'PDF',
|
||||
role: 'derived-export',
|
||||
|
|
@ -300,7 +300,7 @@ function resolvePPTXExportNodes(
|
|||
return { pageId: pages[0].id, nodeIds: pages.flatMap((page) => page.childIds) }
|
||||
}
|
||||
|
||||
export const pptxFormat: IOFormatAdapter = {
|
||||
export const pptxFormat: IOFormatAdapter<'pptx'> = {
|
||||
id: 'pptx',
|
||||
label: 'PowerPoint',
|
||||
role: 'derived-export',
|
||||
|
|
@ -335,7 +335,7 @@ export const pptxFormat: IOFormatAdapter = {
|
|||
}
|
||||
}
|
||||
|
||||
export const jsxFormat: IOFormatAdapter = {
|
||||
export const jsxFormat: IOFormatAdapter<'jsx'> = {
|
||||
id: 'jsx',
|
||||
label: 'JSX',
|
||||
role: 'derived-export',
|
||||
|
|
@ -370,7 +370,7 @@ export const jsxFormat: IOFormatAdapter = {
|
|||
}
|
||||
}
|
||||
|
||||
export const BUILTIN_IO_FORMATS: IOFormatAdapter[] = [
|
||||
export const BUILTIN_IO_FORMATS = [
|
||||
figFormat,
|
||||
penFormat,
|
||||
pngFormat,
|
||||
|
|
@ -380,4 +380,6 @@ export const BUILTIN_IO_FORMATS: IOFormatAdapter[] = [
|
|||
pdfFormat,
|
||||
pptxFormat,
|
||||
jsxFormat
|
||||
]
|
||||
] as const satisfies readonly IOFormatAdapter[]
|
||||
|
||||
export type BuiltinIOFormatId = (typeof BUILTIN_IO_FORMATS)[number]['id']
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export { IORegistry } from './registry'
|
|||
export { extractExportGraph } from './subgraph'
|
||||
export {
|
||||
BUILTIN_IO_FORMATS,
|
||||
type BuiltinIOFormatId,
|
||||
figFormat,
|
||||
penFormat,
|
||||
pngFormat,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import type { SceneGraph } from '@open-pencil/scene-graph'
|
|||
import type { ExportRequest, IOContext, IOFormatAdapter, ReadDocumentInput } from './types'
|
||||
|
||||
export class IORegistry {
|
||||
constructor(private readonly adapters: IOFormatAdapter[]) {}
|
||||
constructor(private readonly adapters: readonly IOFormatAdapter[]) {}
|
||||
|
||||
listFormats(): IOFormatAdapter[] {
|
||||
listFormats(): readonly IOFormatAdapter[] {
|
||||
return this.adapters
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -109,8 +109,8 @@ export interface IOFormatExportOptions {
|
|||
colorSpace?: boolean
|
||||
}
|
||||
|
||||
export interface IOFormatAdapter {
|
||||
id: string
|
||||
export interface IOFormatAdapter<Id extends string = string> {
|
||||
id: Id
|
||||
label: string
|
||||
role: IOFormatRole
|
||||
category: IOFormatCategory
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ exportState.addSetting()
|
|||
|
||||
```ts
|
||||
exportState.updateScale(0, 2)
|
||||
exportState.updateFormat(0, 'WEBP')
|
||||
exportState.updateFormat(0, 'webp')
|
||||
```
|
||||
|
||||
## Siehe auch
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ exportState.addSetting()
|
|||
|
||||
```ts
|
||||
exportState.updateScale(0, 2)
|
||||
exportState.updateFormat(0, 'WEBP')
|
||||
exportState.updateFormat(0, 'webp')
|
||||
```
|
||||
|
||||
## Zobacz też
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ It manages:
|
|||
- export settings rows
|
||||
- selected node ids
|
||||
- export name labeling
|
||||
- supported scales and formats
|
||||
- supported scales and formats (`formats` ids and labeled `formatOptions`)
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
@ -30,6 +30,7 @@ const {
|
|||
nodeName,
|
||||
scales,
|
||||
formats,
|
||||
formatOptions,
|
||||
addSetting,
|
||||
updateScale,
|
||||
updateFormat,
|
||||
|
|
@ -48,7 +49,7 @@ exportState.addSetting()
|
|||
|
||||
```ts
|
||||
exportState.updateScale(0, 2)
|
||||
exportState.updateFormat(0, 'WEBP')
|
||||
exportState.updateFormat(0, 'webp')
|
||||
```
|
||||
|
||||
## Related APIs
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ exportState.addSetting()
|
|||
|
||||
```ts
|
||||
exportState.updateScale(0, 2)
|
||||
exportState.updateFormat(0, 'WEBP')
|
||||
exportState.updateFormat(0, 'webp')
|
||||
```
|
||||
|
||||
## Связанные API
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { NodeChange, PluginData, PluginRelaunchData } from '@open-pencil/ki
|
|||
import { guidToString } from '@open-pencil/kiwi/fig/guid'
|
||||
import {
|
||||
clampExportScale,
|
||||
isExportFormatId,
|
||||
type ExportFormatId,
|
||||
type ExportSetting,
|
||||
type PluginDataEntry,
|
||||
|
|
@ -135,12 +136,6 @@ export function extractBoundVariables(nc: NodeChange): Record<string, string> {
|
|||
return bindings
|
||||
}
|
||||
|
||||
function isExportFormatId(value: unknown): value is ExportFormatId {
|
||||
return (
|
||||
value === 'png' || value === 'jpg' || value === 'webp' || value === 'svg' || value === 'pdf'
|
||||
)
|
||||
}
|
||||
|
||||
function parseExportSettingsPluginValue(value: string | null): ExportSetting[] | null {
|
||||
if (!value) return null
|
||||
try {
|
||||
|
|
|
|||
12
packages/scene-graph/src/export-format.ts
Normal file
12
packages/scene-graph/src/export-format.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Formats a node's export settings may persist. Each id must name a Core IO
|
||||
* export adapter that supports node export; labels and options come from that
|
||||
* adapter, so this list only decides what the Export panel offers.
|
||||
*/
|
||||
export const EXPORT_FORMAT_IDS = ['png', 'jpg', 'webp', 'svg', 'pdf', 'pptx'] as const
|
||||
|
||||
export type ExportFormatId = (typeof EXPORT_FORMAT_IDS)[number]
|
||||
|
||||
export function isExportFormatId(value: unknown): value is ExportFormatId {
|
||||
return EXPORT_FORMAT_IDS.some((id) => id === value)
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ export {
|
|||
type InstanceOverrideState
|
||||
} from './instance-overrides'
|
||||
export * from './snap'
|
||||
export * from './export-format'
|
||||
export * from './export-scale'
|
||||
export * from './coordinate'
|
||||
export * from './constants'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable max-lines -- scene node contracts are kept together as the public graph type surface */
|
||||
|
||||
import type { ExportFormatId } from './export-format'
|
||||
import type { CanvasGuide } from './guides'
|
||||
import type { InstanceOverrideState } from './instance-overrides'
|
||||
import type { Color, Matrix, Rect, Vector } from './primitives'
|
||||
|
|
@ -305,8 +306,6 @@ export interface PluginDataEntry {
|
|||
value: string
|
||||
}
|
||||
|
||||
export type ExportFormatId = 'png' | 'jpg' | 'webp' | 'svg' | 'pdf'
|
||||
|
||||
export interface ExportSetting {
|
||||
scale: number
|
||||
format: ExportFormatId
|
||||
|
|
|
|||
|
|
@ -2,14 +2,25 @@ import { computed } from 'vue'
|
|||
import type { ComputedRef } from 'vue'
|
||||
|
||||
import type { Editor } from '@open-pencil/core/editor'
|
||||
import { BUILTIN_IO_FORMATS, IORegistry } from '@open-pencil/core/io'
|
||||
import { MAX_EXPORT_SCALE, MIN_EXPORT_SCALE, clampExportScale } from '@open-pencil/scene-graph'
|
||||
import { BUILTIN_IO_FORMATS, IORegistry, type BuiltinIOFormatId } from '@open-pencil/core/io'
|
||||
import {
|
||||
EXPORT_FORMAT_IDS,
|
||||
MAX_EXPORT_SCALE,
|
||||
MIN_EXPORT_SCALE,
|
||||
clampExportScale
|
||||
} from '@open-pencil/scene-graph'
|
||||
import type { ExportFormatId, ExportSetting, PluginDataEntry } from '@open-pencil/scene-graph'
|
||||
|
||||
import { useSceneComputed } from '#vue/internal/scene-computed/use'
|
||||
|
||||
export const EXPORT_SCALES = [0.5, 0.75, 1, 1.5, 2, 3, 4] as const
|
||||
export const EXPORT_FORMATS: ExportFormatId[] = ['png', 'jpg', 'webp', 'svg', 'pdf']
|
||||
// Persisted export-setting formats must name built-in IO adapters.
|
||||
export const EXPORT_FORMATS = EXPORT_FORMAT_IDS satisfies readonly BuiltinIOFormatId[]
|
||||
|
||||
export interface ExportFormatOption {
|
||||
value: ExportFormatId
|
||||
label: string
|
||||
}
|
||||
|
||||
export type ExportPanelTarget = 'selection' | 'page'
|
||||
|
||||
|
|
@ -22,6 +33,12 @@ export { MIN_EXPORT_SCALE, MAX_EXPORT_SCALE, clampExportScale }
|
|||
|
||||
const io = new IORegistry(BUILTIN_IO_FORMATS)
|
||||
|
||||
/** Short, extension-style labels for the node export formats (PNG, PDF, PPTX, …). */
|
||||
export const EXPORT_FORMAT_OPTIONS: ExportFormatOption[] = EXPORT_FORMATS.map((id) => ({
|
||||
value: id,
|
||||
label: (io.getFormat(id)?.extensions[0] ?? id).toUpperCase()
|
||||
}))
|
||||
|
||||
export function createDefaultExportSetting(): ExportSetting {
|
||||
return { scale: 1, format: 'png' }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
EXPORT_FORMATS,
|
||||
EXPORT_FORMAT_OPTIONS,
|
||||
EXPORT_SCALES,
|
||||
MAX_EXPORT_SCALE,
|
||||
MIN_EXPORT_SCALE,
|
||||
|
|
@ -12,7 +13,7 @@ import { useEditor } from '#vue/editor/context'
|
|||
import { useSceneComputed } from '#vue/internal/scene-computed/use'
|
||||
|
||||
export type { ExportFormatId, ExportSetting } from '@open-pencil/scene-graph'
|
||||
export type { ExportPanelTarget } from '#vue/document/export/helpers'
|
||||
export type { ExportFormatOption, ExportPanelTarget } from '#vue/document/export/helpers'
|
||||
|
||||
export function useExport() {
|
||||
const editor = useEditor()
|
||||
|
|
@ -30,6 +31,7 @@ export function useExport() {
|
|||
minScale: MIN_EXPORT_SCALE,
|
||||
clampExportScale,
|
||||
formats: EXPORT_FORMATS,
|
||||
formatOptions: EXPORT_FORMAT_OPTIONS,
|
||||
formatSupportsScale,
|
||||
...targetState,
|
||||
...settingActions
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export type {
|
|||
UseDocumentWorkspaceOptions
|
||||
} from '#vue/document/workspace/use'
|
||||
export { useExport } from '#vue/document/export/use'
|
||||
export type { ExportFormatId, ExportSetting } from '#vue/document/export/use'
|
||||
export type { ExportFormatId, ExportFormatOption, ExportSetting } from '#vue/document/export/use'
|
||||
export { useFillControls } from '#vue/controls/fill/use'
|
||||
export { useColorVariableBinding } from '#vue/controls/color-variable-binding/use'
|
||||
export { useNumberVariableBinding } from '#vue/controls/number-variable-binding/use'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Editor, EditorState } from '@open-pencil/core/editor'
|
||||
import type { ExportRequest, IORegistry } from '@open-pencil/core/io'
|
||||
import type { BuiltinIOFormatId, ExportRequest, IORegistry } from '@open-pencil/core/io'
|
||||
|
||||
import {
|
||||
bundleExportFiles,
|
||||
|
|
@ -38,7 +38,7 @@ export function createDocumentExportActions(
|
|||
const format = io.getFormat(formatId)
|
||||
if (!format) throw new Error(`Unknown export format: ${formatId}`)
|
||||
|
||||
const exportOptions = getExportOptions(formatId, options)
|
||||
const exportOptions = getExportOptions(format, options)
|
||||
|
||||
const result = await io.exportContent(
|
||||
formatId,
|
||||
|
|
@ -50,7 +50,7 @@ export function createDocumentExportActions(
|
|||
const baseName = getExportBaseName(editor.graph, target)
|
||||
return {
|
||||
bytes: getExportBytes(result.data),
|
||||
fileName: getExportFileName(baseName, formatId, result.extension, options),
|
||||
fileName: getExportFileName(baseName, format, result.extension, options),
|
||||
format: format.label,
|
||||
ext: `.${result.extension}`,
|
||||
mime: result.mimeType
|
||||
|
|
@ -103,10 +103,7 @@ export function createDocumentExportActions(
|
|||
)
|
||||
}
|
||||
|
||||
async function exportSelection(
|
||||
scale: number,
|
||||
formatId: 'png' | 'jpg' | 'webp' | 'svg' | 'pdf' | 'pptx' | 'fig'
|
||||
) {
|
||||
async function exportSelection(scale: number, formatId: BuiltinIOFormatId) {
|
||||
await exportTarget(getSelectionExportTarget(), formatId, { scale })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,25 +74,21 @@ export function getExportBaseName(graph: SceneGraph, target: ExportRequest['targ
|
|||
return 'Export'
|
||||
}
|
||||
|
||||
export function getExportOptions(formatId: string, options?: ExportOptions): unknown {
|
||||
if (formatId === 'png' || formatId === 'jpg' || formatId === 'webp') {
|
||||
return {
|
||||
format: formatId.toUpperCase(),
|
||||
scale: options?.scale ?? 1,
|
||||
quality: options?.quality
|
||||
}
|
||||
export function getExportOptions(format: IOFormatAdapter, options?: ExportOptions): unknown {
|
||||
if (format.exportOptions?.scale) {
|
||||
return { scale: options?.scale ?? 1, quality: options?.quality }
|
||||
}
|
||||
if (formatId === 'jsx') return { format: options?.jsxFormat ?? 'openpencil' }
|
||||
if (format.id === 'jsx') return { format: options?.jsxFormat ?? 'openpencil' }
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function getExportFileName(
|
||||
baseName: string,
|
||||
formatId: string,
|
||||
format: IOFormatAdapter,
|
||||
extension: string,
|
||||
options?: ExportOptions
|
||||
): string {
|
||||
return formatId === 'png' || formatId === 'jpg' || formatId === 'webp'
|
||||
return format.exportOptions?.scale
|
||||
? `${baseName}@${options?.scale ?? 1}x.${extension}`
|
||||
: `${baseName}.${extension}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import IconUndo from '~icons/lucide/undo-2'
|
|||
import IconZoomIn from '~icons/lucide/zoom-in'
|
||||
import IconZoomOut from '~icons/lucide/zoom-out'
|
||||
|
||||
import type { BuiltinIOFormatId } from '@open-pencil/core/io'
|
||||
import type { CommandPaletteGroup, CommandPaletteItem, MenuEntry } from '@open-pencil/vue'
|
||||
import { shortcutPlatform, useEditorCommands, useI18n } from '@open-pencil/vue'
|
||||
|
||||
|
|
@ -143,7 +144,7 @@ export function useAppMenu() {
|
|||
}))
|
||||
)
|
||||
|
||||
function exportSelection(format: 'png' | 'svg' | 'pptx' | 'fig') {
|
||||
function exportSelection(format: BuiltinIOFormatId) {
|
||||
if (store.state.selectedIds.size > 0) void store.exportSelection(1, format)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { tryOnScopeDispose, useObjectUrl } from '@vueuse/core'
|
|||
import { computed, onActivated, onDeactivated, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import { useExport, useI18n, useRetainedActivity } from '@open-pencil/vue'
|
||||
import type { ExportFormatId } from '@open-pencil/vue'
|
||||
|
||||
import { useEditorStore } from '@/app/editor/active-store'
|
||||
import ExportScaleInput from '@/components/properties/ExportScaleInput.vue'
|
||||
|
|
@ -28,18 +27,11 @@ const {
|
|||
updateScale,
|
||||
updateFormat,
|
||||
formatSupportsScale,
|
||||
formatOptions,
|
||||
scales,
|
||||
clampExportScale
|
||||
} = useExport()
|
||||
|
||||
const FORMAT_OPTIONS: { value: ExportFormatId; label: string }[] = [
|
||||
{ value: 'png', label: 'PNG' },
|
||||
{ value: 'jpg', label: 'JPG' },
|
||||
{ value: 'webp', label: 'WEBP' },
|
||||
{ value: 'svg', label: 'SVG' },
|
||||
{ value: 'pdf', label: 'PDF' }
|
||||
]
|
||||
|
||||
const previewBlob = shallowRef<Blob | null>(null)
|
||||
const previewURL = useObjectUrl(previewBlob)
|
||||
const showPreview = ref(false)
|
||||
|
|
@ -155,11 +147,11 @@ watch(previewKey, updatePreview, { flush: 'post' })
|
|||
</div>
|
||||
<AppSelect
|
||||
:model-value="setting.format"
|
||||
:options="FORMAT_OPTIONS"
|
||||
:options="formatOptions"
|
||||
:label="panels.exportFormat"
|
||||
:ui="{ trigger: 'w-auto flex-1' }"
|
||||
data-property="export-format"
|
||||
@update:model-value="updateFormat(index, $event as ExportFormatId)"
|
||||
@update:model-value="updateFormat(index, $event)"
|
||||
/>
|
||||
<template #rail="{ removeClass }">
|
||||
<IconButton
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ test('format selector changes to JPG', async () => {
|
|||
await expect(formatTrigger).toHaveAttribute('aria-label', 'Export format')
|
||||
await formatTrigger.click()
|
||||
|
||||
for (const label of ['PNG', 'JPG', 'WEBP', 'SVG', 'PDF']) {
|
||||
for (const label of ['PNG', 'JPG', 'WEBP', 'SVG', 'PDF', 'PPTX']) {
|
||||
await expect(page.locator('[role="option"]').filter({ hasText: label })).toBeVisible()
|
||||
}
|
||||
|
||||
|
|
@ -218,6 +218,16 @@ test('a single export format downloads the file directly', async () => {
|
|||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
test('a PPTX export setting downloads a PowerPoint file', async () => {
|
||||
await createExportableRect([{ scale: 1, format: 'pptx' }])
|
||||
await forceBlobDownload()
|
||||
|
||||
await expect(exportItems().first().getByRole('textbox', { name: 'Export scale' })).toHaveCount(0)
|
||||
const [download] = await Promise.all([page.waitForEvent('download'), exportButton().click()])
|
||||
expect(download.suggestedFilename()).toBe('Export rect 1.pptx')
|
||||
canvas.assertNoErrors()
|
||||
})
|
||||
|
||||
test('preview toggle shows image with blob src', async () => {
|
||||
const formatTrigger = exportItems().first().getByRole('combobox', { name: 'Export format' })
|
||||
await formatTrigger.click()
|
||||
|
|
|
|||
19
tests/engine/io/export-formats.test.ts
Normal file
19
tests/engine/io/export-formats.test.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { BUILTIN_IO_FORMATS, IORegistry } from '@open-pencil/core/io'
|
||||
import { EXPORT_FORMAT_IDS, isExportFormatId } from '@open-pencil/scene-graph'
|
||||
|
||||
const io = new IORegistry(BUILTIN_IO_FORMATS)
|
||||
|
||||
describe('export setting formats', () => {
|
||||
test('every persisted export format is a node-exportable IO adapter', () => {
|
||||
const nodeExportIds = new Set(io.listExportFormats('node').map((format) => format.id))
|
||||
for (const id of EXPORT_FORMAT_IDS) expect(nodeExportIds.has(id)).toBe(true)
|
||||
})
|
||||
|
||||
test('recognizes only persisted export formats', () => {
|
||||
expect(isExportFormatId('pptx')).toBe(true)
|
||||
expect(isExportFormatId('fig')).toBe(false)
|
||||
expect(isExportFormatId('PNG')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -67,7 +67,8 @@ describe('fig roundtrip export settings', () => {
|
|||
name: 'Exportable rect',
|
||||
exportSettings: [
|
||||
{ scale: 2, format: 'png' },
|
||||
{ scale: 3, format: 'webp' }
|
||||
{ scale: 3, format: 'webp' },
|
||||
{ scale: 1, format: 'pptx' }
|
||||
]
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue