Add migration for horizontal alignment (#1443)
This adds a migration to migrate existing alignment options to their legacy counter parts (`start` -> `start-legacy`, `end` -> `end-legacy`, `middle` -> `middle-legacy`). With this change the legacy options don't show any align as active in the Styles panel:  I think this is probably what we want. ### Change Type - [x] `patch` — Bug Fix ### Test Plan 1. Use some old preview link to create Geo and Note shapes with old alignment options. You can use this one: https://examples-kzwtf68jr-tldraw.vercel.app/ 2. Copy and paste these shapes over to staging. Nothing should change visually. 3. Also try out exporting to svg (with both old and new alignment options) - [x] Unit Tests - [ ] Webdriver tests ### Release Notes - Add support for legacy alignment options. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
89428edffc
commit
787eab75d6
11 changed files with 216 additions and 10 deletions
|
@ -1090,15 +1090,18 @@ input,
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-text-label[data-align='start'] {
|
.tl-text-label[data-align='start'],
|
||||||
|
.tl-text-label[data-align='start-legacy'] {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-text-label[data-align='middle'] {
|
.tl-text-label[data-align='middle'],
|
||||||
|
.tl-text-label[data-align='middle-legacy'] {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-text-label[data-align='end'] {
|
.tl-text-label[data-align='end'],
|
||||||
|
.tl-text-label[data-align='end-legacy'] {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,13 @@ import { uniqueId } from '../../utils/data'
|
||||||
import { App } from '../App'
|
import { App } from '../App'
|
||||||
import { TextHelpers } from '../shapeutils/TLTextUtil/TextHelpers'
|
import { TextHelpers } from '../shapeutils/TLTextUtil/TextHelpers'
|
||||||
|
|
||||||
const textAlignmentsForLtr: Record<TLAlignType, string> = {
|
const textAlignmentsForLtr = {
|
||||||
start: 'left',
|
start: 'left',
|
||||||
|
'start-legacy': 'left',
|
||||||
middle: 'center',
|
middle: 'center',
|
||||||
|
'middle-legacy': 'center',
|
||||||
end: 'right',
|
end: 'right',
|
||||||
|
'end-legacy': 'right',
|
||||||
}
|
}
|
||||||
|
|
||||||
type OverflowMode = 'wrap' | 'truncate-ellipsis' | 'truncate-clip'
|
type OverflowMode = 'wrap' | 'truncate-ellipsis' | 'truncate-clip'
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
import { TLDashType, TLGeoShape, TLGeoShapeProps } from '@tldraw/tlschema'
|
import { TLDashType, TLGeoShape, TLGeoShapeProps } from '@tldraw/tlschema'
|
||||||
import { SVGContainer } from '../../../components/SVGContainer'
|
import { SVGContainer } from '../../../components/SVGContainer'
|
||||||
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
||||||
|
import { getLegacyOffsetX } from '../../../utils/legacy'
|
||||||
import { App } from '../../App'
|
import { App } from '../../App'
|
||||||
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
||||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||||
|
@ -646,9 +647,14 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
|
||||||
width: Math.ceil(bounds.width),
|
width: Math.ceil(bounds.width),
|
||||||
height: Math.ceil(bounds.height),
|
height: Math.ceil(bounds.height),
|
||||||
overflow: 'wrap' as const,
|
overflow: 'wrap' as const,
|
||||||
|
offsetX: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
const spans = this.app.textMeasure.measureTextSpans(props.text, opts)
|
const spans = this.app.textMeasure.measureTextSpans(props.text, opts)
|
||||||
|
const offsetX = getLegacyOffsetX(shape.props.align, padding, spans, bounds.width)
|
||||||
|
if (offsetX) {
|
||||||
|
opts.offsetX = offsetX
|
||||||
|
}
|
||||||
|
|
||||||
const groupEl = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
const groupEl = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives'
|
import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives'
|
||||||
import { TLNoteShape } from '@tldraw/tlschema'
|
import { TLNoteShape } from '@tldraw/tlschema'
|
||||||
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
||||||
|
import { getLegacyOffsetX } from '../../../utils/legacy'
|
||||||
import { App } from '../../App'
|
import { App } from '../../App'
|
||||||
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
||||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||||
|
@ -149,6 +150,11 @@ export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
|
||||||
const spans = this.app.textMeasure.measureTextSpans(shape.props.text, opts)
|
const spans = this.app.textMeasure.measureTextSpans(shape.props.text, opts)
|
||||||
|
|
||||||
opts.width = bounds.width
|
opts.width = bounds.width
|
||||||
|
const offsetX = getLegacyOffsetX(shape.props.align, PADDING, spans, bounds.width)
|
||||||
|
if (offsetX) {
|
||||||
|
opts.offsetX = offsetX
|
||||||
|
}
|
||||||
|
|
||||||
opts.padding = PADDING
|
opts.padding = PADDING
|
||||||
|
|
||||||
const textElm = createTextSvgElementFromSpans(this.app, spans, opts)
|
const textElm = createTextSvgElementFromSpans(this.app, spans, opts)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
import { LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
||||||
import { stopEventPropagation } from '../../../utils/dom'
|
import { stopEventPropagation } from '../../../utils/dom'
|
||||||
|
import { isLegacyAlign } from '../../../utils/legacy'
|
||||||
import { TextHelpers } from '../TLTextUtil/TextHelpers'
|
import { TextHelpers } from '../TLTextUtil/TextHelpers'
|
||||||
import { useEditableText } from './useEditableText'
|
import { useEditableText } from './useEditableText'
|
||||||
|
|
||||||
|
@ -50,6 +51,7 @@ export const TextLabel = React.memo(function TextLabel<
|
||||||
const isInteractive = isEditing || isEditableFromHover
|
const isInteractive = isEditing || isEditableFromHover
|
||||||
const finalText = TextHelpers.normalizeTextForDom(text)
|
const finalText = TextHelpers.normalizeTextForDom(text)
|
||||||
const hasText = finalText.trim().length > 0
|
const hasText = finalText.trim().length > 0
|
||||||
|
const legacyAlign = isLegacyAlign(align)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -62,7 +64,7 @@ export const TextLabel = React.memo(function TextLabel<
|
||||||
style={
|
style={
|
||||||
hasText || isInteractive
|
hasText || isInteractive
|
||||||
? {
|
? {
|
||||||
justifyContent: align === 'middle' ? 'center' : align,
|
justifyContent: align === 'middle' || legacyAlign ? 'center' : align,
|
||||||
alignItems: verticalAlign === 'middle' ? 'center' : verticalAlign,
|
alignItems: verticalAlign === 'middle' ? 'center' : verticalAlign,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|
26
packages/editor/src/lib/utils/legacy.ts
Normal file
26
packages/editor/src/lib/utils/legacy.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { Box2d } from '@tldraw/primitives'
|
||||||
|
import { Box2dModel, TLAlignType } from '@tldraw/tlschema'
|
||||||
|
|
||||||
|
export function getLegacyOffsetX(
|
||||||
|
align: TLAlignType | string,
|
||||||
|
padding: number,
|
||||||
|
spans: { text: string; box: Box2dModel }[],
|
||||||
|
totalWidth: number
|
||||||
|
): number | undefined {
|
||||||
|
if ((align === 'start-legacy' || align === 'end-legacy') && spans.length !== 0) {
|
||||||
|
const spansBounds = Box2d.From(spans[0].box)
|
||||||
|
for (const { box } of spans) {
|
||||||
|
spansBounds.union(box)
|
||||||
|
}
|
||||||
|
if (align === 'start-legacy') {
|
||||||
|
return (totalWidth - 2 * padding - spansBounds.width) / 2
|
||||||
|
} else if (align === 'end-legacy') {
|
||||||
|
return -(totalWidth - 2 * padding - spansBounds.width) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sneaky TLAlignType for legacies
|
||||||
|
export function isLegacyAlign(align: TLAlignType | string): boolean {
|
||||||
|
return align === 'start-legacy' || align === 'middle-legacy' || align === 'end-legacy'
|
||||||
|
}
|
|
@ -704,6 +704,60 @@ describe('Add verticalAlign to props for next shape', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Migrate GeoShape legacy horizontal alignment', () => {
|
||||||
|
const { up, down } = geoShapeTypeMigrations.migrators[6]
|
||||||
|
|
||||||
|
test('up works as expected', () => {
|
||||||
|
expect(up({ props: { align: 'start', type: 'ellipse' } })).toEqual({
|
||||||
|
props: { align: 'start-legacy', type: 'ellipse' },
|
||||||
|
})
|
||||||
|
expect(up({ props: { align: 'middle', type: 'ellipse' } })).toEqual({
|
||||||
|
props: { align: 'middle-legacy', type: 'ellipse' },
|
||||||
|
})
|
||||||
|
expect(up({ props: { align: 'end', type: 'ellipse' } })).toEqual({
|
||||||
|
props: { align: 'end-legacy', type: 'ellipse' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
test('down works as expected', () => {
|
||||||
|
expect(down({ props: { align: 'start-legacy', type: 'ellipse' } })).toEqual({
|
||||||
|
props: { align: 'start', type: 'ellipse' },
|
||||||
|
})
|
||||||
|
expect(down({ props: { align: 'middle-legacy', type: 'ellipse' } })).toEqual({
|
||||||
|
props: { align: 'middle', type: 'ellipse' },
|
||||||
|
})
|
||||||
|
expect(down({ props: { align: 'end-legacy', type: 'ellipse' } })).toEqual({
|
||||||
|
props: { align: 'end', type: 'ellipse' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Migrate NoteShape legacy horizontal alignment', () => {
|
||||||
|
const { up, down } = noteShapeTypeMigrations.migrators[3]
|
||||||
|
|
||||||
|
test('up works as expected', () => {
|
||||||
|
expect(up({ props: { align: 'start', color: 'red' } })).toEqual({
|
||||||
|
props: { align: 'start-legacy', color: 'red' },
|
||||||
|
})
|
||||||
|
expect(up({ props: { align: 'middle', color: 'red' } })).toEqual({
|
||||||
|
props: { align: 'middle-legacy', color: 'red' },
|
||||||
|
})
|
||||||
|
expect(up({ props: { align: 'end', color: 'red' } })).toEqual({
|
||||||
|
props: { align: 'end-legacy', color: 'red' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
test('down works as expected', () => {
|
||||||
|
expect(down({ props: { align: 'start-legacy', color: 'red' } })).toEqual({
|
||||||
|
props: { align: 'start', color: 'red' },
|
||||||
|
})
|
||||||
|
expect(down({ props: { align: 'middle-legacy', color: 'red' } })).toEqual({
|
||||||
|
props: { align: 'middle', color: 'red' },
|
||||||
|
})
|
||||||
|
expect(down({ props: { align: 'end-legacy', color: 'red' } })).toEqual({
|
||||||
|
props: { align: 'end', color: 'red' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Removing isReadOnly from user_document', () => {
|
describe('Removing isReadOnly from user_document', () => {
|
||||||
const { up, down } = userDocumentTypeMigrations.migrators[userDocumentVersions.RemoveIsReadOnly]
|
const { up, down } = userDocumentTypeMigrations.migrators[userDocumentVersions.RemoveIsReadOnly]
|
||||||
const prev = {
|
const prev = {
|
||||||
|
|
|
@ -74,11 +74,12 @@ const Versions = {
|
||||||
RemoveJustify: 3,
|
RemoveJustify: 3,
|
||||||
AddCheckBox: 4,
|
AddCheckBox: 4,
|
||||||
AddVerticalAlign: 5,
|
AddVerticalAlign: 5,
|
||||||
|
MigrateLegacyAlign: 6,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const geoShapeTypeMigrations = defineMigrations({
|
export const geoShapeTypeMigrations = defineMigrations({
|
||||||
currentVersion: Versions.AddVerticalAlign,
|
currentVersion: Versions.MigrateLegacyAlign,
|
||||||
migrators: {
|
migrators: {
|
||||||
[Versions.AddUrlProp]: {
|
[Versions.AddUrlProp]: {
|
||||||
up: (shape) => {
|
up: (shape) => {
|
||||||
|
@ -158,5 +159,51 @@ export const geoShapeTypeMigrations = defineMigrations({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[Versions.MigrateLegacyAlign]: {
|
||||||
|
up: (shape) => {
|
||||||
|
let newAlign: TLAlignType
|
||||||
|
switch (shape.props.align) {
|
||||||
|
case 'start':
|
||||||
|
newAlign = 'start-legacy' as TLAlignType
|
||||||
|
break
|
||||||
|
case 'end':
|
||||||
|
newAlign = 'end-legacy' as TLAlignType
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
newAlign = 'middle-legacy' as TLAlignType
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...shape,
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
align: newAlign,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
down: (shape) => {
|
||||||
|
let oldAlign: TLAlignType
|
||||||
|
switch (shape.props.align) {
|
||||||
|
case 'start-legacy':
|
||||||
|
oldAlign = 'start'
|
||||||
|
break
|
||||||
|
case 'end-legacy':
|
||||||
|
oldAlign = 'end'
|
||||||
|
break
|
||||||
|
case 'middle-legacy':
|
||||||
|
oldAlign = 'middle'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
oldAlign = shape.props.align
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...shape,
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
align: oldAlign,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -43,11 +43,12 @@ export const noteShapeTypeValidator: T.Validator<TLNoteShape> = createShapeValid
|
||||||
const Versions = {
|
const Versions = {
|
||||||
AddUrlProp: 1,
|
AddUrlProp: 1,
|
||||||
RemoveJustify: 2,
|
RemoveJustify: 2,
|
||||||
|
MigrateLegacyAlign: 3,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const noteShapeTypeMigrations = defineMigrations({
|
export const noteShapeTypeMigrations = defineMigrations({
|
||||||
currentVersion: Versions.RemoveJustify,
|
currentVersion: Versions.MigrateLegacyAlign,
|
||||||
migrators: {
|
migrators: {
|
||||||
[Versions.AddUrlProp]: {
|
[Versions.AddUrlProp]: {
|
||||||
up: (shape) => {
|
up: (shape) => {
|
||||||
|
@ -77,5 +78,52 @@ export const noteShapeTypeMigrations = defineMigrations({
|
||||||
return { ...shape }
|
return { ...shape }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[Versions.MigrateLegacyAlign]: {
|
||||||
|
up: (shape) => {
|
||||||
|
let newAlign: TLAlignType
|
||||||
|
switch (shape.props.align) {
|
||||||
|
case 'start':
|
||||||
|
newAlign = 'start-legacy' as TLAlignType
|
||||||
|
break
|
||||||
|
case 'end':
|
||||||
|
newAlign = 'end-legacy' as TLAlignType
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
newAlign = 'middle-legacy' as TLAlignType
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...shape,
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
align: newAlign,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
down: (shape) => {
|
||||||
|
let oldAlign: TLAlignType
|
||||||
|
switch (shape.props.align) {
|
||||||
|
case 'start-legacy':
|
||||||
|
oldAlign = 'start'
|
||||||
|
break
|
||||||
|
case 'end-legacy':
|
||||||
|
oldAlign = 'end'
|
||||||
|
break
|
||||||
|
case 'middle-legacy':
|
||||||
|
oldAlign = 'middle'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
oldAlign = shape.props.align
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...shape,
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
align: oldAlign,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -125,6 +125,14 @@ export interface TLFontStyle extends TLBaseStyle {
|
||||||
/** @public */
|
/** @public */
|
||||||
export const TL_ALIGN_TYPES = new Set(['start', 'middle', 'end'] as const)
|
export const TL_ALIGN_TYPES = new Set(['start', 'middle', 'end'] as const)
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export const TL_ALIGN_TYPES_WITH_LEGACY_STUFF = new Set([
|
||||||
|
...TL_ALIGN_TYPES,
|
||||||
|
'start-legacy',
|
||||||
|
'end-legacy',
|
||||||
|
'middle-legacy',
|
||||||
|
] as const)
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLAlignType = SetValue<typeof TL_ALIGN_TYPES>
|
export type TLAlignType = SetValue<typeof TL_ALIGN_TYPES>
|
||||||
|
|
||||||
|
@ -142,7 +150,7 @@ export interface TLAlignStyle extends TLBaseStyle {
|
||||||
|
|
||||||
// Geo Text Vertical Align
|
// Geo Text Vertical Align
|
||||||
/** @public */
|
/** @public */
|
||||||
export const TL_VERTICAL_ALIGN_TYPES = TL_ALIGN_TYPES
|
export const TL_VERTICAL_ALIGN_TYPES = new Set(['start', 'middle', 'end'] as const)
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLVerticalAlignType = SetValue<typeof TL_VERTICAL_ALIGN_TYPES>
|
export type TLVerticalAlignType = SetValue<typeof TL_VERTICAL_ALIGN_TYPES>
|
||||||
|
|
|
@ -6,7 +6,8 @@ import type { TLPageId } from './records/TLPage'
|
||||||
import type { TLParentId, TLShapeId } from './records/TLShape'
|
import type { TLParentId, TLShapeId } from './records/TLShape'
|
||||||
import type { TLUserId } from './records/TLUser'
|
import type { TLUserId } from './records/TLUser'
|
||||||
import {
|
import {
|
||||||
TL_ALIGN_TYPES,
|
TLAlignType,
|
||||||
|
TL_ALIGN_TYPES_WITH_LEGACY_STUFF,
|
||||||
TL_ARROWHEAD_TYPES,
|
TL_ARROWHEAD_TYPES,
|
||||||
TL_COLOR_TYPES,
|
TL_COLOR_TYPES,
|
||||||
TL_DASH_TYPES,
|
TL_DASH_TYPES,
|
||||||
|
@ -63,7 +64,9 @@ export const sizeValidator = T.setEnum(TL_SIZE_TYPES)
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const fontValidator = T.setEnum(TL_FONT_TYPES)
|
export const fontValidator = T.setEnum(TL_FONT_TYPES)
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const alignValidator = T.setEnum(TL_ALIGN_TYPES)
|
export const alignValidator = T.setEnum<TLAlignType>(
|
||||||
|
TL_ALIGN_TYPES_WITH_LEGACY_STUFF as Set<TLAlignType>
|
||||||
|
)
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const verticalAlignValidator = T.setEnum(TL_VERTICAL_ALIGN_TYPES)
|
export const verticalAlignValidator = T.setEnum(TL_VERTICAL_ALIGN_TYPES)
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue