Implemented better page numbering (#779)

* Implemented better page numbering

* Added spanish and french translation

* Add tests, fix regex

* Improve page naming logic

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
Enrico 2022-07-04 16:17:47 +02:00 committed by GitHub
parent 1de57cffc0
commit 4d900fb7fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 184 additions and 69 deletions

View file

@ -13,6 +13,7 @@ export const defaultDocument: TDDocument = {
bindings: {}, bindings: {},
}, },
}, },
assets: {},
pageStates: { pageStates: {
page: { page: {
id: 'page', id: 'page',

View file

@ -1485,24 +1485,6 @@ left past the initial left edge) then swap points on that axis.
static metaKey(e: KeyboardEvent | React.KeyboardEvent): boolean { static metaKey(e: KeyboardEvent | React.KeyboardEvent): boolean {
return Utils.isDarwin() ? e.metaKey : e.ctrlKey return Utils.isDarwin() ? e.metaKey : e.ctrlKey
} }
/**
* Get an incremented name (e.g. New page (2)) from a name (e.g. New page), based on an array of existing names.
*
* @param name The name to increment.
* @param others The array of existing names.
*/
static getIncrementedName(name: string, others: string[]) {
let result = name
while (others.includes(result)) {
result = /\s\((\d+)\)$/.exec(result)?.[1]
? result.replace(/\d+(?=\)$)/, (m) => (+m + 1).toString())
: `${result} (1)`
}
return result
}
} }
export default Utils export default Utils

View file

@ -66,7 +66,9 @@ function PageMenuContent({ onClose }: { onClose: () => void }) {
const currentPageId = app.useStore(currentPageIdSelector) const currentPageId = app.useStore(currentPageIdSelector)
const handleCreatePage = React.useCallback(() => { const handleCreatePage = React.useCallback(() => {
app.createPage(undefined, intl.formatMessage({ id: 'new.page' })) const pageName =
intl.formatMessage({ id: 'page' }) + ' ' + (Object.keys(app.document.pages).length + 1)
app.createPage(undefined, pageName)
}, [app]) }, [app])
const handleChangePage = React.useCallback( const handleChangePage = React.useCallback(

View file

@ -1,19 +1,10 @@
import * as React from 'react' import * as React from 'react'
import { TRANSLATIONS, TDLanguage } from '../translations/translations' import { getTranslation, TDLanguage } from '../translations/translations'
export function useTranslation(code?: TDLanguage) { export function useTranslation(code?: TDLanguage) {
return React.useMemo(() => { return React.useMemo(() => {
const locale = code ?? navigator.language.split(/[-_]/)[0] const locale = code ?? navigator.language.split(/[-_]/)[0]
const translation = TRANSLATIONS.find((t) => t.code === locale) return getTranslation(locale)
const defaultTranslation = TRANSLATIONS.find((t) => t.code === 'en')!
const messages = {
...defaultTranslation.messages,
...translation?.messages,
}
return { locale, messages }
}, [code]) }, [code])
} }

View file

@ -83,6 +83,7 @@ import { StateManager } from './StateManager'
import { clearPrevSize } from './shapes/shared/getTextSize' import { clearPrevSize } from './shapes/shared/getTextSize'
import { getClipboard, setClipboard } from './IdbClipboard' import { getClipboard, setClipboard } from './IdbClipboard'
import { deepCopy } from './StateManager/copy' import { deepCopy } from './StateManager/copy'
import { getTranslation } from '~translations'
const uuid = Utils.uniqueId() const uuid = Utils.uniqueId()
@ -1226,9 +1227,15 @@ export class TldrawApp extends StateManager<TDSnapshot> {
this.pasteInfo.offset = [0, 0] this.pasteInfo.offset = [0, 0]
this.currentTool = this.tools.select this.currentTool = this.tools.select
const doc = TldrawApp.defaultDocument
// Set the default page name to the localized version of "Page"
const translation = getTranslation(this.settings.language)
doc.pages['page'].name = translation.messages['page'] + ' 1' ?? 'Page 1'
this.resetHistory() this.resetHistory()
.clearSelectHistory() .clearSelectHistory()
.loadDocument(migrate(TldrawApp.defaultDocument, TldrawApp.version)) .loadDocument(migrate(doc, TldrawApp.version))
.persist({}) .persist({})
return this return this

View file

@ -1,8 +1,17 @@
import { mockDocument, TldrawTestApp } from '~test' import { mockDocument, TldrawTestApp } from '~test'
describe('Create page command', () => { let app: TldrawTestApp
const app = new TldrawTestApp()
beforeEach(() => {
app = new TldrawTestApp()
})
function createPageWithName(app: TldrawTestApp) {
const pageName = 'Page' + ' ' + (Object.keys(app.document.pages).length + 1)
app.createPage(undefined, pageName)
}
describe('Create page command', () => {
it('does, undoes and redoes command', () => { it('does, undoes and redoes command', () => {
app.loadDocument(mockDocument) app.loadDocument(mockDocument)
@ -34,30 +43,116 @@ describe('Create page command', () => {
it('increments page names', () => { it('increments page names', () => {
app.loadDocument(mockDocument) app.loadDocument(mockDocument)
app.createPage() createPageWithName(app)
expect(app.page.name).toBe('New page') expect(app.page.name).toBe('Page 2')
app.createPage() createPageWithName(app)
expect(app.page.name).toBe('New page (1)') expect(app.page.name).toBe('Page 3')
app.createPage()
expect(app.page.name).toBe('New page (2)')
app.renamePage(app.page.id, 'New page!')
app.createPage()
expect(app.page.name).toBe('New page (2)')
app.deletePage(app.page.id) app.deletePage(app.page.id)
expect(app.page.name).toBe('New page!') createPageWithName(app)
app.createPage(undefined, 'New page!') expect(app.page.name).toBe('Page 3')
expect(app.page.name).toBe('New page! (1)') createPageWithName(app)
expect(app.page.name).toBe('Page 4')
app.renamePage(app.page.id, 'Page!')
createPageWithName(app)
expect(app.page.name).toBe('Page 5')
app.renamePage(app.page.id, 'Page 6')
createPageWithName(app)
expect(app.page.name).toBe('Page 7')
})
})
describe('when the page name exists', () => {
it('when others is empty', () => {
app.loadDocument(mockDocument)
app.createPage(undefined, 'Apple')
expect(app.page.name).toBe('Apple')
})
it('when others has no match', () => {
app.createPage(undefined, 'Orange')
app.createPage(undefined, 'Apple')
expect(app.page.name).toBe('Apple')
})
it('when others has one match', () => {
app.createPage(undefined, 'Orange')
app.createPage(undefined, 'Apple')
app.createPage(undefined, 'Apple')
expect(app.page.name).toBe('Apple 1')
})
it('when others has two matches', () => {
app.createPage(undefined, 'Orange')
app.createPage(undefined, 'Apple')
app.createPage(undefined, 'Apple 1')
app.createPage(undefined, 'Apple')
expect(app.page.name).toBe('Apple 2')
})
it('when others has a near match', () => {
app.createPage(undefined, 'Orange')
app.createPage(undefined, 'Apple')
app.createPage(undefined, 'Apple ()')
app.createPage(undefined, 'Apples')
app.createPage(undefined, 'Apple')
expect(app.page.name).toBe('Apple 1')
})
it('when others has a near match', () => {
app.createPage(undefined, 'Orange')
app.createPage(undefined, 'Apple')
app.createPage(undefined, 'Apple 1!')
app.createPage(undefined, 'Apple')
expect(app.page.name).toBe('Apple 1')
})
it('when others has a near match', () => {
app.createPage(undefined, 'Orange')
app.createPage(undefined, 'Apple')
app.createPage(undefined, 'Apple 1!')
app.createPage(undefined, 'Apple 1!')
expect(app.page.name).toBe('Apple 1! 1')
})
it('when others has a near match', () => {
app.createPage(undefined, 'Orange')
app.createPage(undefined, 'Apple')
app.createPage(undefined, 'Apple 1')
app.createPage(undefined, 'Apple 2')
app.createPage(undefined, 'Apple 3')
app.createPage(undefined, 'Apple 5')
app.createPage(undefined, 'Apple')
expect(app.page.name).toBe('Apple 4')
})
it('when others has a near match', () => {
app.createPage(undefined, 'Orange')
app.createPage(undefined, 'Apple')
app.createPage(undefined, 'Apple 1')
app.createPage(undefined, 'Apple 2')
app.createPage(undefined, 'Apple 3')
app.createPage(undefined, 'Apple 4')
app.createPage(undefined, 'Apple 5')
app.createPage(undefined, 'Apple 6')
app.createPage(undefined, 'Apple 7')
app.createPage(undefined, 'Apple 8')
app.createPage(undefined, 'Apple 9')
app.createPage(undefined, 'Apple 10')
app.createPage(undefined, 'Apple')
expect(app.page.name).toBe('Apple 11')
}) })
}) })

View file

@ -1,12 +1,13 @@
import type { TldrawCommand, TDPage } from '~types' import type { TldrawCommand, TDPage } from '~types'
import { Utils, TLPageState } from '@tldraw/core' import { Utils, TLPageState } from '@tldraw/core'
import type { TldrawApp } from '~state' import type { TldrawApp } from '~state'
import { getIncrementedName } from '../shared/getIncrementedName'
export function createPage( export function createPage(
app: TldrawApp, app: TldrawApp,
center: number[], center: number[],
pageId = Utils.uniqueId(), pageId = Utils.uniqueId(),
pageName = 'New page' pageName = 'Page'
): TldrawCommand { ): TldrawCommand {
const { currentPageId } = app const { currentPageId } = app
@ -20,7 +21,7 @@ export function createPage(
const page: TDPage = { const page: TDPage = {
id: pageId, id: pageId,
name: Utils.getIncrementedName( name: getIncrementedName(
pageName, pageName,
pages.map((p) => p.name ?? '') pages.map((p) => p.name ?? '')
), ),

View file

@ -0,0 +1,18 @@
/**
* Get an incremented name (e.g. Page 2) from a name (e.g. Page 1), based on an array of existing names.
*
* @param name The name to increment.
* @param others The array of existing names.
*/
export function getIncrementedName(name: string, others: string[]) {
let result = name
const set = new Set(others)
while (set.has(result)) {
result = /^.*(\d+)$/.exec(result)?.[1]
? result.replace(/(\d+)(?=\D?)$/, (m) => (+m + 1).toString())
: `${result} 1`
}
return result
}

View file

@ -51,6 +51,7 @@
"create.page": "Crear página", "create.page": "Crear página",
"new.page": "Nueva página", "new.page": "Nueva página",
"page.name": "Nombre de página", "page.name": "Nombre de página",
"page": "Página",
"duplicate": "Duplicar", "duplicate": "Duplicar",
"cancel": "Cancelar", "cancel": "Cancelar",
"copy.invite.link": "Copiar invitación", "copy.invite.link": "Copiar invitación",

View file

@ -51,6 +51,7 @@
"create.page": "Créer une Page", "create.page": "Créer une Page",
"new.page": "Nouvelle Page", "new.page": "Nouvelle Page",
"page.name": "Nom de la Page", "page.name": "Nom de la Page",
"page": "Page",
"duplicate": "Dupliquer", "duplicate": "Dupliquer",
"cancel": "Annuler", "cancel": "Annuler",
"copy.invite.link": "Copier le Lien d'Invitation", "copy.invite.link": "Copier le Lien d'Invitation",

View file

@ -51,6 +51,7 @@
"create.page": "Crea nuova pagina", "create.page": "Crea nuova pagina",
"new.page": "Nuova pagina", "new.page": "Nuova pagina",
"page.name": "Nome pagina", "page.name": "Nome pagina",
"page": "Pagina",
"duplicate": "Duplica", "duplicate": "Duplica",
"cancel": "Chiudi", "cancel": "Chiudi",
"copy.invite.link": "Copia link invito", "copy.invite.link": "Copia link invito",

View file

@ -51,6 +51,7 @@
"create.page": "Create Page", "create.page": "Create Page",
"new.page": "New Page", "new.page": "New Page",
"page.name": "Page Name", "page.name": "Page Name",
"page": "Page",
"duplicate": "Duplicate", "duplicate": "Duplicate",
"cancel": "Cancel", "cancel": "Cancel",
"copy.invite.link": "Copy Invite Link", "copy.invite.link": "Copy Invite Link",

View file

@ -22,23 +22,23 @@ import zh_cn from './zh-cn.json'
// translation instead. // translation instead.
export const TRANSLATIONS: TDTranslations = [ export const TRANSLATIONS: TDTranslations = [
{ code: 'ar', label: 'عربي', messages: ar }, { code: 'ar', locale: 'ar', label: 'عربي', messages: ar },
{ code: 'da', label: 'Danish', messages: da }, { code: 'en', locale: 'en', label: 'English', messages: en },
{ code: 'de', label: 'Deutsch', messages: de }, { code: 'es', locale: 'es', label: 'Español', messages: es },
{ code: 'en', label: 'English', messages: en }, { code: 'fr', locale: 'fr', label: 'Français', messages: fr },
{ code: 'es', label: 'Español', messages: es }, { code: 'fa', locale:'fa', label: 'فارسی', messages: fa },
{ code: 'fa', label: 'فارسی', messages: fa }, { code: 'it', locale: 'it', label: 'Italiano', messages: it },
{ code: 'fr', label: 'Français', messages: fr }, { code: 'ja', locale: 'ja', label: '日本語', messages: ja },
{ code: 'it', label: 'Italiano', messages: it }, { code: 'ko-kr', locale: 'ko-kr', label: '한국어', messages: ko_kr },
{ code: 'ja', label: '日本語', messages: ja }, { code: 'ne', locale: 'ne', label: 'नेपाली', messages: ne },
{ code: 'ko-kr', label: '한국어', messages: ko_kr }, { code: 'no', locale: 'no', label: 'Norwegian', messages: no },
{ code: 'ne', label: 'नेपाली', messages: ne }, { code: 'pl', locale: 'pl', label: 'Polski', messages: pl },
{ code: 'no', label: 'Norwegian', messages: no }, { code: 'pt-br', locale: 'pt-br', label: 'Português - Brasil', messages: pt_br },
{ code: 'pl', label: 'Polski', messages: pl }, { code: 'tr', locale: 'tr', label: 'Türkçe', messages: tr },
{ code: 'pt-br', label: 'Português - Brasil', messages: pt_br }, { code: 'zh-cn', locale: 'zh-ch', label: 'Chinese - Simplified', messages: zh_cn },
{ code: 'ru', label: 'Russian', messages: ru }, { code: 'da', locale: 'da', label: 'Danish', messages: da },
{ code: 'tr', label: 'Türkçe', messages: tr }, { code: 'de', locale: 'de', label: 'Deutsch', messages: de},
{ code: 'zh-cn', label: 'Chinese - Simplified', messages: zh_cn }, { code: 'ru', locale: 'ru', label: 'Russian', messages: ru },
] ]
/* ----------------- (do not change) ---------------- */ /* ----------------- (do not change) ---------------- */
@ -48,9 +48,23 @@ TRANSLATIONS.sort((a, b) => (a.code < b.code ? -1 : 1))
export type TDTranslation = { export type TDTranslation = {
readonly code: string readonly code: string
readonly label: string readonly label: string
readonly locale: string
readonly messages: Partial<typeof en> readonly messages: Partial<typeof en>
} }
export type TDTranslations = TDTranslation[] export type TDTranslations = TDTranslation[]
export type TDLanguage = TDTranslations[number]['code'] export type TDLanguage = TDTranslations[number]['code']
export function getTranslation(code: TDLanguage): TDTranslation {
const translation = TRANSLATIONS.find((t) => t.code === code)
const defaultTranslation = TRANSLATIONS.find((t) => t.code === 'en')!
const messages = {
...defaultTranslation.messages,
...translation?.messages,
}
return { code, messages, locale: code, label: translation?.label ?? code }
}