Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/electron-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ jobs:
git config user.email "ci@github-actions"
git config user.name "GitHub Actions"

- name: Bump minor version and push
- name: Bump patch version and push
id: bump
run: |
npm version minor --no-git-tag-version
npm version patch --no-git-tag-version
VERSION=$(node -p "require('./package.json').version")
echo "version=${VERSION}" >> $GITHUB_OUTPUT
git add package.json package-lock.json
Expand Down
82 changes: 82 additions & 0 deletions src/components/questionaire/EntryExamples.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<template>
<div v-if="items.length" class="text--secondary text-sm mt-1">
<strong>Examples: </strong>
<span v-for="(example, eIdx) in items" :key="`ex-${eIdx}`">
<v-tooltip v-if="example.description" :text="example.description" location="top">
<template v-slot:activator="{ props }">
<span v-bind="props" class="example-item">{{ example.label }}</span>
</template>
</v-tooltip>
<span v-else class="example-item">{{ example.label }}</span>
<span v-if="eIdx < items.length - 1">, </span>
</span>
</div>
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps({
examples: {
type: [Array, String],
default: null
},
entryId: {
type: String,
required: true
}
})

// Memoized: computed once per entry, not 3 times
const items = computed(() => {
if (!props.examples) return []

if (Array.isArray(props.examples)) {
return props.examples
.map((example) => {
if (typeof example === 'string') {
const label = example.trim()
return label ? { label, description: '' } : null
}
if (example && typeof example === 'object') {
const label = String(example.label || '').trim()
if (!label) return null

let description = example.description || ''

// Append tools in parentheses if tools array exists and has items
if (Array.isArray(example.tools) && example.tools.length > 0) {
const toolsText = example.tools.join(', ')
description = description.trim()
// Remove trailing period if present before adding tools
if (description.endsWith('.')) {
description = description.slice(0, -1)
}
description = `${description} (${toolsText}).`
}

return { label, description }
}
return null
})
.filter(Boolean)
}

if (typeof props.examples === 'string') {
return props.examples
.split(',')
.map((item) => item.trim())
.filter(Boolean)
.map((label) => ({ label, description: '' }))
}

return []
})
</script>

<style scoped>
.example-item {
cursor: help;
text-decoration: underline dotted;
}
</style>
116 changes: 60 additions & 56 deletions src/components/questionaire/Questionnaire.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,28 +90,36 @@
<v-col cols="12">
<v-text-field
label="Software Product"
v-model="currentCategory.metadata.productName"
:model-value="currentCategory.metadata.productName"
@blur="currentCategory.metadata.productName = $event.target.value"
@click:clear="currentCategory.metadata.productName = ''"
clearable
/>
</v-col>
<v-col cols="12">
<v-text-field
label="Company"
v-model="currentCategory.metadata.company"
:model-value="currentCategory.metadata.company"
@blur="currentCategory.metadata.company = $event.target.value"
@click:clear="currentCategory.metadata.company = ''"
clearable
/>
</v-col>
<v-col cols="12">
<v-text-field
label="Department"
v-model="currentCategory.metadata.department"
:model-value="currentCategory.metadata.department"
@blur="currentCategory.metadata.department = $event.target.value"
@click:clear="currentCategory.metadata.department = ''"
clearable
/>
</v-col>
<v-col cols="12">
<v-text-field
label="Contact Person"
v-model="currentCategory.metadata.contactPerson"
:model-value="currentCategory.metadata.contactPerson"
@blur="currentCategory.metadata.contactPerson = $event.target.value"
@click:clear="currentCategory.metadata.contactPerson = ''"
clearable
/>
</v-col>
Expand Down Expand Up @@ -150,7 +158,8 @@
<v-col cols="12">
<v-textarea
label="Description"
v-model="currentCategory.metadata.description"
:model-value="currentCategory.metadata.description"
@blur="currentCategory.metadata.description = $event.target.value"
placeholder="Brief description of the software product..."
rows="4"
auto-grow
Expand Down Expand Up @@ -220,18 +229,7 @@
</v-tooltip>
</div>
<div v-if="entry.description" class="text-body-2 mt-1" v-html="renderTextWithLinks(entry.description)"></div>
<div v-if="getExampleItems(entry.examples).length" class="text--secondary text-sm mt-1">
<strong>Examples: </strong>
<span v-for="(example, eIdx) in getExampleItems(entry.examples)" :key="`${entry.id}-ex-${eIdx}`">
<v-tooltip v-if="example.description" :text="example.description" location="top">
<template v-slot:activator="{ props }">
<span v-bind="props" class="example-item">{{ example.label }}</span>
</template>
</v-tooltip>
<span v-else class="example-item">{{ example.label }}</span>
<span v-if="eIdx < getExampleItems(entry.examples).length - 1">, </span>
</span>
</div>
<EntryExamples v-if="entry.examples" :examples="entry.examples" :entry-id="entry.id" />
</div>
<div class="ml-4" style="min-width: 190px;">
<v-select
Expand All @@ -257,7 +255,8 @@
<div class="mt-4">
<v-textarea
label="General Comment"
v-model="entry.entryComment"
:model-value="entry.entryComment"
@blur="entry.entryComment = $event.target.value"
rows="2"
density="compact"
variant="outlined"
Expand Down Expand Up @@ -335,7 +334,8 @@
<v-col cols="12">
<v-textarea
label="Comment"
v-model="answer.comments"
:model-value="answer.comments"
@blur="answer.comments = $event.target.value"
rows="2"
class="resizable-textarea"
/>
Expand Down Expand Up @@ -372,8 +372,10 @@
<script>
import { computed, ref, watch, nextTick, onMounted } from 'vue'
import { useWorkspaceStore } from '../../stores/workspaceStore'
import EntryExamples from './EntryExamples.vue'

export default {
components: { EntryExamples },
props: {
categories: {
type: Array,
Expand Down Expand Up @@ -408,6 +410,21 @@ export default {
'not applicable': 'Entry does not apply to this solution.',
unknown: 'Applicability is not known yet.'
}

// Static options - don't need to be recomputed
const applicabilityFilterOptions = [
{ title: 'All', value: 'all' },
...store.applicabilityOptions.map((label) => ({
title: label.charAt(0).toUpperCase() + label.slice(1),
value: label
}))
]

const applicabilityItems = store.applicabilityOptions.map((label) => ({
label,
description: applicabilityDescriptions[label] || ''
}))

const metadataCategory = computed(() => props.categories.find((category) => category.isMetadata) || null)
const metadataValue = computed(() => metadataCategory.value?.metadata || null)
const architecturalRoleValue = computed(() => metadataValue.value?.architecturalRole || '')
Expand Down Expand Up @@ -523,22 +540,32 @@ export default {
return result.filter((entry) => hiddenEntries.value.has(entry.id)).length
})

const applicabilityFilterOptions = computed(() => {
return [
{ title: 'All', value: 'all' },
...store.applicabilityOptions.map((label) => ({
title: label.charAt(0).toUpperCase() + label.slice(1),
value: label
}))
]
// Cache for category visibility to avoid recalculating on every render
const categoryVisibilityCache = computed(() => {
const cache = new Map()
visibleCategories.value.forEach(cat => {
if (cat.isMetadata) {
cache.set(cat.id, true)
} else {
const entries = Array.isArray(cat.entries) ? cat.entries : []
const filtered = entries.filter((entry) => appliesToMatches(entry.appliesTo, metadataValue.value))

if (applicabilityFilter.value === 'all') {
cache.set(cat.id, filtered.length > 0)
} else {
const withApplicability = filtered.filter((entry) =>
(entry.applicability || 'applicable') === applicabilityFilter.value
)
cache.set(cat.id, withApplicability.length > 0)
}
}
})
return cache
})

const applicabilityItems = computed(() => {
return store.applicabilityOptions.map((label) => ({
label,
description: applicabilityDescriptions[label] || ''
}))
})
function categoryHasVisibleEntries(category) {
return categoryVisibilityCache.value.get(category.id) ?? false
}

const hasNext = computed(() => {
return visibleCategories.value.findIndex((category) => category.id === activeCategoryId.value) < visibleCategories.value.length - 1
Expand Down Expand Up @@ -607,24 +634,6 @@ export default {
}
}

function categoryHasVisibleEntries(category) {
if (category.isMetadata) return true

const entries = Array.isArray(category.entries) ? category.entries : []
const filteredByMetadata = entries.filter((entry) => appliesToMatches(entry.appliesTo, metadataValue.value))

if (applicabilityFilter.value === 'all') {
return filteredByMetadata.length > 0
}

const filteredByApplicability = filteredByMetadata.filter((entry) => {
const entryApplicability = entry.applicability || 'applicable'
return entryApplicability === applicabilityFilter.value
})

return filteredByApplicability.length > 0
}

function setAllApplicability(value) {
if (!value || currentCategory.value.isMetadata) return

Expand Down Expand Up @@ -681,7 +690,6 @@ export default {
applicabilityFilterOptions,
getStatusTooltip: store.getStatusTooltip,
renderTextWithLinks: store.renderTextWithLinks,
getExampleItems: store.getExampleItems,
isEntryApplicable: store.isEntryApplicable,
setApplicability: store.setApplicability,
addAnswer: store.addAnswer,
Expand Down Expand Up @@ -709,10 +717,6 @@ export default {

<style scoped>
.font-weight-medium { font-weight: 500; }
.example-item {
cursor: help;
text-decoration: underline dotted;
}

.resizable-textarea :deep(textarea) {
resize: vertical;
Expand Down
2 changes: 1 addition & 1 deletion src/stores/workspaceStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
() => [workspace.value, activeQuestionnaireId.value, openQuestionnaireIds.value, activeWorkspaceTabId.value, openProjectSummaryIds.value, questionnaireHiddenEntries.value],
() => {
clearTimeout(persistDebounceTimer)
persistDebounceTimer = setTimeout(() => persist(), 500)
persistDebounceTimer = setTimeout(() => persist(), 1500)
},
{ deep: true }
)
Expand Down