From ffe63544dbb40ab86cb856226592850309da1f25 Mon Sep 17 00:00:00 2001 From: "jongyi kim (hibiya)" Date: Fri, 14 Mar 2025 13:55:20 +0900 Subject: [PATCH 1/6] page: fix tests --- jest.config.js | 3 +++ src/__svg_mock.ts | 1 + src/layout/legacy.ts | 2 +- src/page/actions/index.ts | 2 +- src/page/test/Layer.spec.ts | 1 + src/page/test/actions/describe-action.ts | 27 +++++++++++++++++------- src/svg.d.ts | 9 ++++++++ tsconfig.json | 3 ++- 8 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 src/__svg_mock.ts create mode 100644 src/svg.d.ts diff --git a/jest.config.js b/jest.config.js index 4b7c85ab..8a117493 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,6 +20,9 @@ module.exports = { '/src/**/*.spec.jsx', '/src/**/*.spec.tsx', ], transformIgnorePatterns: ['/node_modules/'], + moduleNameMapper: { + '\\.svg(\\?raw)?$': '/src/__svg_mock.ts' + }, resetMocks: true }; diff --git a/src/__svg_mock.ts b/src/__svg_mock.ts new file mode 100644 index 00000000..61bae3eb --- /dev/null +++ b/src/__svg_mock.ts @@ -0,0 +1 @@ +export default '' diff --git a/src/layout/legacy.ts b/src/layout/legacy.ts index d7601913..cef9a138 100644 --- a/src/layout/legacy.ts +++ b/src/layout/legacy.ts @@ -52,7 +52,7 @@ export default class LegacyLayout extends LayoutBase { params: Array | Map, template: string, values: any, - meta: any + meta?: any }) { const createdMeta = meta || { description, icon } diff --git a/src/page/actions/index.ts b/src/page/actions/index.ts index 5540989d..1064693e 100644 --- a/src/page/actions/index.ts +++ b/src/page/actions/index.ts @@ -29,4 +29,4 @@ export default new Map([ Layer_Item_Remove, Layer_Hide // Layer_Clear -].map(module => [ module.id, module ])) +].map(module => [ module.id, module ])) diff --git a/src/page/test/Layer.spec.ts b/src/page/test/Layer.spec.ts index d3c4d08f..a41fdb34 100644 --- a/src/page/test/Layer.spec.ts +++ b/src/page/test/Layer.spec.ts @@ -18,6 +18,7 @@ describe('Composer.Page: Layer', () => { expect(dump).toEqual({ id: 'test1', layout: setup.ListLayoutDefinition.id, + hidden: false, values: { list: [ { param1: 'foo' }, diff --git a/src/page/test/actions/describe-action.ts b/src/page/test/actions/describe-action.ts index eba228fb..8bd48cdb 100644 --- a/src/page/test/actions/describe-action.ts +++ b/src/page/test/actions/describe-action.ts @@ -10,17 +10,24 @@ import { uniqueId } from '../../../util' import Act from '../../../state/act' import Actions from '../../actions' import State from '../../../state' +import type Action from '../../../state/action' import type ActTarget from '../../../state/acttarget' import type Path from '../../path' +import type { Paths } from '../../path' import * as setup from '../setup' import Layout from '../../../layout/legacy' import Page from '../..' -const createHelper = ({ actionsMap, actionName }) => ({ +type GetActionT> = C extends Action ? T : never +type KeyOfMap> = M extends Map ? K : never +type ValueInMapRecord, K> = M extends Map ? V : never + +const createHelper = ({ actionsMap, actionName }: { actionsMap: typeof Actions, actionName: string }) => ({ mocked: { uniqueId: >jest.mocked(uniqueId) }, + createState(layouts: Layout | Map = setup.MinimalLayouts): [ Page, State ] { if(layouts instanceof Layout) layouts = new Map([ [ layouts.id, layouts ] ]) @@ -29,16 +36,20 @@ const createHelper = ({ actionsMap, actionName }) => ({ const state = new State({ modules: { page }}) return [ page, state ] }, - createAct( // << extends new (arg0: infer Arg0, ...rest: infer R) => any ? [Arg0, ...R] | R : never - ): Act { + + createAct( // << extends new (arg0: infer Arg0, ...rest: infer R) => any? [Arg0, ...R] | R : never + ): Act { // ¯\_(ツ)_/¯ if(!(typeof args[0] === 'string' && actionsMap.has(args[0]))) args.unshift(actionName) - return new Act(...>>args) + const action = actionsMap.get(actionName) + type ActionT = GetActionT> + return new Act(...>>args) }, - checkTimeParadox(state: State, assertions: Array) { + + checkTimeParadox(state: State, assertions: Array>) { const runAct = act => { if(act instanceof Function) act = act() @@ -100,8 +111,8 @@ function describeAction( } const actions = [ actionName, ...dependentActions ].map(action => - [ action, Actions.get(action) - ]) + [ action, Actions.get(action) ] + ) const notfound = actions.filter(l => l[1] == null).map(l => l[0]).join(', ') if(notfound) throw new ReferenceError(`following actions requested, \ diff --git a/src/svg.d.ts b/src/svg.d.ts new file mode 100644 index 00000000..3ed0d2ee --- /dev/null +++ b/src/svg.d.ts @@ -0,0 +1,9 @@ +declare module '*.svg?raw' { + const content: string; + export default content; +} + +declare module '*.svg' { + const content: string; + export default content; +} diff --git a/tsconfig.json b/tsconfig.json index 60ffc46a..5c34dbce 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,8 @@ "outDir": "./dist" }, "include": [ - "src/**/*.ts" + "src/**/*.ts", + "src/**/*.d.ts" ], "exclude": [ "./dist" ] } From 98da4f3305332ab8d3c42b9c2f94b4de7e12c155 Mon Sep 17 00:00:00 2001 From: "jongyi kim (hibiya)" Date: Fri, 14 Mar 2025 13:57:06 +0900 Subject: [PATCH 2/6] page: Layer.Remove: support multiple paths to be removed --- src/page/actions/Layer.Remove.ts | 48 +++++++++++----- src/page/test/actions/Layer.Remove.spec.ts | 65 +++++++++++++++++++++- 2 files changed, 97 insertions(+), 16 deletions(-) diff --git a/src/page/actions/Layer.Remove.ts b/src/page/actions/Layer.Remove.ts index f1035dfe..c470c6c3 100644 --- a/src/page/actions/Layer.Remove.ts +++ b/src/page/actions/Layer.Remove.ts @@ -1,34 +1,56 @@ import type Action from '../../state/action' import type Page from '..' +import type Path from '../path' -import Path from '../path' +import { Paths } from '../path' import icon from './icons/Layer.Remove.svg?raw' -export default >{ +export default >{ id: 'layer.remove', title: '레이어 삭제', icon, perform(root, self, act) { const path = act.target + const paths = path.type === 'path' ? [path] : path.paths - const oldLayer = self.getLayerByPath(path) - if(!oldLayer) - throw new Error(`layer requested to removal couldn't be found (${path.toString()})`) + const sortedPaths = [...paths].sort((a, b) => { + const indexA = self.pathToIndex(a) ?? 0 + const indexB = self.pathToIndex(b) ?? 0 + return indexB - indexA + }) - const removedIndex = self.removeLayer(path) ?? 0 - // let moveFocusTo: Path | undefined - // if(self.state.length > removedIndex) - // moveFocusTo = self.state[removedIndex - 1]?.path + const removedLayers = [] + const pathsBeforeRemoved = [] - const pathBeforeRemoved = self.indexToPath(removedIndex) + act.meta = self.describe(path) + + for (const path of sortedPaths) { + const oldLayer = self.getLayerByPath(path) + if (!oldLayer) + throw new Error(`layer requested to removal couldn't be found (${path.toString()})`) + + const removedIndex = self.removeLayer(path) ?? 0 + const pathBeforeRemoved = self.indexToPath(removedIndex) + + removedLayers.push(oldLayer) + pathsBeforeRemoved.push(pathBeforeRemoved) + } - // self.moveFocus(moveFocusTo) self.setFocus() - return act.remember({ layer: oldLayer }, pathBeforeRemoved) // , moveFocusTo) + + return act.remember({ + layers: removedLayers, + positions: pathsBeforeRemoved + }, path.type === 'path' ? paths[0] : new Paths(paths)) }, rollback(root, self, { target, destination, capturedState }) { + const { layers, positions } = capturedState + + for (let i = layers.length - 1; i >= 0; i--) { + self.restoreLayer(positions[i], layers[i]) + } + self.setFocus(target) - self.restoreLayer(destination!, capturedState.layer) } } diff --git a/src/page/test/actions/Layer.Remove.spec.ts b/src/page/test/actions/Layer.Remove.spec.ts index f85847f8..1722ab4d 100644 --- a/src/page/test/actions/Layer.Remove.spec.ts +++ b/src/page/test/actions/Layer.Remove.spec.ts @@ -4,18 +4,17 @@ import * as setup from '../setup' import type Act from '../../../state/act' import Path from '../../path' +import { Paths } from '../../path' describeAction('layer.remove', ['layer.new'], helpers => { - it('should work: do, undo', () => { + it('should work with single layer: do, undo', () => { const [ page, state ] = helpers.createState() let actNew = helpers.createAct('layer.new', null, setup.MinimalLayout.id) helpers.mocked.uniqueId.mockReturnValueOnce('a') actNew = state.perform(actNew) - console.log(page.state) - expect(actNew.destination.layer).toBe('a') helpers.checkTimeParadox(state, [ @@ -30,10 +29,70 @@ describeAction('layer.remove', ['layer.new'], helpers => { ]) }) + it('should work with multiple layers: do, undo', () => { + const [ page, state ] = helpers.createState() + + // Create 3 layers + const actA = helpers.createAct('layer.new', null, setup.MinimalLayout.id) + helpers.mocked.uniqueId.mockReturnValueOnce('a') + state.perform(actA) + const pathA = actA.destination + + const actB = helpers.createAct('layer.new', null, setup.MinimalLayout.id) + helpers.mocked.uniqueId.mockReturnValueOnce('b') + state.perform(actB) + const pathB = actB.destination + + const actC = helpers.createAct('layer.new', null, setup.MinimalLayout.id) + helpers.mocked.uniqueId.mockReturnValueOnce('c') + state.perform(actC) + const pathC = actC.destination + + const paths = new Paths([pathA, pathB]) + + helpers.checkTimeParadox(state, [ + function before() { + expect(page.state.length).toEqual(3) + expect(page.state.map(l => l.id)).toEqual(['a', 'b', 'c']) + }, + helpers.createAct(paths), + function after() { + expect(page.state.length).toEqual(1) + expect(page.state[0].id).toBe('c') + } + ]) + }) + + it('should maintain correct order when removing multiple non-adjacent layers', () => { + const [ page, state ] = helpers.createState() + + // Create 5 layers + const layers = ['a', 'b', 'c', 'd', 'e'].map(id => { + const act = helpers.createAct('layer.new', null, setup.MinimalLayout.id) + helpers.mocked.uniqueId.mockReturnValueOnce(id) + state.perform(act) + return act.destination + }) + + // Remove first and fourth layers (a, d) + const paths = new Paths([layers[3], layers[0]]) + + helpers.checkTimeParadox(state, [ + function before() { + expect(page.state.map(l => l.id)).toEqual(['a', 'b', 'c', 'd', 'e']) + }, + helpers.createAct(paths), + function after() { + expect(page.state.map(l => l.id)).toEqual(['b', 'c', 'e']) + } + ]) + }) + it('should throw on nonexistent layer', () => { const [ page, state ] = helpers.createState(setup.ListLayout) expect(() => state.perform(helpers.createAct(new Path('?')))).toThrowError() + expect(() => state.perform(helpers.createAct(new Paths([new Path('?')])))).toThrowError() }) }) From 33b94ea582de7d0cb4045ad52c3988e5eae5e695 Mon Sep 17 00:00:00 2001 From: "jongyi kim (hibiya)" Date: Fri, 14 Mar 2025 13:57:16 +0900 Subject: [PATCH 3/6] legacy: fix styles --- src/legacy/assets/scss/_pane.scss | 6 +++--- src/legacy/views/history/index.vue | 3 +++ src/legacy/views/history/item.vue | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/legacy/assets/scss/_pane.scss b/src/legacy/assets/scss/_pane.scss index 53858dea..31973f66 100644 --- a/src/legacy/assets/scss/_pane.scss +++ b/src/legacy/assets/scss/_pane.scss @@ -11,14 +11,14 @@ user-select: none; - > .fcc-pane-title { + .fcc-pane-title { display: flex; position: sticky; top: 0; z-index: 999; } - > .fcc-pane-toolbar { + .fcc-pane-toolbar { display: flex; align-items: center; @@ -74,7 +74,7 @@ } } - > .fcc-pane-content { + .fcc-pane-content { min-height: 0; overflow-y: scroll; overscroll-behavior: contain; diff --git a/src/legacy/views/history/index.vue b/src/legacy/views/history/index.vue index 71d95b29..36992d5d 100644 --- a/src/legacy/views/history/index.vue +++ b/src/legacy/views/history/index.vue @@ -88,6 +88,9 @@ export default { @import '../../assets/scss/utils/utilities'; .fcc-history-list { + margin: 0; + padding: 0; + list-style: none; > .fcc-history-item { margin: 0.4rem 0 0.4rem 0.4rem; } diff --git a/src/legacy/views/history/item.vue b/src/legacy/views/history/item.vue index c7f7352e..bdeb98c5 100644 --- a/src/legacy/views/history/item.vue +++ b/src/legacy/views/history/item.vue @@ -46,6 +46,7 @@ export default { .fcc-history-icon { width: 2.4rem; height: 2.4rem; + vertical-align: top; } &.current { box-shadow: 0 0 0 0.2rem $foreground; From f385bc07e5bd2b68bcd49d477086c8c38ebc356c Mon Sep 17 00:00:00 2001 From: "jongyi kim (hibiya)" Date: Fri, 14 Mar 2025 13:57:26 +0900 Subject: [PATCH 4/6] legacy: multiple trash --- src/legacy/views/layers.vue | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/legacy/views/layers.vue b/src/legacy/views/layers.vue index 11bbb93a..7e682d45 100644 --- a/src/legacy/views/layers.vue +++ b/src/legacy/views/layers.vue @@ -35,6 +35,12 @@ :disabled="!checkedCount"> {{ checkedCount && areEveryCheckedLayerVisible? 'visibility_off' : 'visibility' }} + @@ -209,6 +215,10 @@ this.$set(this, 'checked', Object.fromEntries(this.layers.map(_ => [_.id, true]))) } }, + trashChecked() { + const paths = new Paths(this.checkedLayers.map(_ => _.layer.path)) + this.state.act('layer.remove', paths) + }, hideChecked() { const paths = new Paths(this.checkedLayers.map(_ => _.layer.path)) this.state.act('layer.hide', paths) From a8af234c93ad0e8bb8f80aefde9efb621e249971 Mon Sep 17 00:00:00 2001 From: "jongyi kim (hibiya)" Date: Fri, 14 Mar 2025 14:00:25 +0900 Subject: [PATCH 5/6] Release 2.1.6 --- package.json | 2 +- src/legacy/views/changelog.vue | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 97718ad7..0de20888 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@day1co/fastcomposer", - "version": "2.1.5", + "version": "2.1.6", "author": "fastcampus", "main": "./dist/fastcomposer.js", "exports": { diff --git a/src/legacy/views/changelog.vue b/src/legacy/views/changelog.vue index b3c41020..5ca1cec1 100644 --- a/src/legacy/views/changelog.vue +++ b/src/legacy/views/changelog.vue @@ -1,5 +1,18 @@ @@ -21,11 +35,36 @@ export default { default() { return {} }, required: true }, - index: Number + index: Number, + label: String + }, + data() { + return { + isEditing: false, + editableText: '' + } }, computed: { icon() { return iconToUri(this.layout.meta.icon) + }, + canEdit() { + return this.label !== null && this.label !== undefined + } + }, + methods: { + startEditing() { + if(!this.canEdit) + return + this.editableText = this.label + this.isEditing = true + this.$nextTick(() => this.$refs.editInput.focus()) + }, + save() { + if(this.isEditing) { + this.isEditing = false + this.$emit('update:label', this.editableText.trim()) + } } } } @@ -40,6 +79,8 @@ export default { display: flex; align-items: center; + flex-grow: 1; + margin: 0; text-align: left; @@ -65,9 +106,14 @@ export default { } } + &__index { + opacity: 0.5; + } &__label { line-height: 2.2rem; - font-size: 1.6rem; + font-size: 1.8rem; + + flex-grow: 1; white-space: nowrap; text-overflow: clip; @@ -76,10 +122,6 @@ export default { > strong, > span { @include readable-font-features; font-variant-numeric: tabular-nums; - font-size: 1.8rem; - } - > span { - opacity: 0.5; } .fcc-composer__simple-layers & { display: flex; @@ -89,13 +131,33 @@ export default { display: none; } } + + &-input { + @include readable-font-features; + font: inherit; + line-height: inherit; + width: 100%; + padding: 0; + margin: 0; + border: none; + outline: none; + } + + &-text { + display: inline; + font-size: 1.4rem; + + &.fcc-is-label { + font-style: italic; + } + } } &.small &__label { line-height: 1.8rem; font-size: 1.4rem; - > strong, > span { - font-size: 1.6rem; + .fcc-layout-info__id, .fcc-layout-info__index { + font-size: 1.5rem; } } } diff --git a/src/legacy/views/layers.vue b/src/legacy/views/layers.vue index 7e682d45..822c1f37 100644 --- a/src/legacy/views/layers.vue +++ b/src/legacy/views/layers.vue @@ -57,15 +57,15 @@ @@ -91,7 +91,12 @@ @click="e => $nextTick(() => onCheck(layer, e))" /> {{ checked[layer.id]? 'check_box' : 'check_box_outline_blank' }} - +