Feat/initial plan (#6)
* chore: upgrade libs * fix outdated tests * init edit steps form * tasks have now initialPlan * simpler to directly have an history and put initialPlan and steps has getters * consistent test script names * display initial plan to task view * display initial if only it exists (next when only it changes * display initial plan only if it chanegd
This commit is contained in:
63
package.json
63
package.json
@@ -4,52 +4,53 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check build-only",
|
||||
"build": "run-p test:types build-only",
|
||||
"preview": "vite preview",
|
||||
"test": "pnpm lint && pnpm type-check && pnpm test:unit",
|
||||
"test:types": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"test:unit": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test": "pnpm lint && pnpm test:types && pnpm test:unit",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^10.6.1",
|
||||
"@vueuse/math": "^10.6.1",
|
||||
"@vueuse/core": "^10.7.2",
|
||||
"@vueuse/math": "^10.7.2",
|
||||
"bulma": "^0.9.4",
|
||||
"html-to-image": "^1.11.11",
|
||||
"nanoid": "^5.0.3",
|
||||
"nanoid": "^5.0.5",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"suretype": "^3.3.1",
|
||||
"vue": "^3.3.9",
|
||||
"vue": "^3.4.19",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@pinia/testing": "^0.0.16",
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@types/jsdom": "^21.1.1",
|
||||
"@types/node": "^18.14.2",
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"@vitest/browser": "^0.31.0",
|
||||
"@vitest/ui": "^0.31.0",
|
||||
"@vue/eslint-config-prettier": "^7.1.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/test-utils": "^2.3.2",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-plugin-vue": "^9.11.0",
|
||||
"jsdom": "^21.1.1",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@pinia/testing": "^0.1.3",
|
||||
"@rushstack/eslint-patch": "^1.7.2",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@types/jsdom": "^21.1.6",
|
||||
"@types/node": "^18.19.17",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitest/browser": "^1.3.0",
|
||||
"@vitest/ui": "^1.3.0",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/test-utils": "^2.4.4",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.21.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.8",
|
||||
"sass": "^1.62.1",
|
||||
"typescript": "~4.8.4",
|
||||
"vite": "^4.3.1",
|
||||
"vite-plugin-pwa": "^0.14.7",
|
||||
"vitest": "^0.30.1",
|
||||
"vue-tsc": "^1.4.4",
|
||||
"webdriverio": "^8.8.8"
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.71.0",
|
||||
"typescript": "~5.3.3",
|
||||
"vite": "^5.1.3",
|
||||
"vite-plugin-pwa": "^0.19.0",
|
||||
"vitest": "^1.3.0",
|
||||
"vue-tsc": "^1.8.27",
|
||||
"webdriverio": "^8.32.2"
|
||||
}
|
||||
}
|
||||
|
||||
3501
pnpm-lock.yaml
generated
3501
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
44
src/modules/record/components/RecordStepTable.vue
Normal file
44
src/modules/record/components/RecordStepTable.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import type { Stepable } from '@/modules/task/interfaces/stepable'
|
||||
import StepRecord from '@/modules/record/components/StepRecord.vue'
|
||||
|
||||
defineProps<{
|
||||
id: string
|
||||
steps: Stepable[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table class="table record-step-table is-striped is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>status</th>
|
||||
<th>task</th>
|
||||
<th>estimation</th>
|
||||
<th>actual</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<step-record
|
||||
v-for="(step, key) in steps"
|
||||
:task-id="id"
|
||||
:key="step.id"
|
||||
:step-id="step.id"
|
||||
:step-number="key + 1"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.record-step-table {
|
||||
.step-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,9 @@
|
||||
import { fixtureTask } from '@/modules/task/models/task.fixture'
|
||||
import { router } from '@/router'
|
||||
import { toISODate } from '@/shared/types/date'
|
||||
import { withPlugins } from '@/tests/utils'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { fixtureRecordable } from '../interfaces/recordable.fixture'
|
||||
import TaskRecordPreview from './TaskRecordPreview.vue'
|
||||
|
||||
@@ -14,7 +13,7 @@ const mountTaskRecordPreview = (withRecord = false) => {
|
||||
const record = fixtureRecordable({
|
||||
taskId: task.id,
|
||||
stepRecords: {
|
||||
[faker.datatype.uuid()]: {
|
||||
[faker.string.uuid()]: {
|
||||
start: toISODate(new Date('2023-04-17T19:00:00.000Z')),
|
||||
end
|
||||
}
|
||||
@@ -45,12 +44,6 @@ const mountTaskRecordPreview = (withRecord = false) => {
|
||||
}
|
||||
|
||||
describe('Task Record Preview', () => {
|
||||
it('displays a start recording', () => {
|
||||
const { wrapper } = mountTaskRecordPreview()
|
||||
|
||||
expect(wrapper.text()).toContain('start recording')
|
||||
})
|
||||
|
||||
it('displays no record yet if there is no record', () => {
|
||||
const { wrapper } = mountTaskRecordPreview()
|
||||
|
||||
@@ -60,22 +53,6 @@ describe('Task Record Preview', () => {
|
||||
it('displays the duration of a recorded task', () => {
|
||||
const { wrapper } = mountTaskRecordPreview(true)
|
||||
|
||||
expect(wrapper.text()).toContain('last time: 60 minutes')
|
||||
})
|
||||
|
||||
it('navigates to recording view on click', async () => {
|
||||
const { task, wrapper } = mountTaskRecordPreview()
|
||||
|
||||
const spyOnPush = vi.spyOn(router, 'push')
|
||||
|
||||
await wrapper.find('a').trigger('click')
|
||||
|
||||
expect(spyOnPush).toHaveBeenCalledTimes(1)
|
||||
expect(spyOnPush).toHaveBeenCalledWith({
|
||||
name: 'record-view',
|
||||
params: {
|
||||
taskId: task.id
|
||||
}
|
||||
})
|
||||
expect(wrapper.text()).toContain('Last record took 60 minutes.')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('use task record metadata', () => {
|
||||
const end = toISODate(new Date('2023-04-17T20:00:00.000Z'))
|
||||
const record = fixtureRecordable({
|
||||
stepRecords: {
|
||||
[faker.datatype.uuid()]: {
|
||||
[faker.string.uuid()]: {
|
||||
start: toISODate(new Date('2023-04-17T19:00:00.000Z')),
|
||||
end
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ import type { Recordable } from './recordable'
|
||||
export const fixtureRecordable = (
|
||||
partialRecordable?: Partial<Recordable>
|
||||
): Recordable => ({
|
||||
taskId: partialRecordable?.taskId ?? faker.datatype.uuid(),
|
||||
taskId: partialRecordable?.taskId ?? faker.string.uuid(),
|
||||
notes: partialRecordable?.notes ?? faker.lorem.paragraph(),
|
||||
start: partialRecordable?.start ?? toISODate(faker.datatype.datetime()),
|
||||
start: partialRecordable?.start ?? toISODate(faker.date.anytime()),
|
||||
breakTime: partialRecordable?.breakTime ?? undefined,
|
||||
stepRecords: partialRecordable?.stepRecords ?? {
|
||||
[faker.datatype.uuid()]: {
|
||||
start: toISODate(faker.datatype.datetime())
|
||||
[faker.string.uuid()]: {
|
||||
start: toISODate(faker.date.anytime())
|
||||
}
|
||||
},
|
||||
currentStepId: null,
|
||||
|
||||
@@ -5,11 +5,11 @@ import type { TimeRange } from './time-range'
|
||||
export const fixtureTimeRange = (
|
||||
partialTimeRange?: Partial<TimeRange>
|
||||
): TimeRange => {
|
||||
const [start, end] = faker.date.betweens(
|
||||
toISODate(faker.date.past(1)),
|
||||
toISODate(new Date()),
|
||||
2
|
||||
)
|
||||
const [start, end] = faker.date.betweens({
|
||||
from: toISODate(faker.date.past({ years: 1 })),
|
||||
to: toISODate(new Date()),
|
||||
count: 2
|
||||
})
|
||||
|
||||
return {
|
||||
start: partialTimeRange?.start ?? toISODate(start),
|
||||
|
||||
@@ -9,13 +9,13 @@ import { TaskRecord } from './task-record'
|
||||
describe('Task Record', () => {
|
||||
it('creates a Record from a Recordable', () => {
|
||||
const recordable: Recordable = fixtureRecordable({
|
||||
taskId: faker.datatype.uuid(),
|
||||
taskId: faker.string.uuid(),
|
||||
notes: faker.lorem.paragraphs(),
|
||||
start: toISODate(faker.date.past(1)),
|
||||
start: toISODate(faker.date.past({ years: 1 })),
|
||||
end: toISODate(faker.date.past()),
|
||||
breakTime: fixtureTimeRange(),
|
||||
stepRecords: {
|
||||
[faker.datatype.uuid()]: fixtureTimeRange()
|
||||
[faker.string.uuid()]: fixtureTimeRange()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
70
src/modules/step/components/StepTable.vue
Normal file
70
src/modules/step/components/StepTable.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import type { Stepable } from '@/modules/task/interfaces/stepable'
|
||||
|
||||
defineProps<{
|
||||
id: string
|
||||
steps: Stepable[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table class="table record-step-table is-striped is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>status</th>
|
||||
<th>task</th>
|
||||
<th>estimation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="step-row" v-for="(step, index) in steps" :key="step.id">
|
||||
<td class="td-rank">
|
||||
<div class="rank">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="step-title">
|
||||
{{ step.title }}
|
||||
</td>
|
||||
<td class="estimation minutes">{{ step.estimation }} min</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.step-table {
|
||||
.step-row {
|
||||
.status {
|
||||
text-align: center;
|
||||
|
||||
div {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.td-rank {
|
||||
&.added-afterward {
|
||||
background: #fbc124;
|
||||
}
|
||||
}
|
||||
|
||||
.rank {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.minutes {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
46
src/modules/task/components/EditStepsForm.vue
Normal file
46
src/modules/task/components/EditStepsForm.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import type { Stepable } from '@/modules/task/interfaces/stepable'
|
||||
import { ref, watch } from 'vue'
|
||||
import StepInput from './StepInput.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
isActive: boolean
|
||||
initialSteps: Stepable[]
|
||||
}>()
|
||||
const emits = defineEmits<{
|
||||
(event: 'submit', steps: Stepable[]): void
|
||||
(event: 'close'): void
|
||||
}>()
|
||||
const steps = ref<Stepable[]>(props.initialSteps)
|
||||
|
||||
watch(props.initialSteps, (initialSteps) => {
|
||||
steps.value = initialSteps
|
||||
})
|
||||
|
||||
const save = () => {
|
||||
emits('submit', steps.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="modal" :class="{ 'is-active': isActive }">
|
||||
<div class="modal-background" @click="$emit('close')"></div>
|
||||
<div class="edit-steps-form modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Edit steps</p>
|
||||
<button
|
||||
class="delete"
|
||||
aria-label="close"
|
||||
@click="$emit('close')"
|
||||
></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<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" @click="$emit('close')">cancel</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -6,5 +6,5 @@ const id = createUuid()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<task-form :id="id" />
|
||||
<task-form :id="id" :action="'new'" />
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import EstimationTimeArrival from '@/components/EstimationTimeArrival.vue'
|
||||
import { createUuid } from '@/shared/create-uuid'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { Stepable } from '../interfaces/stepable'
|
||||
@@ -8,48 +7,23 @@ import type { Taskable } from '../interfaces/taskable'
|
||||
import { Task } from '../models/task'
|
||||
import { useTaskStore } from '../stores/useTask.store'
|
||||
import StepInput from './StepInput.vue'
|
||||
import { exampleSteps } from '@/modules/task/examples/steps'
|
||||
|
||||
const store = useTaskStore()
|
||||
const router = useRouter()
|
||||
const props = defineProps<{ id: string; initialTask?: Taskable }>()
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
initialTask?: Taskable
|
||||
}>()
|
||||
const id = computed(() => props.id)
|
||||
const hasTasks = computed(() => store.tasks.length > 0)
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
|
||||
const steps = ref<Stepable[]>(
|
||||
props.initialTask?.steps ?? (hasTasks.value ? [] : exampleSteps)
|
||||
props.initialTask
|
||||
? Task.fromTaskable(props.initialTask).steps
|
||||
: hasTasks.value
|
||||
? []
|
||||
: exampleSteps
|
||||
)
|
||||
|
||||
const title = ref(props.initialTask?.title ?? '')
|
||||
@@ -60,23 +34,24 @@ const totalEstimation = computed(() =>
|
||||
)
|
||||
|
||||
const saveTask = () => {
|
||||
const task = new Task(id.value, title.value)
|
||||
const task = new Task(id.value, title.value, props.initialTask?.stepHistory)
|
||||
|
||||
if (link.value) {
|
||||
task.link = link.value
|
||||
}
|
||||
task.addSteps(...steps.value)
|
||||
task.newSteps(steps.value)
|
||||
|
||||
if (Task.validate(task)) {
|
||||
store.saveTask(task)
|
||||
router.push({
|
||||
name: 'task-view',
|
||||
params: {
|
||||
id: id.value
|
||||
}
|
||||
})
|
||||
if (!Task.validate(task)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
store.saveTask(task)
|
||||
router.push({
|
||||
name: 'task-view',
|
||||
params: {
|
||||
id: id.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isValid = computed(() => title.value && steps.value.length > 0)
|
||||
|
||||
35
src/modules/task/examples/steps.ts
Normal file
35
src/modules/task/examples/steps.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
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
|
||||
}
|
||||
]
|
||||
@@ -6,5 +6,5 @@ export interface Taskable {
|
||||
title: string
|
||||
date: ISODate
|
||||
link: string | null
|
||||
steps: Stepable[]
|
||||
stepHistory?: Stepable[][]
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { Stepable } from '@/modules/task/interfaces/stepable'
|
||||
import { faker } from '@faker-js/faker'
|
||||
|
||||
export const fixtureStep = (partialStep?: Partial<Stepable>) => ({
|
||||
id: partialStep?.id ?? faker.datatype.uuid(),
|
||||
id: partialStep?.id ?? faker.string.uuid(),
|
||||
title: partialStep?.title ?? faker.animal.bird(),
|
||||
estimation:
|
||||
partialStep?.estimation ??
|
||||
faker.datatype.number({
|
||||
faker.number.int({
|
||||
min: 0,
|
||||
max: 40
|
||||
})
|
||||
|
||||
@@ -9,6 +9,6 @@ export const fixtureTask = (
|
||||
...steps: Stepable[]
|
||||
) =>
|
||||
new Task(
|
||||
partialTask?.id ?? faker.datatype.uuid(),
|
||||
partialTask?.id ?? faker.string.uuid(),
|
||||
partialTask?.title ?? faker.animal.bird()
|
||||
).addSteps(...(steps ?? fixtureStep()))
|
||||
|
||||
@@ -7,7 +7,7 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('Task', () => {
|
||||
it('has a simple id', () => {
|
||||
const uuid = faker.datatype.uuid()
|
||||
const uuid = faker.string.uuid()
|
||||
|
||||
const task = new Task(uuid, faker.animal.bear())
|
||||
|
||||
@@ -16,11 +16,11 @@ describe('Task', () => {
|
||||
|
||||
it('allows a new task from a taskable object', () => {
|
||||
const taskable: Taskable = {
|
||||
id: faker.datatype.uuid(),
|
||||
id: faker.string.uuid(),
|
||||
date: toISODate(faker.date.recent()),
|
||||
title: faker.animal.lion(),
|
||||
link: faker.internet.url(),
|
||||
steps: [fixtureStep()]
|
||||
stepHistory: [[fixtureStep()]]
|
||||
}
|
||||
const task = Task.fromTaskable(taskable)
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('Task', () => {
|
||||
})
|
||||
|
||||
it('adds steps and removes them', () => {
|
||||
const task = new Task(faker.datatype.uuid(), faker.color.human())
|
||||
const task = new Task(faker.string.uuid(), faker.color.human())
|
||||
|
||||
const [firstStep, secondStep] = [fixtureStep(), fixtureStep()]
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('Task', () => {
|
||||
})
|
||||
|
||||
it('must have an id, a title and steps to be valid', () => {
|
||||
const task = new Task(faker.datatype.uuid(), faker.color.human())
|
||||
const task = new Task(faker.string.uuid(), faker.color.human())
|
||||
expect(Task.validate(task)).toEqual(false)
|
||||
|
||||
task.addSteps(fixtureStep())
|
||||
@@ -52,7 +52,7 @@ describe('Task', () => {
|
||||
})
|
||||
|
||||
it('calculates the total estimation of steps', () => {
|
||||
const task = new Task(faker.datatype.uuid(), faker.color.human())
|
||||
const task = new Task(faker.string.uuid(), faker.color.human())
|
||||
|
||||
task.addSteps(
|
||||
fixtureStep({ estimation: 1 }),
|
||||
@@ -62,4 +62,29 @@ describe('Task', () => {
|
||||
|
||||
expect(task.totalEstimation).toEqual(6)
|
||||
})
|
||||
|
||||
it('save the initial plan even after the task is updated', () => {
|
||||
const steps = [
|
||||
fixtureStep({ estimation: 1 }),
|
||||
fixtureStep({ estimation: 2 })
|
||||
]
|
||||
const task = new Task(faker.string.uuid(), faker.color.human(), [steps])
|
||||
|
||||
task.updateSteps([
|
||||
fixtureStep({ estimation: 3 }),
|
||||
fixtureStep({ estimation: 1 }),
|
||||
fixtureStep({ estimation: 3 })
|
||||
])
|
||||
|
||||
expect(steps).toEqual(task.initialPlan)
|
||||
})
|
||||
|
||||
it('says if the task was updated', () => {
|
||||
const task = new Task(faker.string.uuid(), faker.color.human())
|
||||
|
||||
expect(task.wasUpdated).toEqual(false)
|
||||
|
||||
task.addSteps(fixtureStep())
|
||||
expect(task.wasUpdated).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,14 +3,37 @@ import type { Taskable } from '@/modules/task/interfaces/taskable'
|
||||
import { toISODate } from '@/shared/types/date'
|
||||
|
||||
export class Task implements Taskable {
|
||||
public readonly stepHistory: Stepable[][] = []
|
||||
public date = toISODate(new Date())
|
||||
public steps: Stepable[] = []
|
||||
public link: string | null = null
|
||||
|
||||
constructor(public readonly id: string, public readonly title: string) {}
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly title: string,
|
||||
history: Stepable[][] = []
|
||||
) {
|
||||
this.stepHistory = history
|
||||
}
|
||||
|
||||
public get initialPlan() {
|
||||
return this.stepHistory[0] ?? []
|
||||
}
|
||||
|
||||
public get steps(): Stepable[] {
|
||||
return this.stepHistory[this.stepHistory.length - 1] ?? []
|
||||
}
|
||||
|
||||
public get wasUpdated() {
|
||||
return this.stepHistory.length > 0
|
||||
}
|
||||
|
||||
public addSteps(...steps: Stepable[]) {
|
||||
this.steps.push(...steps)
|
||||
this.stepHistory.push([...this.steps, ...steps])
|
||||
return this
|
||||
}
|
||||
|
||||
public newSteps(steps: Stepable[]) {
|
||||
this.stepHistory.push(steps)
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -23,7 +46,12 @@ export class Task implements Taskable {
|
||||
return this
|
||||
}
|
||||
|
||||
this.steps.splice(index, 1)
|
||||
this.stepHistory.push(this.steps.filter((_, i) => i !== index))
|
||||
return this
|
||||
}
|
||||
|
||||
public updateSteps(steps: Stepable[]) {
|
||||
this.stepHistory.push(steps)
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -32,15 +60,14 @@ export class Task implements Taskable {
|
||||
}
|
||||
|
||||
public static fromTaskable(taskable: Taskable) {
|
||||
const task = new Task(taskable.id, taskable.title)
|
||||
const task = new Task(taskable.id, taskable.title, taskable.stepHistory)
|
||||
task.link = taskable.link
|
||||
task.date = taskable.date
|
||||
task.addSteps(...taskable.steps)
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
public static validate(task: Taskable) {
|
||||
return !!task.id && !!task.title && task.steps.length > 0
|
||||
return !!task.id && !!task.title && Task.fromTaskable(task).steps.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,25 +18,29 @@ export const useTaskStore = defineStore('task-store', {
|
||||
this.tasks.push(task)
|
||||
},
|
||||
addStepsToTask(taskId: string, steps: Stepable[], fromStepId: string) {
|
||||
const task = this.tasks.find((t) => t.id === taskId)
|
||||
this.tasks = this.tasks.map((task) => {
|
||||
if (task.id !== taskId) {
|
||||
return task
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
return
|
||||
}
|
||||
const fromStepIndex = Task.fromTaskable(task).steps.findIndex(
|
||||
(s) => s.id === fromStepId
|
||||
)
|
||||
|
||||
const fromStepIndex = task.steps.findIndex((s) => s.id === fromStepId)
|
||||
if (fromStepIndex < 0) {
|
||||
return task
|
||||
}
|
||||
|
||||
if (fromStepIndex < 0) {
|
||||
return
|
||||
}
|
||||
const newTask = Task.fromTaskable(task)
|
||||
|
||||
const newSteps = [
|
||||
...task.steps.slice(0, fromStepIndex + 1),
|
||||
...steps.map((step) => ({ ...step, addedAfterward: true })),
|
||||
...task.steps.slice(fromStepIndex + 1)
|
||||
]
|
||||
newTask.newSteps([
|
||||
...newTask.steps.slice(0, fromStepIndex + 1),
|
||||
...steps.map((step) => ({ ...step, addedAfterward: true })),
|
||||
...newTask.steps.slice(fromStepIndex + 1)
|
||||
])
|
||||
|
||||
task.steps = newSteps
|
||||
return newTask
|
||||
})
|
||||
},
|
||||
reset() {
|
||||
this.tasks = []
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { TaskStoreState } from '@/modules/task/stores/useTask.store'
|
||||
import { router } from '@/router'
|
||||
import { toISODate } from '@/shared/types/date'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import type { GlobalMountOptions } from '@vue/test-utils/dist/types'
|
||||
import type { GlobalMountOptions } from 'node_modules/@vue/test-utils/dist/types'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
export interface InitialState {
|
||||
|
||||
@@ -27,7 +27,7 @@ const resetTasks = () => {
|
||||
</div>
|
||||
<task-list class="column task-list" />
|
||||
</div>
|
||||
<!--
|
||||
<!--
|
||||
<footer>
|
||||
<p>
|
||||
Made with <img src="@/assets/icons/love.svg" alt="love" /> by
|
||||
|
||||
@@ -6,7 +6,8 @@ import { useCopyRecord } from '@/modules/record/hooks/useCopyRecord.hook'
|
||||
import { useTaskStore } from '@/modules/task/stores/useTask.store'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import StepRecord from '@/modules/record/components/StepRecord.vue'
|
||||
import RecordStepTable from '@/modules/record/components/RecordStepTable.vue'
|
||||
import StepTable from '@/modules/step/components/StepTable.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
@@ -84,40 +85,17 @@ const { canShareTask, taskCopied, shareTask } = useCopyRecord(task)
|
||||
<estimation-time-arrival :estimation="task.totalEstimation" />
|
||||
</h2>
|
||||
<task-record-preview :task-id="id" />
|
||||
<table class="table is-striped is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>status</th>
|
||||
<th>task</th>
|
||||
<th>estimation</th>
|
||||
<th>actual</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<step-record
|
||||
v-for="(step, key) in task.steps"
|
||||
:task-id="id"
|
||||
:key="step.id"
|
||||
:step-id="step.id"
|
||||
:step-number="key + 1"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
<record-step-table :id="id" :steps="task.steps" />
|
||||
<details v-if="task.initialPlan && task.wasUpdated">
|
||||
<summary>Initial plan</summary>
|
||||
<step-table :id="id" :steps="task.initialPlan" />
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<task-not-found v-else />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.step-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
float: right;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"extends": [
|
||||
"@tsconfig/node18/tsconfig.json",
|
||||
"@vue/tsconfig/tsconfig.json"
|
||||
],
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { mergeConfig } from 'vite'
|
||||
import { configDefaults, defineConfig } from 'vitest/config'
|
||||
import viteConfig from './vite.config'
|
||||
import viteConfig from './vite.config.mjs'
|
||||
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
|
||||
Reference in New Issue
Block a user