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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -12,7 +12,6 @@ defineProps<{
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>status</th>
|
||||
<th>task</th>
|
||||
<th>estimation</th>
|
||||
</tr>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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"`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -5,5 +5,4 @@ export interface Stepable {
|
||||
* estimation in minutes
|
||||
*/
|
||||
estimation: number
|
||||
addedAfterward?: boolean
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
3
src/shared/generate-id.ts
Normal file
3
src/shared/generate-id.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import hash from 'md5'
|
||||
|
||||
export const generateId = (seed: string): string => hash(seed)
|
||||
@@ -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}`">
|
||||
|
||||
Reference in New Issue
Block a user