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,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "run-p type-check build-only",
|
"build": "run-p test:types build-only",
|
||||||
"preview": "vite preview",
|
"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:unit": "vitest",
|
||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
|
"test": "pnpm lint && pnpm test:types && pnpm test:unit",
|
||||||
"build-only": "vite build",
|
"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",
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^10.6.1",
|
"@vueuse/core": "^10.7.2",
|
||||||
"@vueuse/math": "^10.6.1",
|
"@vueuse/math": "^10.7.2",
|
||||||
"bulma": "^0.9.4",
|
"bulma": "^0.9.4",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"nanoid": "^5.0.3",
|
"nanoid": "^5.0.5",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"pinia-plugin-persistedstate": "^3.2.0",
|
"pinia-plugin-persistedstate": "^3.2.1",
|
||||||
"suretype": "^3.3.1",
|
"suretype": "^3.3.1",
|
||||||
"vue": "^3.3.9",
|
"vue": "^3.4.19",
|
||||||
"vue-router": "^4.2.5"
|
"vue-router": "^4.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^7.6.0",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@pinia/testing": "^0.0.16",
|
"@pinia/testing": "^0.1.3",
|
||||||
"@rushstack/eslint-patch": "^1.2.0",
|
"@rushstack/eslint-patch": "^1.7.2",
|
||||||
"@types/jsdom": "^21.1.1",
|
"@tsconfig/node18": "^18.2.2",
|
||||||
"@types/node": "^18.14.2",
|
"@types/jsdom": "^21.1.6",
|
||||||
"@vitejs/plugin-vue": "^4.1.0",
|
"@types/node": "^18.19.17",
|
||||||
"@vitest/browser": "^0.31.0",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vitest/ui": "^0.31.0",
|
"@vitest/browser": "^1.3.0",
|
||||||
"@vue/eslint-config-prettier": "^7.1.0",
|
"@vitest/ui": "^1.3.0",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
"@vue/test-utils": "^2.3.2",
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
"@vue/tsconfig": "^0.1.3",
|
"@vue/test-utils": "^2.4.4",
|
||||||
"eslint": "^8.39.0",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
"eslint-plugin-vue": "^9.11.0",
|
"eslint": "^8.56.0",
|
||||||
"jsdom": "^21.1.1",
|
"eslint-plugin-vue": "^9.21.1",
|
||||||
|
"jsdom": "^24.0.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^3.2.5",
|
||||||
"sass": "^1.62.1",
|
"sass": "^1.71.0",
|
||||||
"typescript": "~4.8.4",
|
"typescript": "~5.3.3",
|
||||||
"vite": "^4.3.1",
|
"vite": "^5.1.3",
|
||||||
"vite-plugin-pwa": "^0.14.7",
|
"vite-plugin-pwa": "^0.19.0",
|
||||||
"vitest": "^0.30.1",
|
"vitest": "^1.3.0",
|
||||||
"vue-tsc": "^1.4.4",
|
"vue-tsc": "^1.8.27",
|
||||||
"webdriverio": "^8.8.8"
|
"webdriverio": "^8.32.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3553
pnpm-lock.yaml
generated
3553
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 { fixtureTask } from '@/modules/task/models/task.fixture'
|
||||||
import { router } from '@/router'
|
|
||||||
import { toISODate } from '@/shared/types/date'
|
import { toISODate } from '@/shared/types/date'
|
||||||
import { withPlugins } from '@/tests/utils'
|
import { withPlugins } from '@/tests/utils'
|
||||||
import { faker } from '@faker-js/faker'
|
import { faker } from '@faker-js/faker'
|
||||||
import { mount } from '@vue/test-utils'
|
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 { fixtureRecordable } from '../interfaces/recordable.fixture'
|
||||||
import TaskRecordPreview from './TaskRecordPreview.vue'
|
import TaskRecordPreview from './TaskRecordPreview.vue'
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@ const mountTaskRecordPreview = (withRecord = false) => {
|
|||||||
const record = fixtureRecordable({
|
const record = fixtureRecordable({
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
stepRecords: {
|
stepRecords: {
|
||||||
[faker.datatype.uuid()]: {
|
[faker.string.uuid()]: {
|
||||||
start: toISODate(new Date('2023-04-17T19:00:00.000Z')),
|
start: toISODate(new Date('2023-04-17T19:00:00.000Z')),
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
@@ -45,12 +44,6 @@ const mountTaskRecordPreview = (withRecord = false) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('Task Record Preview', () => {
|
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', () => {
|
it('displays no record yet if there is no record', () => {
|
||||||
const { wrapper } = mountTaskRecordPreview()
|
const { wrapper } = mountTaskRecordPreview()
|
||||||
|
|
||||||
@@ -60,22 +53,6 @@ describe('Task Record Preview', () => {
|
|||||||
it('displays the duration of a recorded task', () => {
|
it('displays the duration of a recorded task', () => {
|
||||||
const { wrapper } = mountTaskRecordPreview(true)
|
const { wrapper } = mountTaskRecordPreview(true)
|
||||||
|
|
||||||
expect(wrapper.text()).toContain('last time: 60 minutes')
|
expect(wrapper.text()).toContain('Last record took 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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describe('use task record metadata', () => {
|
|||||||
const end = toISODate(new Date('2023-04-17T20:00:00.000Z'))
|
const end = toISODate(new Date('2023-04-17T20:00:00.000Z'))
|
||||||
const record = fixtureRecordable({
|
const record = fixtureRecordable({
|
||||||
stepRecords: {
|
stepRecords: {
|
||||||
[faker.datatype.uuid()]: {
|
[faker.string.uuid()]: {
|
||||||
start: toISODate(new Date('2023-04-17T19:00:00.000Z')),
|
start: toISODate(new Date('2023-04-17T19:00:00.000Z')),
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import type { Recordable } from './recordable'
|
|||||||
export const fixtureRecordable = (
|
export const fixtureRecordable = (
|
||||||
partialRecordable?: Partial<Recordable>
|
partialRecordable?: Partial<Recordable>
|
||||||
): Recordable => ({
|
): Recordable => ({
|
||||||
taskId: partialRecordable?.taskId ?? faker.datatype.uuid(),
|
taskId: partialRecordable?.taskId ?? faker.string.uuid(),
|
||||||
notes: partialRecordable?.notes ?? faker.lorem.paragraph(),
|
notes: partialRecordable?.notes ?? faker.lorem.paragraph(),
|
||||||
start: partialRecordable?.start ?? toISODate(faker.datatype.datetime()),
|
start: partialRecordable?.start ?? toISODate(faker.date.anytime()),
|
||||||
breakTime: partialRecordable?.breakTime ?? undefined,
|
breakTime: partialRecordable?.breakTime ?? undefined,
|
||||||
stepRecords: partialRecordable?.stepRecords ?? {
|
stepRecords: partialRecordable?.stepRecords ?? {
|
||||||
[faker.datatype.uuid()]: {
|
[faker.string.uuid()]: {
|
||||||
start: toISODate(faker.datatype.datetime())
|
start: toISODate(faker.date.anytime())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
currentStepId: null,
|
currentStepId: null,
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import type { TimeRange } from './time-range'
|
|||||||
export const fixtureTimeRange = (
|
export const fixtureTimeRange = (
|
||||||
partialTimeRange?: Partial<TimeRange>
|
partialTimeRange?: Partial<TimeRange>
|
||||||
): TimeRange => {
|
): TimeRange => {
|
||||||
const [start, end] = faker.date.betweens(
|
const [start, end] = faker.date.betweens({
|
||||||
toISODate(faker.date.past(1)),
|
from: toISODate(faker.date.past({ years: 1 })),
|
||||||
toISODate(new Date()),
|
to: toISODate(new Date()),
|
||||||
2
|
count: 2
|
||||||
)
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start: partialTimeRange?.start ?? toISODate(start),
|
start: partialTimeRange?.start ?? toISODate(start),
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import { TaskRecord } from './task-record'
|
|||||||
describe('Task Record', () => {
|
describe('Task Record', () => {
|
||||||
it('creates a Record from a Recordable', () => {
|
it('creates a Record from a Recordable', () => {
|
||||||
const recordable: Recordable = fixtureRecordable({
|
const recordable: Recordable = fixtureRecordable({
|
||||||
taskId: faker.datatype.uuid(),
|
taskId: faker.string.uuid(),
|
||||||
notes: faker.lorem.paragraphs(),
|
notes: faker.lorem.paragraphs(),
|
||||||
start: toISODate(faker.date.past(1)),
|
start: toISODate(faker.date.past({ years: 1 })),
|
||||||
end: toISODate(faker.date.past()),
|
end: toISODate(faker.date.past()),
|
||||||
breakTime: fixtureTimeRange(),
|
breakTime: fixtureTimeRange(),
|
||||||
stepRecords: {
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<task-form :id="id" />
|
<task-form :id="id" :action="'new'" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import EstimationTimeArrival from '@/components/EstimationTimeArrival.vue'
|
import EstimationTimeArrival from '@/components/EstimationTimeArrival.vue'
|
||||||
import { createUuid } from '@/shared/create-uuid'
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import type { Stepable } from '../interfaces/stepable'
|
import type { Stepable } from '../interfaces/stepable'
|
||||||
@@ -8,48 +7,23 @@ import type { Taskable } from '../interfaces/taskable'
|
|||||||
import { Task } from '../models/task'
|
import { Task } from '../models/task'
|
||||||
import { useTaskStore } from '../stores/useTask.store'
|
import { useTaskStore } from '../stores/useTask.store'
|
||||||
import StepInput from './StepInput.vue'
|
import StepInput from './StepInput.vue'
|
||||||
|
import { exampleSteps } from '@/modules/task/examples/steps'
|
||||||
|
|
||||||
const store = useTaskStore()
|
const store = useTaskStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const props = defineProps<{ id: string; initialTask?: Taskable }>()
|
const props = defineProps<{
|
||||||
|
id: string
|
||||||
|
initialTask?: Taskable
|
||||||
|
}>()
|
||||||
const id = computed(() => props.id)
|
const id = computed(() => props.id)
|
||||||
const hasTasks = computed(() => store.tasks.length > 0)
|
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[]>(
|
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 ?? '')
|
const title = ref(props.initialTask?.title ?? '')
|
||||||
@@ -60,13 +34,17 @@ const totalEstimation = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const saveTask = () => {
|
const saveTask = () => {
|
||||||
const task = new Task(id.value, title.value)
|
const task = new Task(id.value, title.value, props.initialTask?.stepHistory)
|
||||||
|
|
||||||
if (link.value) {
|
if (link.value) {
|
||||||
task.link = link.value
|
task.link = link.value
|
||||||
}
|
}
|
||||||
task.addSteps(...steps.value)
|
task.newSteps(steps.value)
|
||||||
|
|
||||||
|
if (!Task.validate(task)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (Task.validate(task)) {
|
|
||||||
store.saveTask(task)
|
store.saveTask(task)
|
||||||
router.push({
|
router.push({
|
||||||
name: 'task-view',
|
name: 'task-view',
|
||||||
@@ -76,9 +54,6 @@ const saveTask = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = computed(() => title.value && steps.value.length > 0)
|
const isValid = computed(() => title.value && steps.value.length > 0)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
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
|
title: string
|
||||||
date: ISODate
|
date: ISODate
|
||||||
link: string | null
|
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'
|
import { faker } from '@faker-js/faker'
|
||||||
|
|
||||||
export const fixtureStep = (partialStep?: Partial<Stepable>) => ({
|
export const fixtureStep = (partialStep?: Partial<Stepable>) => ({
|
||||||
id: partialStep?.id ?? faker.datatype.uuid(),
|
id: partialStep?.id ?? faker.string.uuid(),
|
||||||
title: partialStep?.title ?? faker.animal.bird(),
|
title: partialStep?.title ?? faker.animal.bird(),
|
||||||
estimation:
|
estimation:
|
||||||
partialStep?.estimation ??
|
partialStep?.estimation ??
|
||||||
faker.datatype.number({
|
faker.number.int({
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 40
|
max: 40
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ export const fixtureTask = (
|
|||||||
...steps: Stepable[]
|
...steps: Stepable[]
|
||||||
) =>
|
) =>
|
||||||
new Task(
|
new Task(
|
||||||
partialTask?.id ?? faker.datatype.uuid(),
|
partialTask?.id ?? faker.string.uuid(),
|
||||||
partialTask?.title ?? faker.animal.bird()
|
partialTask?.title ?? faker.animal.bird()
|
||||||
).addSteps(...(steps ?? fixtureStep()))
|
).addSteps(...(steps ?? fixtureStep()))
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { describe, expect, it } from 'vitest'
|
|||||||
|
|
||||||
describe('Task', () => {
|
describe('Task', () => {
|
||||||
it('has a simple id', () => {
|
it('has a simple id', () => {
|
||||||
const uuid = faker.datatype.uuid()
|
const uuid = faker.string.uuid()
|
||||||
|
|
||||||
const task = new Task(uuid, faker.animal.bear())
|
const task = new Task(uuid, faker.animal.bear())
|
||||||
|
|
||||||
@@ -16,11 +16,11 @@ describe('Task', () => {
|
|||||||
|
|
||||||
it('allows a new task from a taskable object', () => {
|
it('allows a new task from a taskable object', () => {
|
||||||
const taskable: Taskable = {
|
const taskable: Taskable = {
|
||||||
id: faker.datatype.uuid(),
|
id: faker.string.uuid(),
|
||||||
date: toISODate(faker.date.recent()),
|
date: toISODate(faker.date.recent()),
|
||||||
title: faker.animal.lion(),
|
title: faker.animal.lion(),
|
||||||
link: faker.internet.url(),
|
link: faker.internet.url(),
|
||||||
steps: [fixtureStep()]
|
stepHistory: [[fixtureStep()]]
|
||||||
}
|
}
|
||||||
const task = Task.fromTaskable(taskable)
|
const task = Task.fromTaskable(taskable)
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ describe('Task', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('adds steps and removes them', () => {
|
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()]
|
const [firstStep, secondStep] = [fixtureStep(), fixtureStep()]
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ describe('Task', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('must have an id, a title and steps to be valid', () => {
|
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)
|
expect(Task.validate(task)).toEqual(false)
|
||||||
|
|
||||||
task.addSteps(fixtureStep())
|
task.addSteps(fixtureStep())
|
||||||
@@ -52,7 +52,7 @@ describe('Task', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('calculates the total estimation of steps', () => {
|
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(
|
task.addSteps(
|
||||||
fixtureStep({ estimation: 1 }),
|
fixtureStep({ estimation: 1 }),
|
||||||
@@ -62,4 +62,29 @@ describe('Task', () => {
|
|||||||
|
|
||||||
expect(task.totalEstimation).toEqual(6)
|
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'
|
import { toISODate } from '@/shared/types/date'
|
||||||
|
|
||||||
export class Task implements Taskable {
|
export class Task implements Taskable {
|
||||||
|
public readonly stepHistory: Stepable[][] = []
|
||||||
public date = toISODate(new Date())
|
public date = toISODate(new Date())
|
||||||
public steps: Stepable[] = []
|
|
||||||
public link: string | null = null
|
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[]) {
|
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
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +46,12 @@ export class Task implements Taskable {
|
|||||||
return this
|
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
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,15 +60,14 @@ export class Task implements Taskable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static fromTaskable(taskable: 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.link = taskable.link
|
||||||
task.date = taskable.date
|
task.date = taskable.date
|
||||||
task.addSteps(...taskable.steps)
|
|
||||||
|
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
public static validate(task: Taskable) {
|
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)
|
this.tasks.push(task)
|
||||||
},
|
},
|
||||||
addStepsToTask(taskId: string, steps: Stepable[], fromStepId: string) {
|
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) {
|
||||||
if (!task) {
|
return task
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromStepIndex = task.steps.findIndex((s) => s.id === fromStepId)
|
const fromStepIndex = Task.fromTaskable(task).steps.findIndex(
|
||||||
|
(s) => s.id === fromStepId
|
||||||
|
)
|
||||||
|
|
||||||
if (fromStepIndex < 0) {
|
if (fromStepIndex < 0) {
|
||||||
return
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSteps = [
|
const newTask = Task.fromTaskable(task)
|
||||||
...task.steps.slice(0, fromStepIndex + 1),
|
|
||||||
...steps.map((step) => ({ ...step, addedAfterward: true })),
|
|
||||||
...task.steps.slice(fromStepIndex + 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
task.steps = newSteps
|
newTask.newSteps([
|
||||||
|
...newTask.steps.slice(0, fromStepIndex + 1),
|
||||||
|
...steps.map((step) => ({ ...step, addedAfterward: true })),
|
||||||
|
...newTask.steps.slice(fromStepIndex + 1)
|
||||||
|
])
|
||||||
|
|
||||||
|
return newTask
|
||||||
|
})
|
||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
this.tasks = []
|
this.tasks = []
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { TaskStoreState } from '@/modules/task/stores/useTask.store'
|
|||||||
import { router } from '@/router'
|
import { router } from '@/router'
|
||||||
import { toISODate } from '@/shared/types/date'
|
import { toISODate } from '@/shared/types/date'
|
||||||
import { createTestingPinia } from '@pinia/testing'
|
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'
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
export interface InitialState {
|
export interface InitialState {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const resetTasks = () => {
|
|||||||
</div>
|
</div>
|
||||||
<task-list class="column task-list" />
|
<task-list class="column task-list" />
|
||||||
</div>
|
</div>
|
||||||
<!--
|
<!--
|
||||||
<footer>
|
<footer>
|
||||||
<p>
|
<p>
|
||||||
Made with <img src="@/assets/icons/love.svg" alt="love" /> by
|
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 { useTaskStore } from '@/modules/task/stores/useTask.store'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
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<{
|
const props = defineProps<{
|
||||||
id: string
|
id: string
|
||||||
@@ -84,40 +85,17 @@ const { canShareTask, taskCopied, shareTask } = useCopyRecord(task)
|
|||||||
<estimation-time-arrival :estimation="task.totalEstimation" />
|
<estimation-time-arrival :estimation="task.totalEstimation" />
|
||||||
</h2>
|
</h2>
|
||||||
<task-record-preview :task-id="id" />
|
<task-record-preview :task-id="id" />
|
||||||
<table class="table is-striped is-hoverable">
|
<record-step-table :id="id" :steps="task.steps" />
|
||||||
<thead>
|
<details v-if="task.initialPlan && task.wasUpdated">
|
||||||
<tr>
|
<summary>Initial plan</summary>
|
||||||
<th>#</th>
|
<step-table :id="id" :steps="task.initialPlan" />
|
||||||
<th>status</th>
|
</details>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<task-not-found v-else />
|
<task-not-found v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.step-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: space-between;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
float: right;
|
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"],
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
"exclude": ["src/**/__tests__/*"],
|
"exclude": ["src/**/__tests__/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
"extends": [
|
||||||
|
"@tsconfig/node18/tsconfig.json",
|
||||||
|
"@vue/tsconfig/tsconfig.json"
|
||||||
|
],
|
||||||
"include": [
|
"include": [
|
||||||
"vite.config.*",
|
"vite.config.*",
|
||||||
"vitest.config.*",
|
"vitest.config.*",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { mergeConfig } from 'vite'
|
import { mergeConfig } from 'vite'
|
||||||
import { configDefaults, defineConfig } from 'vitest/config'
|
import { configDefaults, defineConfig } from 'vitest/config'
|
||||||
import viteConfig from './vite.config'
|
import viteConfig from './vite.config.mjs'
|
||||||
|
|
||||||
export default mergeConfig(
|
export default mergeConfig(
|
||||||
viteConfig,
|
viteConfig,
|
||||||
|
|||||||
Reference in New Issue
Block a user