Skip to content

Commit

Permalink
Implemented workbox approve/delete
Browse files Browse the repository at this point in the history
  • Loading branch information
mlhaufe committed Oct 12, 2024
1 parent 85768c1 commit 76bfbd5
Show file tree
Hide file tree
Showing 26 changed files with 301 additions and 212 deletions.
120 changes: 84 additions & 36 deletions components/WorkboxDataView.vue
Original file line number Diff line number Diff line change
@@ -1,61 +1,109 @@
<script lang="ts" setup>
import type Dialog from 'primevue/dialog'
import camelCaseToTitle from '~/utils/camelCaseToTitle.js';
import type { Requirement } from '~/server/domain/Requirement.js';
import type { ParsedRequirement } from '~/server/domain/ParsedRequirement.js';
type RowType = { id: string; name: string; }
import type { ParsedRequirement, ParsedReqColType } from '~/server/domain/ParsedRequirement.js';
const props = defineProps<{
parsedRequirement: ParsedRequirement,
onApprove: (parentId: string, itemId: string) => Promise<void>,
onReject: (parentId: string, itemId: string) => Promise<void>
onItemApprove: (type: ParsedReqColType, itemId: string) => Promise<void>,
onItemUpdate: (type: ParsedReqColType, data: Requirement) => Promise<void>,
onItemDelete: (type: ParsedReqColType, itemId: string) => Promise<void>
}>()
const confirm = useConfirm()
type RequirementType = { type: string, items: Requirement[] }
const requirements: RequirementType[] = Object.entries(props.parsedRequirement)
.filter(([_, value]) => Array.isArray(value) && value.length > 0)
.map(([key, value]) => ({ type: key, items: value as Requirement[] }))
const onReject = (parentId: string, item: RowType) => new Promise<void>((resolve, _reject) => {
confirm.require({
message: `Are you sure you want to reject ${item.name}?`,
header: 'Delete Confirmation',
icon: 'pi pi-exclamation-triangle',
rejectLabel: 'Cancel',
acceptLabel: 'Reject',
accept: async () => {
await props.onReject(parentId, item.id)
resolve()
},
reject: () => { }
})
})
const editDialog = ref<Dialog>(),
editDialogVisible = ref(false),
editDialogItem = ref<Requirement>(Object.create(null)),
dataView = ref<DataView>()
type RequirementType = { type: ParsedReqColType, items: Requirement[] }
const groupRequirements = () =>
Object.entries(props.parsedRequirement)
.filter(([_, value]) => Array.isArray(value) && value.length > 0)
.map(([key, value]) => ({ type: key as ParsedReqColType, items: value as Requirement[] }))
const requirements = ref<RequirementType[]>(groupRequirements())
const onItemDelete = async (type: ParsedReqColType, item: Requirement) => {
if (confirm(`Are you sure you want to delete "${item.name}"?`))
await props.onItemDelete(type, item.id)
}
const onItemApprove = async (type: ParsedReqColType, item: Requirement) => {
await props.onItemApprove(type, item.id)
}
const openEditDialog = async (type: ParsedReqColType, item: Requirement) => {
editDialogItem.value = { ...item }
editDialogVisible.value = true
}
const onEditDialogSave = async (e: Event) => {
const form = e.target as HTMLFormElement
if (!form.reportValidity())
return
const data = [...new FormData(form).entries()].reduce((acc, [key, value]) => {
// If the data entry was from a form input element with inputmode="numeric", convert it to a number
const input = form.querySelector(`[name="${key}"]`) as HTMLInputElement
Object.assign(acc, { [key]: input.inputMode === 'numeric' ? parseFloat(value as string) : value })
return acc
}, {} as Requirement)
// await props.onItemUpdate('{unknown}', data)
editDialogVisible.value = false
editDialogItem.value = Object.create(null)
}
const onEditDialogCancel = () => {
editDialogVisible.value = false
editDialogItem.value = Object.create(null)
}
const reqKeys = (req: object) => Object.keys(req)
.filter(key => !['id', 'lastModified', 'modifiedBy', 'follows', 'solution'].includes(key))
</script>
<template>
<ConfirmDialog></ConfirmDialog>
<DataView :value="requirements" :data-key="undefined">
<DataView :value="requirements" :data-key="undefined" ref="dataView">
<template #list="{ items }: { items: RequirementType[] }">
<div v-for="(requirements, index) in items" :key="index" :value="requirements">
<div class="mt-3" v-for="(requirements, index) in items" :key="index" :value="requirements">
<DataTable :value="requirements.items">
<template #header>
<div class="flex flex-wrap align-items-center justify-content-between gap-2">
<span class="text-xl text-900 font-bold">{{ requirements.type }}</span>
<span class="text-xl text-900 font-bold">{{ camelCaseToTitle(requirements.type) }}</span>
</div>
</template>
<Column v-for="col of Object.keys(requirements.items[0])" :key="col" :field="col" :header="col">
<Column v-for="col of reqKeys(requirements.items[0])" :key="col" :field="col"
:header="camelCaseToTitle(col)">
</Column>
<!--
<Column header="Actions" frozen align-frozen="right">
<template #body="{ data }">
<Button icon="pi pi-eye" class="text rounded mr-2" />
<Button icon="pi pi-trash" text rounded severity="danger" />
<Button icon="pi pi-check" text rounded class="mr-2" severity="success" title="Approve"
@click="onItemApprove(requirements.type, data)" />
<!-- <Button icon="pi pi-pencil" text rounded class="mr-2" title="Edit"
@click="openEditDialog(requirements.type, data)" /> -->
<Button icon="pi pi-trash" text rounded severity="danger" title="Delete"
@click="onItemDelete(requirements.type, data)" />
</template>
</Column>
-->
</DataTable>
</div>
</template>
</DataView>

<!-- FIXME: This is too generic to be useful. field types are needed. -->
<Dialog ref="editDialog" v-model:visible="editDialogVisible" :modal="true" class="p-fluid">
<template #header>Edit Item</template>
<form id="editDialogForm" autocomplete="off" @submit.prevent="onEditDialogSave" @reset="onEditDialogCancel">
<div class="field grid" v-for="key in reqKeys(editDialogItem)">
<label :for="key" class="col-4">{{ camelCaseToTitle(key) }}</label>
<input :id="key" :name="key" v-model="(editDialogItem as any)[key]" class="col" required />
</div>
</form>
<template #footer>
<Button label="Save" type="submit" form="editDialogForm" icon="pi pi-check" class="p-button-text" />
<Button label="Cancel" type="reset" form="editDialogForm" icon="pi pi-times" class="p-button-text" />
</template>
</Dialog>
</template>
6 changes: 0 additions & 6 deletions components/XDataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@ const props = defineProps<{
onUpdate: (data: RowType) => Promise<void>
}>()
const slots = defineSlots<{
rows: { data: RowType }[],
createDialog: { data: RowType },
editDialog: { data: RowType }
}>()
const dataTable = ref<DataTable>(),
createDisabled = ref(false),
sortField = ref<string | undefined>('name'),
Expand Down
26 changes: 13 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 42 additions & 25 deletions pages/o/[organization-slug]/[solution-slug]/workbox.client.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<script lang="ts" setup>
import type { ParsedRequirement } from '~/server/domain/index.js';
import type { ParsedRequirement, Requirement, ParsedReqColType } from '~/server/domain/index.js';
useHead({ title: 'Workbox' });
definePageMeta({ name: 'Workbox' });
const { $eventBus } = useNuxtApp(),
router = useRouter(),
{ solutionslug, organizationslug } = useRoute('Workbox').params,
{ data: solutions, error: solutionError } = await useFetch('/api/solutions', {
query: {
Expand All @@ -14,29 +15,47 @@ const { $eventBus } = useNuxtApp(),
}),
solution = solutions.value![0];
const { data: parsedRequirements, error: parsedRequirementsError } = await useFetch<ParsedRequirement[]>('/api/parse-requirements', {
const { data: parsedRequirements, error: parsedRequirementsError, refresh } = await useFetch<ParsedRequirement[]>('/api/parse-requirements', {
method: 'get',
query: { solutionId: solution.id }
query: { solutionId: solution.id },
transform: (data: ParsedRequirement[]) => data.map((parsedRequirement) => {
parsedRequirement.lastModified = new Date(parsedRequirement.lastModified)
return parsedRequirement;
})
});
// counts the number of requirements in the meta requirement
const countRequirements = (parsedRequirement: ParsedRequirement) =>
Object.values(parsedRequirement)
.filter(Array.isArray)
.reduce((acc, val) => acc + val.length, 0);
if (solutionError.value)
$eventBus.$emit('page-error', solutionError.value);
if (parsedRequirementsError.value)
$eventBus.$emit('page-error', parsedRequirementsError.value);
const onApprove = async (parentId: string, itemId: string) => {
alert('Approve: Not implemented yet')
const renderParsedRequirements = ref(true);
const onItemApprove = async (type: ParsedReqColType, itemId: string) => {
await $fetch(`/api/${camelCaseToSlug(type)}/${itemId}`, {
// @ts-ignore: method not recognized
method: 'put',
body: { solutionId: solution.id, isSilence: false }
});
renderParsedRequirements.value = false;
await refresh();
renderParsedRequirements.value = true;
}
const onReject = async (parentId: string, itemId: string) => {
alert('Reject: Not implemented yet')
const onItemDelete = async (type: ParsedReqColType, itemId: string) => {
await $fetch(`/api/${camelCaseToSlug(type)}/${itemId}`, {
// @ts-ignore: method not recognized
method: 'delete',
body: { solutionId: solution.id }
});
renderParsedRequirements.value = false;
await refresh();
renderParsedRequirements.value = true;
}
const onItemUpdate = async (type: ParsedReqColType, data: Requirement) => {
alert(`UPDATE ${type} not implemented`)
}
</script>

Expand All @@ -46,21 +65,19 @@ const onReject = async (parentId: string, itemId: string) => {
<InlineMessage v-if="!parsedRequirements?.length" severity="info">
The Workbox is empty.
</InlineMessage>
<Accordion v-if="parsedRequirements">
<Accordion v-if="renderParsedRequirements">
<AccordionTab v-for="parsedRequirement in parsedRequirements" :key="parsedRequirement.id">
<template #header>
<span class="flex align-items-center gap-2 w-full">
<InlineMessage severity="secondary">{{ parsedRequirement.lastModified }}</InlineMessage>
by
<span class="font-bold white-space-nowrap">{{ parsedRequirement.modifiedBy.name }}</span>
<Badge :value="countRequirements(parsedRequirement)" class="ml-auto mr-2" />
<!-- <Button icon="pi pi-refresh" rounded raised /> -->
</span>
<div class="grid w-11">
<InlineMessage class="col-fixed" severity="secondary">{{
parsedRequirement.lastModified.toLocaleString() }}
</InlineMessage>
<span class="col-fixed font-bold">by {{ parsedRequirement.modifiedBy.name }}</span>
<span class="col font-italic">{{ parsedRequirement.statement }}</span>
</div>
</template>
<h2>Statement</h2>
<p class="font-italic">{{ parsedRequirement.statement }}</p>
<WorkboxDataView :parsedRequirement="parsedRequirement" :on-reject="onReject" :on-approve="onApprove" />
<WorkboxDataView :parsedRequirement="parsedRequirement" :on-item-delete="onItemDelete"
:on-item-approve="onItemApprove" :on-item-update="onItemUpdate" />
</AccordionTab>
</Accordion>
</div>
Expand Down
12 changes: 6 additions & 6 deletions server/api/assumptions/[id].put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const paramSchema = z.object({

const bodySchema = z.object({
solutionId: z.string().uuid(),
name: z.string().default("{Untitled Assumption}"),
statement: z.string().default(""),
name: z.string().optional(),
statement: z.string().optional(),
isSilence: z.boolean().optional()
})

Expand All @@ -30,11 +30,11 @@ export default defineEventHandler(async (event) => {
})

Object.assign(assumption, {
name,
statement,
name: name ?? assumption.name,
statement: statement ?? assumption.statement,
isSilence: isSilence ?? assumption.isSilence,
modifiedBy: sessionUser,
lastModified: new Date(),
...(isSilence !== undefined && { isSilence })
lastModified: new Date()
})

await em.persistAndFlush(assumption)
Expand Down
14 changes: 7 additions & 7 deletions server/api/constraints/[id].put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const paramSchema = z.object({

const bodySchema = z.object({
solutionId: z.string().uuid(),
name: z.string().default("{Untitled Constraint}"),
statement: z.string().default(""),
name: z.string().optional(),
statement: z.string().optional(),
category: z.nativeEnum(ConstraintCategory).optional(),
isSilence: z.boolean().optional()
})
Expand All @@ -31,12 +31,12 @@ export default defineEventHandler(async (event) => {
})

Object.assign(constraint, {
name,
statement,
category,
name: name ?? constraint.name,
statement: statement ?? constraint.statement,
category: category ?? constraint.category,
isSilence: isSilence ?? constraint.isSilence,
modifiedBy: sessionUser,
lastModified: new Date(),
...(isSilence !== undefined && { isSilence })
lastModified: new Date()
})

await em.persistAndFlush(constraint)
Expand Down
10 changes: 5 additions & 5 deletions server/api/effects/[id].put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const paramSchema = z.object({

const bodySchema = z.object({
solutionId: z.string().uuid(),
name: z.string().default("{Untitled Effect}"),
statement: z.string().default(""),
name: z.string().optional(),
statement: z.string().optional(),
isSilence: z.boolean().optional()
})

Expand All @@ -30,11 +30,11 @@ export default defineEventHandler(async (event) => {
})

Object.assign(effect, {
name,
statement,
name: name ?? effect.name,
isSilence: isSilence ?? effect.isSilence,
statement: statement ?? effect.statement,
modifiedBy: sessionUser,
lastModified: new Date(),
...(isSilence !== undefined && { isSilence })
})

await em.persistAndFlush(effect)
Expand Down
Loading

0 comments on commit 76bfbd5

Please sign in to comment.