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: {},
},
},
assets: {},
pageStates: {
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 {
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

View file

@ -66,7 +66,9 @@ function PageMenuContent({ onClose }: { onClose: () => void }) {
const currentPageId = app.useStore(currentPageIdSelector)
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])
const handleChangePage = React.useCallback(

View file

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

View file

@ -83,6 +83,7 @@ import { StateManager } from './StateManager'
import { clearPrevSize } from './shapes/shared/getTextSize'
import { getClipboard, setClipboard } from './IdbClipboard'
import { deepCopy } from './StateManager/copy'
import { getTranslation } from '~translations'
const uuid = Utils.uniqueId()
@ -1226,9 +1227,15 @@ export class TldrawApp extends StateManager<TDSnapshot> {
this.pasteInfo.offset = [0, 0]
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()
.clearSelectHistory()
.loadDocument(migrate(TldrawApp.defaultDocument, TldrawApp.version))
.loadDocument(migrate(doc, TldrawApp.version))
.persist({})
return this

View file

@ -1,8 +1,17 @@
import { mockDocument, TldrawTestApp } from '~test'
describe('Create page command', () => {
const app = new TldrawTestApp()
let app: 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', () => {
app.loadDocument(mockDocument)
@ -34,30 +43,116 @@ describe('Create page command', () => {
it('increments page names', () => {
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)')
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)')
expect(app.page.name).toBe('Page 3')
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 { Utils, TLPageState } from '@tldraw/core'
import type { TldrawApp } from '~state'
import { getIncrementedName } from '../shared/getIncrementedName'
export function createPage(
app: TldrawApp,
center: number[],
pageId = Utils.uniqueId(),
pageName = 'New page'
pageName = 'Page'
): TldrawCommand {
const { currentPageId } = app
@ -20,7 +21,7 @@ export function createPage(
const page: TDPage = {
id: pageId,
name: Utils.getIncrementedName(
name: getIncrementedName(
pageName,
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",
"new.page": "Nueva página",
"page.name": "Nombre de página",
"page": "Página",
"duplicate": "Duplicar",
"cancel": "Cancelar",
"copy.invite.link": "Copiar invitación",

View file

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

View file

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

View file

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

View file

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