feat/edit steps (#7)

* refactor: ♻️ edit steps

* add md5 hash lib

* now step ids are generated based on titles and estimation

* feat:  edit steps

now add steps will be a modal for editing all steps

* chore(commitizen): init cz changelog

* chore(changelog): init git cliff

* feat(edit steps): add a message to alert on the fact that the record may change

* feat(task): task link is a normal button now

* with the good ids

* remove column

* autofocus to title

* --wip-- [skip ci]

* lint

* can modify the whole task when recording
This commit is contained in:
Julien Calixte
2024-04-09 00:06:07 +02:00
committed by GitHub
parent 4e83c26233
commit 364d0b2eed
22 changed files with 830 additions and 85 deletions

View File

@@ -6,7 +6,7 @@ import { logicAnd } from '@vueuse/math'
import { computed, onUnmounted, ref } from 'vue'
import type { TaskRecord } from '../models/task-record'
import { useTaskRecordStore } from '../stores/useTaskRecordStore'
import NewStepsFormVue from '@/modules/task/components/NewStepsForm.vue'
import NewStepsForm from '@/modules/task/components/NewStepsForm.vue'
import type { Stepable } from '@/modules/task/interfaces/stepable'
const props = defineProps<{
@@ -78,17 +78,17 @@ const nextStep = () => {
}
const isAddingSteps = ref(false)
const addStepsForm = () => {
const editStepsForm = () => {
isAddingSteps.value = true
}
const addSteps = (steps: Stepable[]) => {
const editSteps = (steps: Stepable[]) => {
if (!record.value.currentStepId) {
return
}
isAddingSteps.value = false
taskStore.addStepsToTask(props.taskId, steps, record.value.currentStepId)
taskStore.editStepsToTask(props.taskId, steps)
}
const reset = () => {
@@ -170,7 +170,7 @@ onUnmounted(() => {
<button
v-if="!record.end && record.currentStepId"
class="button is-primary is-light"
@click="addStepsForm"
@click="editStepsForm"
>
<img src="/icons/plus.svg" alt="plus" />
</button>
@@ -186,10 +186,12 @@ onUnmounted(() => {
<p><kbd>p</kbd>: pause</p>
</div>
</div>
<NewStepsFormVue
<NewStepsForm
v-if="task"
:is-active="isAddingSteps"
:initial-steps="task.steps"
@close="isAddingSteps = false"
@submit="addSteps"
@submit="editSteps"
/>
</template>

View File

@@ -71,7 +71,7 @@ const isOffEstimation = computed(() => {
<template>
<tr v-if="step" class="step-record">
<td class="td-rank" :class="{ 'added-afterward': step.addedAfterward }">
<td class="td-rank">
<div class="rank">
<template v-if="isCurrentStep">
<img
@@ -121,12 +121,6 @@ $blob-color: $link;
}
}
.td-rank {
&.added-afterward {
background: #fbc124;
}
}
.rank {
text-align: right;
display: flex;

View File

@@ -188,6 +188,33 @@ export const useTaskRecordStore = defineStore('task-record-store', {
}
}
})
},
cleanCurrentStepId(task: Task) {
const record = this.records[task.id]
if (!record) {
return
}
const nextStepIndex = task.steps.findIndex(
(step) =>
!Object.keys(record.stepRecords).find((stepId) => stepId === step.id)
)
if (nextStepIndex >= 0) {
task.steps
.filter((_, index) => index > nextStepIndex)
.map((step) => step.id)
.forEach((stepId) => {
delete record.stepRecords[stepId]
})
this.startStepRecord({
taskId: task.id,
stepId: task.steps[nextStepIndex].id,
start: toISODate(new Date())
})
}
}
},
getters: {

View File

@@ -12,7 +12,6 @@ defineProps<{
<thead>
<tr>
<th>#</th>
<th>status</th>
<th>task</th>
<th>estimation</th>
</tr>

View File

@@ -38,7 +38,7 @@ const save = () => {
<step-input v-if="isActive" v-model="steps" size="small" />
</section>
<footer class="modal-card-foot">
<button class="button is-primary" @click="save">add</button>
<button class="button is-primary" @click="save">edit</button>
<button class="button" @click="$emit('close')">cancel</button>
</footer>
</div>

View File

@@ -1,20 +1,26 @@
<script setup lang="ts">
import type { Stepable } from '@/modules/task/interfaces/stepable'
import { ref } from 'vue'
import { ref, watch } from 'vue'
import StepInput from './StepInput.vue'
defineProps<{
const props = defineProps<{
isActive: boolean
initialSteps: Stepable[]
}>()
const emits = defineEmits<{
(event: 'submit', steps: Stepable[]): void
(event: 'close'): void
}>()
const steps = ref<Stepable[]>([])
const steps = ref<Stepable[]>(props.initialSteps)
watch(props.initialSteps, (initialSteps) => {
steps.value = initialSteps
})
const save = () => {
emits('submit', steps.value)
steps.value = []
}
</script>
@@ -23,7 +29,7 @@ const save = () => {
<div class="modal-background" @click="$emit('close')"></div>
<div class="new-step-form modal-card">
<header class="modal-card-head">
<p class="modal-card-title">New steps</p>
<p class="modal-card-title">Edit steps</p>
<button
class="delete"
aria-label="close"
@@ -31,7 +37,12 @@ const save = () => {
></button>
</header>
<section class="modal-card-body">
<step-input v-if="isActive" v-model="steps" size="small" />
<section class="message is-info">
<div class="message-body">
Current record will start from the first unfinished step.
</div>
</section>
<step-input v-if="isActive" v-model="steps" size="large" />
</section>
<footer class="modal-card-foot">
<button class="button is-primary" @click="save">add</button>

View File

@@ -68,7 +68,13 @@ const isValid = computed(() => title.value && steps.value.length > 0)
<div class="field">
<label class="label" for="title">Title</label>
<div class="control">
<input class="input" type="text" id="title" v-model="title" />
<input
class="input"
type="text"
id="title"
v-model="title"
autofocus
/>
</div>
</div>
<div class="field">

View File

@@ -1,35 +1,10 @@
import { adaptTextareaToSteps } from '@/modules/task/infra/adaptStepsToTextarea'
import type { Stepable } from '@/modules/task/interfaces/stepable'
import { createUuid } from '@/shared/create-uuid'
export const exampleSteps: Stepable[] = [
{
id: createUuid(),
title: 'create math.test file, test add function for simple cases',
estimation: 3
},
{
id: createUuid(),
title: 'create the math file, implement add function',
estimation: 3
},
{
id: createUuid(),
title: 'commit',
estimation: 1
},
{
id: createUuid(),
title: 'TDD for the multiply function',
estimation: 8
},
{
id: createUuid(),
title: 'write documentation',
estimation: 10
},
{
id: createUuid(),
title: 'commit, push and create the PR',
estimation: 5
}
]
export const exampleSteps: Stepable[] =
adaptTextareaToSteps(`create math.test file, test add function for simple cases | 3
create the math file, implement add function | 3
commit | 1
TDD for the multiply function | 8
write documentation | 10
commit, push and create the PR | 5`)

View File

@@ -71,4 +71,26 @@ describe('adapt steps to textarea value', () => {
expect(adaptTextareaToSteps(stepInTextArea)).toEqual([])
})
it('creates generated ids based on title and estimation', () => {
const stepInTextarea = '- step 1 | 3'
const [step] = adaptTextareaToSteps(stepInTextarea)
expect(step.id).toMatchInlineSnapshot(`"66f312736335fce1df9a8b95c7be3fce-1"`)
})
it('creates generated ids based on title and estimation and indexes when duplicated', () => {
const stepInTextarea = `- step duplicated | 3
- step duplicated | 3`
const [step1, step2] = adaptTextareaToSteps(stepInTextarea)
expect(step1.id).toMatchInlineSnapshot(
`"9b237c28d5254f2b819fa66c853a9a60-1"`
)
expect(step2.id).toMatchInlineSnapshot(
`"9b237c28d5254f2b819fa66c853a9a60-2"`
)
})
})

View File

@@ -1,4 +1,4 @@
import { createUuid } from '@/shared/create-uuid'
import { generateId } from '@/shared/generate-id'
import type { Stepable } from '../interfaces/stepable'
export const adaptStepsToTextarea = (steps: Stepable[]) =>
@@ -33,6 +33,12 @@ export const adaptTextareaToSteps = (textareaValue: string): Stepable[] =>
return null
}
return { id: createUuid(), title, estimation }
return { id: generateId(`${title}-${estimation}`), title, estimation }
})
.filter((step): step is Stepable => step !== null)
.map((step, index, steps) => {
const subSteps = steps.slice(0, index + 1)
const duplicates = subSteps.filter((s) => s.id === step.id).length
return { ...step, id: `${step.id}-${duplicates}` }
})
.filter((step) => step !== null) as Stepable[]

View File

@@ -5,5 +5,4 @@ export interface Stepable {
* estimation in minutes
*/
estimation: number
addedAfterward?: boolean
}

View File

@@ -11,4 +11,4 @@ export const fixtureTask = (
new Task(
partialTask?.id ?? faker.string.uuid(),
partialTask?.title ?? faker.animal.bird()
).addSteps(...(steps ?? fixtureStep()))
).editSteps(...(steps ?? fixtureStep()))

View File

@@ -32,7 +32,7 @@ describe('Task', () => {
const [firstStep, secondStep] = [fixtureStep(), fixtureStep()]
task.addSteps(firstStep, secondStep)
task.editSteps(firstStep, secondStep)
expect(task.steps).toEqual([firstStep, secondStep])
@@ -47,14 +47,14 @@ describe('Task', () => {
const task = new Task(faker.string.uuid(), faker.color.human())
expect(Task.validate(task)).toEqual(false)
task.addSteps(fixtureStep())
task.editSteps(fixtureStep())
expect(Task.validate(task)).toEqual(true)
})
it('calculates the total estimation of steps', () => {
const task = new Task(faker.string.uuid(), faker.color.human())
task.addSteps(
task.editSteps(
fixtureStep({ estimation: 1 }),
fixtureStep({ estimation: 2 }),
fixtureStep({ estimation: 3 })
@@ -84,7 +84,7 @@ describe('Task', () => {
expect(task.wasUpdated).toEqual(false)
task.addSteps(fixtureStep())
task.editSteps(fixtureStep())
expect(task.wasUpdated).toEqual(true)
})
})

View File

@@ -27,7 +27,7 @@ export class Task implements Taskable {
return this.stepHistory.length > 0
}
public addSteps(...steps: Stepable[]) {
public editSteps(...steps: Stepable[]) {
this.stepHistory.push([...this.steps, ...steps])
return this
}

View File

@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import type { Stepable } from '../interfaces/stepable'
import type { Taskable } from '../interfaces/taskable'
import { Task } from '../models/task'
import { useTaskRecordStore } from '@/modules/record/stores/useTaskRecordStore'
export interface TaskStoreState {
tasks: Taskable[]
@@ -17,27 +18,16 @@ export const useTaskStore = defineStore('task-store', {
this.remove(task.id)
this.tasks.push(task)
},
addStepsToTask(taskId: string, steps: Stepable[], fromStepId: string) {
editStepsToTask(taskId: string, steps: Stepable[]) {
this.tasks = this.tasks.map((task) => {
if (task.id !== taskId) {
return task
}
const fromStepIndex = Task.fromTaskable(task).steps.findIndex(
(s) => s.id === fromStepId
)
if (fromStepIndex < 0) {
return task
}
const newTask = Task.fromTaskable(task)
newTask.newSteps([
...newTask.steps.slice(0, fromStepIndex + 1),
...steps.map((step) => ({ ...step, addedAfterward: true })),
...newTask.steps.slice(fromStepIndex + 1)
])
newTask.newSteps(steps)
useTaskRecordStore().cleanCurrentStepId(newTask)
return newTask
})

View File

@@ -0,0 +1,3 @@
import hash from 'md5'
export const generateId = (seed: string): string => hash(seed)

View File

@@ -76,7 +76,7 @@ const { canShareTask, taskCopied, shareTask } = useCopyRecord(task)
:href="task.link"
target="_blank"
rel="noopener noreferrer"
class="button is-link"
class="button"
>Task link</a
>
<div class="content" :id="`task-${id}`">