diff --git a/_old/domain/entities/Limit.mts b/_old/domain/entities/Limit.mts deleted file mode 100644 index cb447c50..00000000 --- a/_old/domain/entities/Limit.mts +++ /dev/null @@ -1,3 +0,0 @@ -import Requirement from './Requirement.mjs'; - -export default class Limit extends Requirement { } \ No newline at end of file diff --git a/src/data/LimitRepository.mts b/src/data/LimitRepository.mts new file mode 100644 index 00000000..d6d15a50 --- /dev/null +++ b/src/data/LimitRepository.mts @@ -0,0 +1,8 @@ +import Limit from '~/domain/Limit.mjs'; +import { LocalStorageRepository } from './LocalStorageRepository.mjs'; +import LimitToJsonMapper from '~/mappers/LimitToJsonMapper.mjs'; +import pkg from '~/../package.json' with { type: 'json' }; + +export default class UseCaseRepository extends LocalStorageRepository { + constructor() { super('limits', new LimitToJsonMapper(pkg.version)); } +} \ No newline at end of file diff --git a/src/domain/Goals.mts b/src/domain/Goals.mts index 1c345848..4e6a68a7 100644 --- a/src/domain/Goals.mts +++ b/src/domain/Goals.mts @@ -17,6 +17,7 @@ export default class Goals extends PEGS { stakeholders: Uuid[]; situation: string; useCases: Uuid[]; + limits: Uuid[]; constructor(options: Properties) { super(options); @@ -26,5 +27,6 @@ export default class Goals extends PEGS { this.stakeholders = options.stakeholders; this.situation = options.situation; this.useCases = options.useCases; + this.limits = options.limits; } } \ No newline at end of file diff --git a/src/domain/Limit.mts b/src/domain/Limit.mts new file mode 100644 index 00000000..ee1b2166 --- /dev/null +++ b/src/domain/Limit.mts @@ -0,0 +1,12 @@ +import type { Properties } from '~/types/Properties.mjs'; +import Requirement from './Requirement.mjs'; + +/** + * A Limit is a requirement describing a property that is out-of-scope. + * Example: "Providing an interface to the user to change the color of the background is out-of-scope." + */ +export default class Limit extends Requirement { + constructor(properties: Properties) { + super(properties); + } +} \ No newline at end of file diff --git a/src/mappers/GoalsToJsonMapper.mts b/src/mappers/GoalsToJsonMapper.mts index c13c0da9..989a2bca 100644 --- a/src/mappers/GoalsToJsonMapper.mts +++ b/src/mappers/GoalsToJsonMapper.mts @@ -9,6 +9,7 @@ export interface GoalsJson extends PEGSJson { situation: string; stakeholders: Uuid[]; useCases: Uuid[]; + limits: Uuid[]; } export default class GoalsToJsonMapper extends PEGSToJsonMapper { @@ -18,7 +19,8 @@ export default class GoalsToJsonMapper extends PEGSToJsonMapper { if (version.startsWith('0.3.')) return new Goals({ ...target, - useCases: target.useCases ?? [] + useCases: target.useCases ?? [], + limits: target.limits ?? [] }); throw new Error(`Unsupported serialization version: ${version}`); @@ -31,7 +33,8 @@ export default class GoalsToJsonMapper extends PEGSToJsonMapper { outcomes: source.outcomes, situation: source.situation, stakeholders: source.stakeholders, - useCases: source.useCases + useCases: source.useCases, + limits: source.limits }; } } \ No newline at end of file diff --git a/src/mappers/LimitToJsonMapper.mts b/src/mappers/LimitToJsonMapper.mts new file mode 100644 index 00000000..6c2b26a8 --- /dev/null +++ b/src/mappers/LimitToJsonMapper.mts @@ -0,0 +1,18 @@ +import Limit from '~/domain/Limit.mjs'; +import RequirementToJsonMapper, { type RequirementJson } from './RequirementToJsonMapper.mjs'; + +export interface LimitJson extends RequirementJson { } + +export default class BehaviorToJsonMapper extends RequirementToJsonMapper { + override mapFrom(target: LimitJson): Limit { + const version = target.serializationVersion ?? '{undefined}'; + + if (version.startsWith('0.3.')) + return new Limit(target); + + throw new Error(`Unsupported serialization version: ${version}`); + } + override mapTo(source: Limit): LimitJson { + return super.mapTo(source); + } +} \ No newline at end of file diff --git a/src/presentation/Application.mts b/src/presentation/Application.mts index 7a49157c..10b2a828 100644 --- a/src/presentation/Application.mts +++ b/src/presentation/Application.mts @@ -77,7 +77,8 @@ export default class Application extends Container { ['/goals/:slug/rationale', (await import('./pages/goals/Rationale.mjs')).Rationale], ['/goals/:slug/functionality', (await import('./pages/goals/Functionality.mjs')).Functionality], ['/goals/:slug/stakeholders', (await import('./pages/goals/Stakeholders.mjs')).Stakeholders], - ['/goals/:slug/use-cases', (await import('./pages/goals/UseCases.mjs')).UseCases] + ['/goals/:slug/use-cases', (await import('./pages/goals/UseCases.mjs')).UseCases], + ['/goals/:slug/limitations', (await import('./pages/goals/Limitations.mjs')).Limitations], ]); this.#router.addEventListener('route', this); } diff --git a/src/presentation/pages/goals/Goal.mts b/src/presentation/pages/goals/Goal.mts index 425eba3a..2d3a8590 100644 --- a/src/presentation/pages/goals/Goal.mts +++ b/src/presentation/pages/goals/Goal.mts @@ -28,6 +28,11 @@ export class Goal extends Page { title: 'Use Cases', icon: 'briefcase', href: `${location.pathname}/use-cases` + }), + new MiniCard({ + title: 'Limitations', + icon: 'x-circle', + href: `${location.pathname}/limitations` }) ]) ]); diff --git a/src/presentation/pages/goals/Limitations.mts b/src/presentation/pages/goals/Limitations.mts new file mode 100644 index 00000000..b0154f34 --- /dev/null +++ b/src/presentation/pages/goals/Limitations.mts @@ -0,0 +1,68 @@ +import Limit from '~/domain/Limit.mjs'; +import type Goals from '~/domain/Goals.mjs'; +import GoalsRepository from '~/data/GoalsRepository.mjs'; +import LimitRepository from '~/data/LimitRepository.mjs'; +import html from '~/presentation/lib/html.mjs'; +import { DataTable } from '~/presentation/components/DataTable.mjs'; +import { SlugPage } from '../SlugPage.mjs'; + +const { p } = html; + +export class Limitations extends SlugPage { + static { + customElements.define('x-limitations-page', this); + } + + #goalsRepository = new GoalsRepository(); + #limitRepository = new LimitRepository(); + #goals?: Goals; + + constructor() { + super({ title: 'Limitations' }, [ + p([ + `Limitations are the constraints on functionality. + They describe What that is out-of-scope and excluded. + Example: "Providing an interface to the user to change + the color of the background is out-of-scope." + ` + ]) + ]); + + const dataTable = new DataTable({ + columns: { + id: { headerText: 'ID', readonly: true, formType: 'hidden' }, + statement: { headerText: 'Statement', required: true, formType: 'text' } + }, + select: async () => { + if (!this.#goals) + return []; + + return await this.#limitRepository.getAll(l => this.#goals!.limits.includes(l.id)); + }, + onCreate: async item => { + const limit = new Limit({ ...item, id: self.crypto.randomUUID() }); + await this.#limitRepository.add(limit); + this.#goals!.limits.push(limit.id); + await this.#goalsRepository.update(this.#goals!); + }, + onUpdate: async item => { + const limit = (await this.#limitRepository.get(item.id))!; + limit.statement = item.statement; + await this.#limitRepository.update(limit); + }, + onDelete: async id => { + await this.#limitRepository.delete(id); + this.#goals!.limits = this.#goals!.limits.filter(x => x !== id); + await this.#goalsRepository.update(this.#goals!); + } + }); + this.append(dataTable); + + this.#goalsRepository.addEventListener('update', () => dataTable.renderData()); + this.#limitRepository.addEventListener('update', () => dataTable.renderData()); + this.#goalsRepository.getBySlug(this.slug).then(goals => { + this.#goals = goals; + dataTable.renderData(); + }); + } +} \ No newline at end of file diff --git a/src/presentation/pages/goals/NewGoals.mts b/src/presentation/pages/goals/NewGoals.mts index 33b41f94..f84663e4 100644 --- a/src/presentation/pages/goals/NewGoals.mts +++ b/src/presentation/pages/goals/NewGoals.mts @@ -93,7 +93,8 @@ export class NewGoals extends Page { situation: '', stakeholders: [], functionalBehaviors: [], - useCases: [] + useCases: [], + limits: [] }); await this.#repository.add(goals);