Compare commits
31 Commits
feat/move-
...
v3.17.0
| Author | SHA1 | Date | |
|---|---|---|---|
| d4c4a2b9c1 | |||
| 1b617c7910 | |||
| 9676a270da | |||
| f3b4fac4c0 | |||
| a8c6c2625c | |||
| a2a1151ccb | |||
| daaed01d02 | |||
| e7d6c482ad | |||
| ed305f656c | |||
| 30e932a874 | |||
| 0a74e07288 | |||
| 4c3de8c942 | |||
| 49d44d9701 | |||
| 96a7acff33 | |||
| 65588686c7 | |||
| fcf9cc3fc9 | |||
| 20091143d2 | |||
| f708d0a2b2 | |||
| 7e9f4409f6 | |||
| 2e63af1aa2 | |||
| 5ebd2d508c | |||
| 9128fbb165 | |||
| 8473221c13 | |||
| e7554fa2cc | |||
| 7c1fabf58a | |||
| 1203b6cbd1 | |||
| 525f2906fb | |||
| b105382abf | |||
| b1cdec7d17 | |||
| f1d512b2e5 | |||
| db21b43120 |
@@ -2,37 +2,18 @@
|
|||||||
name: 🐛 Bug Report
|
name: 🐛 Bug Report
|
||||||
about: Erstelle einen Bericht, um uns zu helfen, das Projekt zu verbessern.
|
about: Erstelle einen Bericht, um uns zu helfen, das Projekt zu verbessern.
|
||||||
title: '[BUG] '
|
title: '[BUG] '
|
||||||
labels: bug
|
labels: Problem
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Beschreibung**
|
**Beschreibung**
|
||||||
Eine klare und prägnante Beschreibung des Fehlers.
|
|
||||||
|
|
||||||
**Reproduktion**
|
**Reproduktion**
|
||||||
Schritte, um den Fehler zu reproduzieren:
|
|
||||||
|
|
||||||
Entweder:
|
|
||||||
1. Gehe zu '...'
|
|
||||||
2. Klicke auf '...'
|
|
||||||
3. Scrolle runter zu '...'
|
|
||||||
4. Siehe Fehler
|
|
||||||
|
|
||||||
Oder Link zur Seite
|
|
||||||
|
|
||||||
**Erwartetes Verhalten**
|
|
||||||
Eine klare Beschreibung dessen, was du erwartet hast.
|
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
Falls zutreffend, füge hier Screenshots oder Gifs hinzu, um das Problem zu verdeutlichen.
|
|
||||||
|
|
||||||
**Achtung: Achte bitte auf Datenschutz deiner Daten sowie der Daten deiner Kunden. Sollten ein Screenshot nur mit Daten möglich sein, schwärze diese bitte vor dem Upload.**
|
**Achtung: Achte bitte auf Datenschutz deiner Daten sowie der Daten deiner Kunden. Sollten ein Screenshot nur mit Daten möglich sein, schwärze diese bitte vor dem Upload.**
|
||||||
|
|
||||||
**Umgebung:**
|
|
||||||
- Betriebssystem: [z.B. Windows, macOS, Linux]
|
|
||||||
- Browser / Version (falls relevant): [z.B. Chrome 120]
|
|
||||||
- Projekt-Version: [z.B. v1.0.2]
|
|
||||||
|
|
||||||
**Zusätzlicher Kontext**
|
|
||||||
Füge hier alle anderen Informationen zum Problem hinzu.
|
|
||||||
@@ -2,19 +2,16 @@
|
|||||||
name: ✨ Feature Request
|
name: ✨ Feature Request
|
||||||
about: Schlage eine Idee für dieses Projekt vor.
|
about: Schlage eine Idee für dieses Projekt vor.
|
||||||
title: '[FEATURE] '
|
title: '[FEATURE] '
|
||||||
labels: enhancement
|
labels: Funktionswunsch
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Ist dein Feature-Wunsch mit einem Problem verbunden?**
|
**Ist dein Feature-Wunsch mit einem Problem verbunden?**
|
||||||
Eine klare Beschreibung des Problems (z.B. "Ich bin immer genervt, wenn...").
|
|
||||||
|
|
||||||
**Lösungsvorschlag**
|
**Lösungsvorschlag**
|
||||||
Eine klare Beschreibung dessen, was du dir wünschst und wie es funktionieren soll.
|
|
||||||
|
|
||||||
**Alternativen**
|
**Alternativen**
|
||||||
Hast du über alternative Lösungen oder Workarounds nachgedacht?
|
|
||||||
|
|
||||||
**Zusätzlicher Kontext**
|
|
||||||
Hier ist Platz für weitere Informationen, Skizzen oder Beispiele von anderen Tools.
|
|
||||||
@@ -5,7 +5,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
import {useFunctions} from "~/composables/useFunctions.js";
|
import {useFunctions} from "~/composables/useFunctions.js";
|
||||||
import EntityModalButtons from "~/components/EntityModalButtons.vue";
|
import EntityModalButtons from "~/components/EntityModalButtons.vue";
|
||||||
import { documentTemplateHandlebars } from "~/utils/handlebars";
|
import { documentTemplateHandlebars } from "~/utils/handlebars";
|
||||||
import { getTitleSectionMove, moveTitleSection } from "~/utils/documentTitleSections.js";
|
|
||||||
|
|
||||||
const dataStore = useDataStore()
|
const dataStore = useDataStore()
|
||||||
const profileStore = useProfileStore()
|
const profileStore = useProfileStore()
|
||||||
@@ -1018,24 +1017,6 @@ const toggleTitleSection = (rowId) => {
|
|||||||
collapsedTitleRowIds.value = nextCollapsedTitleRowIds
|
collapsedTitleRowIds.value = nextCollapsedTitleRowIds
|
||||||
}
|
}
|
||||||
|
|
||||||
const moveDocumentTitleSection = (rowId, direction) => {
|
|
||||||
if (itemInfo.value.type === 'cancellationInvoices') return
|
|
||||||
const rows = moveTitleSection(itemInfo.value.rows, rowId, direction)
|
|
||||||
if (rows === itemInfo.value.rows) return
|
|
||||||
itemInfo.value.rows = rows
|
|
||||||
setPosNumbers()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTitleSectionMoveMenuItems = (rowId) => [
|
|
||||||
{ label: 'Titelbereich nach oben', icon: 'i-heroicons-arrow-up', direction: 'up' },
|
|
||||||
{ label: 'Titelbereich nach unten', icon: 'i-heroicons-arrow-down', direction: 'down' }
|
|
||||||
].map(({ direction, ...item }) => ({
|
|
||||||
...item,
|
|
||||||
disabled: itemInfo.value.type === 'cancellationInvoices'
|
|
||||||
|| !getTitleSectionMove(itemInfo.value.rows, rowId, direction),
|
|
||||||
onSelect: () => moveDocumentTitleSection(rowId, direction)
|
|
||||||
}))
|
|
||||||
|
|
||||||
const hiddenDocumentRowIds = computed(() => {
|
const hiddenDocumentRowIds = computed(() => {
|
||||||
const hiddenRowIds = new Set()
|
const hiddenRowIds = new Set()
|
||||||
const collapsedTitleLevels = []
|
const collapsedTitleLevels = []
|
||||||
@@ -3135,7 +3116,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
|||||||
v-model="itemInfo.rows"
|
v-model="itemInfo.rows"
|
||||||
handle=".handle"
|
handle=".handle"
|
||||||
tag="tbody"
|
tag="tbody"
|
||||||
itemKey="id"
|
itemKey="pos"
|
||||||
@end="setPosNumbers"
|
@end="setPosNumbers"
|
||||||
>
|
>
|
||||||
<template #item="{element: row}">
|
<template #item="{element: row}">
|
||||||
@@ -3160,21 +3141,6 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
|||||||
@click="toggleTitleSection(row.id)"
|
@click="toggleTitleSection(row.id)"
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
<UDropdownMenu
|
|
||||||
v-if="row.mode === 'title' && itemInfo.type !== 'cancellationInvoices'"
|
|
||||||
:items="getTitleSectionMoveMenuItems(row.id)"
|
|
||||||
:content="{ align: 'start' }"
|
|
||||||
>
|
|
||||||
<UTooltip text="Titel mit untergeordneten Zeilen verschieben">
|
|
||||||
<UButton
|
|
||||||
class="shrink-0"
|
|
||||||
variant="ghost"
|
|
||||||
color="neutral"
|
|
||||||
icon="i-heroicons-arrows-up-down"
|
|
||||||
aria-label="Titel mit untergeordneten Zeilen verschieben"
|
|
||||||
/>
|
|
||||||
</UTooltip>
|
|
||||||
</UDropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
const { test } = require('node:test')
|
|
||||||
const assert = require('node:assert/strict')
|
|
||||||
const fs = require('node:fs')
|
|
||||||
const vm = require('node:vm')
|
|
||||||
const ts = require('typescript')
|
|
||||||
const { parse, compileScript } = require('@vue/compiler-sfc')
|
|
||||||
const { ref, computed } = require('vue')
|
|
||||||
|
|
||||||
const helpers = import('../utils/documentTitleSections.js')
|
|
||||||
const title = (id, titleLevel = 1) => ({ id, mode: 'title', titleLevel, text: id })
|
|
||||||
const position = (id, mode = 'free') => ({ id, mode, quantity: 2, price: 10, linkedEntitys: [42] })
|
|
||||||
const ids = rows => Array.from(rows, row => row.id)
|
|
||||||
|
|
||||||
test('moving a top-level title carries every nested title and row, preserving data and order', async () => {
|
|
||||||
const { moveTitleSection } = await helpers
|
|
||||||
const rows = [position('intro', 'text'), title('a'), position('article', 'normal'),
|
|
||||||
title('a.1', 2), position('service', 'service'), title('a.1.1', 3),
|
|
||||||
position('note', 'text'), position('break', 'pagebreak'), title('b'), position('last')]
|
|
||||||
const before = JSON.stringify(rows)
|
|
||||||
const moved = moveTitleSection(rows, 'a', 'down')
|
|
||||||
assert.deepEqual(ids(moved), ['intro', 'b', 'last', 'a', 'article', 'a.1', 'service', 'a.1.1', 'note', 'break'])
|
|
||||||
assert.equal(JSON.stringify(rows), before)
|
|
||||||
for (const row of rows) assert.equal(moved.find(item => item.id === row.id), row)
|
|
||||||
assert.deepEqual(moveTitleSection(moved, 'a', 'up'), rows)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('subtitles exchange complete sibling sections without leaving their parent', async () => {
|
|
||||||
const { moveTitleSection, getTitleSectionMove } = await helpers
|
|
||||||
const rows = [title('a'), position('parent-intro'), title('a.1', 2), title('a.1.1', 3),
|
|
||||||
position('child'), title('a.2', 2), position('sibling'), title('b'), title('b.1', 2)]
|
|
||||||
assert.deepEqual(ids(moveTitleSection(rows, 'a.2', 'up')),
|
|
||||||
['a', 'parent-intro', 'a.2', 'sibling', 'a.1', 'a.1.1', 'child', 'b', 'b.1'])
|
|
||||||
assert.deepEqual(ids(moveTitleSection(rows, 'a.1', 'down')),
|
|
||||||
['a', 'parent-intro', 'a.2', 'sibling', 'a.1', 'a.1.1', 'child', 'b', 'b.1'])
|
|
||||||
for (const [id, direction] of [['a.1', 'up'], ['a.2', 'down'], ['b.1', 'up']]) {
|
|
||||||
assert.equal(getTitleSectionMove(rows, id, direction), null)
|
|
||||||
assert.equal(moveTitleSection(rows, id, direction), rows)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('level-three titles move within their level-two parent', async () => {
|
|
||||||
const { moveTitleSection } = await helpers
|
|
||||||
const rows = [title('a'), title('a.1', 2), title('x', 3), position('x-row'),
|
|
||||||
title('y', 3), position('y-row'), title('a.2', 2)]
|
|
||||||
const moved = moveTitleSection(rows, 'x', 'down')
|
|
||||||
assert.deepEqual(ids(moved), ['a', 'a.1', 'y', 'y-row', 'x', 'x-row', 'a.2'])
|
|
||||||
assert.equal(moveTitleSection(moved, 'x', 'down'), moved)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('empty titles, the final section and legacy levels are supported', async () => {
|
|
||||||
const { moveTitleSection } = await helpers
|
|
||||||
const rows = [{ id: 'legacy', mode: 'title' }, title('empty', '1'), title('last', 1), position('end')]
|
|
||||||
assert.deepEqual(ids(moveTitleSection(rows, 'legacy', 'down')), ['empty', 'legacy', 'last', 'end'])
|
|
||||||
assert.deepEqual(ids(moveTitleSection(rows, 'last', 'up')), ['legacy', 'last', 'end', 'empty'])
|
|
||||||
assert.equal(moveTitleSection(rows, 'last', 'down'), rows)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('missing intermediate levels still retain their enclosing section', async () => {
|
|
||||||
const { moveTitleSection } = await helpers
|
|
||||||
const rows = [title('a'), title('x', 3), position('x-row'), title('y', 3), title('b')]
|
|
||||||
assert.deepEqual(ids(moveTitleSection(rows, 'x', 'down')), ['a', 'y', 'x', 'x-row', 'b'])
|
|
||||||
assert.equal(moveTitleSection(rows, 'x', 'up'), rows)
|
|
||||||
assert.equal(moveTitleSection(rows, 'y', 'down'), rows)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('invalid requests and document boundaries leave rows untouched', async () => {
|
|
||||||
const { moveTitleSection } = await helpers
|
|
||||||
const rows = [position('intro'), title('only'), position('last')]
|
|
||||||
for (const [id, direction] of [['only', 'up'], ['only', 'down'], ['intro', 'down'],
|
|
||||||
['missing', 'up'], ['only', 'sideways']]) {
|
|
||||||
assert.equal(moveTitleSection(rows, id, direction), rows)
|
|
||||||
}
|
|
||||||
const empty = []
|
|
||||||
assert.equal(moveTitleSection(empty, 'missing', 'up'), empty)
|
|
||||||
})
|
|
||||||
|
|
||||||
async function editorFixture(rows, type = 'invoices') {
|
|
||||||
const { descriptor, errors } = parse(fs.readFileSync(`${__dirname}/../pages/createDocument/edit/[[id]].vue`, 'utf8'))
|
|
||||||
assert.deepEqual(errors, [])
|
|
||||||
compileScript(descriptor, { id: 'document-editor', inlineTemplate: true })
|
|
||||||
const script = ts.createSourceFile('editor.js', descriptor.scriptSetup.content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS)
|
|
||||||
const names = new Set(['moveDocumentTitleSection', 'getTitleSectionMoveMenuItems', 'setPosNumbers',
|
|
||||||
'collapsedTitleRowIds', 'toggleTitleSection', 'hiddenDocumentRowIds', 'isDocumentRowHidden'])
|
|
||||||
const source = script.statements.filter(statement => ts.isVariableStatement(statement)
|
|
||||||
&& statement.declarationList.declarations.some(declaration => names.has(declaration.name.getText(script))))
|
|
||||||
.map(statement => statement.getText(script)).join('\n')
|
|
||||||
const context = vm.createContext({ ...await helpers, ref, computed, itemInfo: ref({ type, rows }) })
|
|
||||||
vm.runInContext(source, context)
|
|
||||||
return { context, run: code => vm.runInContext(code, context) }
|
|
||||||
}
|
|
||||||
|
|
||||||
test('the editor menu moves collapsed sections and updates numbering and reactive visibility', async () => {
|
|
||||||
const { run, context } = await editorFixture([title('a'), title('a.1', 2), position('a-row'),
|
|
||||||
title('b'), position('b-row')])
|
|
||||||
run("setPosNumbers(); toggleTitleSection('a'); toggleTitleSection('a.1')")
|
|
||||||
assert.equal(run("isDocumentRowHidden('a-row')"), true)
|
|
||||||
assert.equal(run("getTitleSectionMoveMenuItems('a')[0].disabled"), true)
|
|
||||||
assert.equal(run("getTitleSectionMoveMenuItems('a')[1].disabled"), false)
|
|
||||||
run("getTitleSectionMoveMenuItems('a')[1].onSelect()")
|
|
||||||
assert.deepEqual(ids(context.itemInfo.value.rows), ['b', 'b-row', 'a', 'a.1', 'a-row'])
|
|
||||||
assert.deepEqual(Array.from(context.itemInfo.value.rows, row => row.pos), ['1', '1.1', '2', '2.1', '2.1.1'])
|
|
||||||
assert.equal(run("isDocumentRowHidden('b-row')"), false)
|
|
||||||
assert.equal(run("isDocumentRowHidden('a-row')"), true)
|
|
||||||
run("toggleTitleSection('a')")
|
|
||||||
assert.equal(run("isDocumentRowHidden('a.1')"), false)
|
|
||||||
assert.equal(run("isDocumentRowHidden('a-row')"), true)
|
|
||||||
run("toggleTitleSection('a.1')")
|
|
||||||
assert.equal(run("isDocumentRowHidden('a-row')"), false)
|
|
||||||
assert.equal(run("getTitleSectionMoveMenuItems('a')[1].disabled"), true)
|
|
||||||
assert.equal(run("getTitleSectionMoveMenuItems('a')[0].disabled"), false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('cancellation documents reject section moves even when called directly', async () => {
|
|
||||||
const { run, context } = await editorFixture([title('a'), position('a-row'), title('b')], 'cancellationInvoices')
|
|
||||||
const before = JSON.stringify(context.itemInfo.value.rows)
|
|
||||||
assert.equal(run("getTitleSectionMoveMenuItems('a').every(item => item.disabled)"), true)
|
|
||||||
run("moveDocumentTitleSection('a', 'down')")
|
|
||||||
assert.equal(JSON.stringify(context.itemInfo.value.rows), before)
|
|
||||||
})
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
const titleLevel = row => Math.min(Math.max(Number(row.titleLevel) || 1, 1), 3)
|
|
||||||
|
|
||||||
// A section includes every row up to the next title at the same or a higher level.
|
|
||||||
const sectionEnd = (rows, start) => {
|
|
||||||
const level = titleLevel(rows[start])
|
|
||||||
let end = start + 1
|
|
||||||
while (end < rows.length && (rows[end].mode !== 'title' || titleLevel(rows[end]) > level)) end += 1
|
|
||||||
return end
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getTitleSectionMove = (rows, rowId, direction) => {
|
|
||||||
if (!['up', 'down'].includes(direction)) return null
|
|
||||||
const start = rows.findIndex(row => row.id === rowId)
|
|
||||||
if (start < 0 || rows[start].mode !== 'title') return null
|
|
||||||
|
|
||||||
const level = titleLevel(rows[start])
|
|
||||||
const end = sectionEnd(rows, start)
|
|
||||||
if (direction === 'down') {
|
|
||||||
// A title at a higher level ends the parent section: never cross it.
|
|
||||||
if (end === rows.length || titleLevel(rows[end]) !== level) return null
|
|
||||||
return { start, end, target: sectionEnd(rows, end) }
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let previous = start - 1; previous >= 0; previous -= 1) {
|
|
||||||
if (rows[previous].mode !== 'title' || titleLevel(rows[previous]) > level) continue
|
|
||||||
if (titleLevel(rows[previous]) < level) return null
|
|
||||||
return { start, end, target: previous }
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const moveTitleSection = (rows, rowId, direction) => {
|
|
||||||
const move = getTitleSectionMove(rows, rowId, direction)
|
|
||||||
if (!move) return rows
|
|
||||||
|
|
||||||
const { start, end, target } = move
|
|
||||||
const result = rows.slice()
|
|
||||||
const section = result.splice(start, end - start)
|
|
||||||
result.splice(target > start ? target - section.length : target, 0, ...section)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user