From fcd1533c63a5c84dafd7b9b1a877fada89f94bd4 Mon Sep 17 00:00:00 2001 From: Michael Haufe Date: Wed, 10 Jan 2024 18:00:33 -0600 Subject: [PATCH] Completed implementation of Solution and refactoring --- src/data/EnvironmentRepository.mts | 4 +- src/data/GoalsRepository.mts | 4 +- src/data/ProjectRepository.mts | 4 +- ...{PEGSRepository.mts => SlugRepository.mts} | 4 +- src/data/SolutionRepository.mts | 11 ++ src/data/SystemRepository.mts | 11 ++ src/domain/Environment.mts | 8 +- src/domain/Goals.mts | 20 ++-- src/domain/PEGS.mts | 54 --------- src/domain/Project.mts | 5 +- src/domain/SlugEntity.mts | 45 ++++++++ src/domain/Solution.mts | 15 +++ src/domain/System.mts | 8 ++ src/lib/slugify.mts | 5 + src/mappers/EnvironmentToJsonMapper.mts | 10 +- src/mappers/GoalsToJsonMapper.mts | 26 ++--- src/mappers/ProjectToJsonMapper.mts | 6 +- ...nMapper.mts => SlugEntityToJsonMapper.mts} | 12 +- src/mappers/SolutionToJsonMapper.mts | 37 ++++++ src/mappers/SystemToJsonMapper.mts | 23 ++++ src/presentation/Application.mts | 25 ++-- src/presentation/components/GlobalNav.mts | 10 +- src/presentation/components/PegsCards.mts | 14 +-- src/presentation/pages/HomePage.mts | 20 +++- src/presentation/pages/NotFoundPage.mts | 2 +- .../pages/environments/EnvironmentPage.mts | 21 ---- .../pages/environments/EnvironmentsPage.mts | 26 ----- src/presentation/pages/goals/GoalsPage.mts | 23 ---- src/presentation/pages/goals/NewGoalsPage.mts | 108 ------------------ .../pages/projects/ProjectsPage.mts | 17 --- .../NewSolutionPage.mts} | 81 +++++++++---- .../pages/solution/SolutionIndexPage.mts | 17 +++ .../pages/solution/SolutionPage.mts | 36 ++++++ .../environment/EnvironmentsIndexPage.mts | 32 ++++++ .../environment}/GlossaryPage.mts | 24 ++-- .../goals/FunctionalityPage.mts | 23 ++-- .../goals/GoalsIndexPage.mts} | 22 ++-- .../{ => solution}/goals/LimitationsPage.mts | 25 ++-- .../{ => solution}/goals/RationalePage.mts | 31 +++-- .../{ => solution}/goals/StakeholdersPage.mts | 26 +++-- .../{ => solution}/goals/UseCasesPage.mts | 23 ++-- .../solution/project/ProjectsIndexPage.mts | 17 +++ 42 files changed, 512 insertions(+), 423 deletions(-) rename src/data/{PEGSRepository.mts => SlugRepository.mts} (74%) create mode 100644 src/data/SolutionRepository.mts create mode 100644 src/data/SystemRepository.mts delete mode 100644 src/domain/PEGS.mts create mode 100644 src/domain/SlugEntity.mts create mode 100644 src/domain/Solution.mts create mode 100644 src/domain/System.mts create mode 100644 src/lib/slugify.mts rename src/mappers/{PEGSToJsonMapper.mts => SlugEntityToJsonMapper.mts} (59%) create mode 100644 src/mappers/SolutionToJsonMapper.mts create mode 100644 src/mappers/SystemToJsonMapper.mts delete mode 100644 src/presentation/pages/environments/EnvironmentPage.mts delete mode 100644 src/presentation/pages/environments/EnvironmentsPage.mts delete mode 100644 src/presentation/pages/goals/GoalsPage.mts delete mode 100644 src/presentation/pages/goals/NewGoalsPage.mts delete mode 100644 src/presentation/pages/projects/ProjectsPage.mts rename src/presentation/pages/{environments/NewEnvironmentPage.mts => solution/NewSolutionPage.mts} (52%) create mode 100644 src/presentation/pages/solution/SolutionIndexPage.mts create mode 100644 src/presentation/pages/solution/SolutionPage.mts create mode 100644 src/presentation/pages/solution/environment/EnvironmentsIndexPage.mts rename src/presentation/pages/{environments => solution/environment}/GlossaryPage.mts (71%) rename src/presentation/pages/{ => solution}/goals/FunctionalityPage.mts (73%) rename src/presentation/pages/{goals/GoalPage.mts => solution/goals/GoalsIndexPage.mts} (66%) rename src/presentation/pages/{ => solution}/goals/LimitationsPage.mts (73%) rename src/presentation/pages/{ => solution}/goals/RationalePage.mts (74%) rename src/presentation/pages/{ => solution}/goals/StakeholdersPage.mts (85%) rename src/presentation/pages/{ => solution}/goals/UseCasesPage.mts (84%) create mode 100644 src/presentation/pages/solution/project/ProjectsIndexPage.mts diff --git a/src/data/EnvironmentRepository.mts b/src/data/EnvironmentRepository.mts index c0b43994..d5605718 100644 --- a/src/data/EnvironmentRepository.mts +++ b/src/data/EnvironmentRepository.mts @@ -1,10 +1,10 @@ import Environment from '~/domain/Environment.mjs'; -import PEGSRepository from './PEGSRepository.mjs'; +import StorageRepository from './StorageRepository.mjs'; import EnvironmentToJsonMapper from '~/mappers/EnvironmentToJsonMapper.mjs'; import pkg from '~/../package.json' with { type: 'json' }; import type { SemVerString } from '~/lib/SemVer.mjs'; -export default class EnvironmentRepository extends PEGSRepository { +export default class EnvironmentRepository extends StorageRepository { constructor(storage: Storage) { super('environments', storage, new EnvironmentToJsonMapper(pkg.version as SemVerString)); } diff --git a/src/data/GoalsRepository.mts b/src/data/GoalsRepository.mts index abe2ed56..a6ef5428 100644 --- a/src/data/GoalsRepository.mts +++ b/src/data/GoalsRepository.mts @@ -1,10 +1,10 @@ import Goals from '~/domain/Goals.mjs'; -import PEGSRepository from './PEGSRepository.mjs'; +import StorageRepository from './StorageRepository.mjs'; import GoalsToJsonMapper from '~/mappers/GoalsToJsonMapper.mjs'; import pkg from '~/../package.json' with { type: 'json' }; import type { SemVerString } from '~/lib/SemVer.mjs'; -export default class GoalsRepository extends PEGSRepository { +export default class GoalsRepository extends StorageRepository { constructor(storage: Storage) { super('goals', storage, new GoalsToJsonMapper(pkg.version as SemVerString)); } diff --git a/src/data/ProjectRepository.mts b/src/data/ProjectRepository.mts index a84154e3..94bd8a0c 100644 --- a/src/data/ProjectRepository.mts +++ b/src/data/ProjectRepository.mts @@ -1,10 +1,10 @@ import Project from '~/domain/Project.mjs'; -import PEGSRepository from './PEGSRepository.mjs'; +import StorageRepository from './StorageRepository.mjs'; import ProjectToJsonMapper from '~/mappers/ProjectToJsonMapper.mjs'; import pkg from '~/../package.json' with { type: 'json' }; import type { SemVerString } from '~/lib/SemVer.mjs'; -export default class ProjectRepository extends PEGSRepository { +export default class ProjectRepository extends StorageRepository { constructor(storage: Storage) { super('projects', storage, new ProjectToJsonMapper(pkg.version as SemVerString)); } diff --git a/src/data/PEGSRepository.mts b/src/data/SlugRepository.mts similarity index 74% rename from src/data/PEGSRepository.mts rename to src/data/SlugRepository.mts index 5180dd2b..257eea27 100644 --- a/src/data/PEGSRepository.mts +++ b/src/data/SlugRepository.mts @@ -1,9 +1,9 @@ -import type PEGS from '~/domain/PEGS.mjs'; +import type SlugEntity from '~/domain/SlugEntity.mjs'; import StorageRepository from './StorageRepository.mjs'; import type Mapper from '~/usecases/Mapper.mjs'; import type { EntityJson } from '~/mappers/EntityToJsonMapper.mjs'; -export default abstract class PEGSRepository extends StorageRepository { +export default abstract class SlugRepository extends StorageRepository { constructor(storageKey: string, storage: Storage, mapper: Mapper) { super(storageKey, storage, mapper); } diff --git a/src/data/SolutionRepository.mts b/src/data/SolutionRepository.mts new file mode 100644 index 00000000..25424db8 --- /dev/null +++ b/src/data/SolutionRepository.mts @@ -0,0 +1,11 @@ +import type Solution from '~/domain/Solution.mjs'; +import SolutionToJsonMapper from '~/mappers/SolutionToJsonMapper.mjs'; +import pkg from '~/../package.json' with { type: 'json' }; +import type { SemVerString } from '~/lib/SemVer.mjs'; +import SlugRepository from './SlugRepository.mjs'; + +export default class SolutionRepository extends SlugRepository { + constructor(storage: Storage) { + super('solution', storage, new SolutionToJsonMapper(pkg.version as SemVerString)); + } +} \ No newline at end of file diff --git a/src/data/SystemRepository.mts b/src/data/SystemRepository.mts new file mode 100644 index 00000000..749c5076 --- /dev/null +++ b/src/data/SystemRepository.mts @@ -0,0 +1,11 @@ +import type System from '~/domain/System.mjs'; +import type { SemVerString } from '~/lib/SemVer.mjs'; +import StorageRepository from './StorageRepository.mjs'; +import pkg from '~/../package.json' with { type: 'json' }; +import SystemToJsonMapper from '~/mappers/SystemToJsonMapper.mjs'; + +export default class SystemRepository extends StorageRepository { + constructor(storage: Storage) { + super('system', storage, new SystemToJsonMapper(pkg.version as SemVerString)); + } +} \ No newline at end of file diff --git a/src/domain/Environment.mts b/src/domain/Environment.mts index 9a5499e2..3bda6db4 100644 --- a/src/domain/Environment.mts +++ b/src/domain/Environment.mts @@ -1,5 +1,5 @@ import type { Uuid } from '~/types/Uuid.mjs'; -import PEGS from './PEGS.mjs'; +import Entity from './Entity.mjs'; import type { Properties } from '~/types/Properties.mjs'; /** @@ -9,11 +9,11 @@ import type { Properties } from '~/types/Properties.mjs'; * An environment describes the application domain and external context in which a * system operates. */ -export default class Environment extends PEGS { - glossary: Uuid[]; +export default class Environment extends Entity { + glossaryIds: Uuid[]; constructor(options: Properties) { super(options); - this.glossary = options.glossary; + this.glossaryIds = options.glossaryIds; } } \ No newline at end of file diff --git a/src/domain/Goals.mts b/src/domain/Goals.mts index 4e6a68a7..989f0c6c 100644 --- a/src/domain/Goals.mts +++ b/src/domain/Goals.mts @@ -1,32 +1,32 @@ import type { Properties } from '~/types/Properties.mjs'; import type { Uuid } from '~/types/Uuid.mjs'; -import PEGS from './PEGS.mjs'; +import Entity from './Entity.mjs'; /** * Goals are the needs and wants of an organization. * They are the things that the organization wants to achieve. */ -export default class Goals extends PEGS { +export default class Goals extends Entity { /** * Functional behaviors specify what results or effects are expected from the system. * They specify "what" the system should do, not "how" it should do it. */ - functionalBehaviors: Uuid[]; + functionalBehaviorIds: Uuid[]; objective: string; outcomes: string; - stakeholders: Uuid[]; + stakeholderIds: Uuid[]; situation: string; - useCases: Uuid[]; - limits: Uuid[]; + useCaseIds: Uuid[]; + limitIds: Uuid[]; constructor(options: Properties) { super(options); - this.functionalBehaviors = options.functionalBehaviors; + this.functionalBehaviorIds = options.functionalBehaviorIds; this.objective = options.objective; this.outcomes = options.outcomes; - this.stakeholders = options.stakeholders; + this.stakeholderIds = options.stakeholderIds; this.situation = options.situation; - this.useCases = options.useCases; - this.limits = options.limits; + this.useCaseIds = options.useCaseIds; + this.limitIds = options.limitIds; } } \ No newline at end of file diff --git a/src/domain/PEGS.mts b/src/domain/PEGS.mts deleted file mode 100644 index 2b0db28f..00000000 --- a/src/domain/PEGS.mts +++ /dev/null @@ -1,54 +0,0 @@ -import type { Properties } from '~/types/Properties.mjs'; -import Entity from './Entity.mjs'; - -/** - * The base class for all PEGS (Project, Environment, Goal, System) - */ -export default class PEGS extends Entity { - static readonly maxNameLength = 60; - static readonly maxDescriptionLength = 200; - - static slugify(str: string) { - return str.toLowerCase().trim() - .replace(/\s/g, '-') - .replace(/[^\w-]+/g, '') - .replace(/--+/g, '-'); - } - - private _name!: string; - private _description!: string; - - constructor(options: Properties) { - super(options); - this.name = options.name; - this.description = options.description; - } - - get name(): string { - return this._name; - } - - set name(value: string) { - const trimmed = value.trim(), - Clazz = this.constructor as typeof PEGS; - if (trimmed.length >= Clazz.maxNameLength) - throw new Error('Project name cannot be longer than 60 characters'); - this._name = trimmed; - } - - get description(): string { - return this._description; - } - - set description(value: string) { - const trimmed = value.trim(), - Clazz = this.constructor as typeof PEGS; - if (trimmed.length >= Clazz.maxDescriptionLength) - throw new Error('Project description cannot be longer than 200 characters'); - this._description = trimmed; - } - - slug(): string { - return PEGS.slugify(this.name); - } -} \ No newline at end of file diff --git a/src/domain/Project.mts b/src/domain/Project.mts index 8d0844be..489f66e6 100644 --- a/src/domain/Project.mts +++ b/src/domain/Project.mts @@ -1,8 +1,7 @@ -import PEGS from './PEGS.mjs'; +import Entity from './Entity.mjs'; /** * A Project is the set of human processes involved in the planning, * construction, revision, and operation of an associated system. - * @extends PEGS */ -export default class Project extends PEGS { } \ No newline at end of file +export default class Project extends Entity { } \ No newline at end of file diff --git a/src/domain/SlugEntity.mts b/src/domain/SlugEntity.mts new file mode 100644 index 00000000..acee47f0 --- /dev/null +++ b/src/domain/SlugEntity.mts @@ -0,0 +1,45 @@ +import type { Properties } from '~/types/Properties.mjs'; +import slugify from '~/lib/slugify.mjs'; +import Entity from './Entity.mjs'; + +export default class SlugEntity extends Entity { + static readonly maxNameLength = 60; + static readonly maxDescriptionLength = 200; + + #name!: string; + #description!: string; + + constructor(options: Properties) { + super(options); + this.name = options.name; + this.description = options.description; + } + + get name(): string { + return this.#name; + } + + set name(value: string) { + const trimmed = value.trim(), + Clazz = this.constructor as typeof SlugEntity; + if (trimmed.length >= Clazz.maxNameLength) + throw new Error('Entity name cannot be longer than 60 characters'); + this.#name = trimmed; + } + + get description(): string { + return this.#description; + } + + set description(value: string) { + const trimmed = value.trim(), + Clazz = this.constructor as typeof SlugEntity; + if (trimmed.length >= Clazz.maxDescriptionLength) + throw new Error('Project description cannot be longer than 200 characters'); + this.#description = trimmed; + } + + slug(): string { + return slugify(this.name); + } +} \ No newline at end of file diff --git a/src/domain/Solution.mts b/src/domain/Solution.mts new file mode 100644 index 00000000..20137049 --- /dev/null +++ b/src/domain/Solution.mts @@ -0,0 +1,15 @@ +import type { Properties } from '~/types/Properties.mjs'; +import type { Uuid } from '~/types/Uuid.mjs'; +import SlugEntity from './SlugEntity.mjs'; + +export default class Solution extends SlugEntity { + projectId!: Uuid; + environmentId!: Uuid; + goalsId!: Uuid; + systemId!: Uuid; + + constructor(options: Properties) { + super(options); + Object.assign(this, options); + } +} \ No newline at end of file diff --git a/src/domain/System.mts b/src/domain/System.mts new file mode 100644 index 00000000..b435e5b9 --- /dev/null +++ b/src/domain/System.mts @@ -0,0 +1,8 @@ +import type { Properties } from '~/types/Properties.mjs'; +import Entity from './Entity.mjs'; + +export default class System extends Entity { + constructor(properties: Properties) { + super(properties); + } +} \ No newline at end of file diff --git a/src/lib/slugify.mts b/src/lib/slugify.mts new file mode 100644 index 00000000..9b633bda --- /dev/null +++ b/src/lib/slugify.mts @@ -0,0 +1,5 @@ +export default (str: string) => + str.toLowerCase().trim() + .replace(/\s/g, '-') + .replace(/[^\w-]+/g, '') + .replace(/--+/g, '-'); \ No newline at end of file diff --git a/src/mappers/EnvironmentToJsonMapper.mts b/src/mappers/EnvironmentToJsonMapper.mts index 42712a2f..88577e68 100644 --- a/src/mappers/EnvironmentToJsonMapper.mts +++ b/src/mappers/EnvironmentToJsonMapper.mts @@ -1,13 +1,13 @@ import type { Uuid } from '~/types/Uuid.mjs'; -import PEGSToJsonMapper, { type PEGSJson } from './PEGSToJsonMapper.mjs'; +import EntityToJsonMapper, { type EntityJson } from './EntityToJsonMapper.mjs'; import Environment from '~/domain/Environment.mjs'; import SemVer from '~/lib/SemVer.mjs'; -export interface EnvironmentJson extends PEGSJson { - glossary: Uuid[]; +export interface EnvironmentJson extends EntityJson { + glossaryIds: Uuid[]; } -export default class EnvironmentToJsonMapper extends PEGSToJsonMapper { +export default class EnvironmentToJsonMapper extends EntityToJsonMapper { override mapFrom(target: EnvironmentJson): Environment { const version = new SemVer(target.serializationVersion); @@ -20,7 +20,7 @@ export default class EnvironmentToJsonMapper extends PEGSToJsonMapper { override mapTo(source: Environment): EnvironmentJson { return { ...super.mapTo(source), - glossary: source.glossary + glossaryIds: source.glossaryIds }; } } \ No newline at end of file diff --git a/src/mappers/GoalsToJsonMapper.mts b/src/mappers/GoalsToJsonMapper.mts index 530b0574..ad903dd7 100644 --- a/src/mappers/GoalsToJsonMapper.mts +++ b/src/mappers/GoalsToJsonMapper.mts @@ -1,27 +1,27 @@ import type { Uuid } from '~/types/Uuid.mjs'; -import PEGSToJsonMapper, { type PEGSJson } from './PEGSToJsonMapper.mjs'; +import EntityToJsonMapper, { type EntityJson } from './EntityToJsonMapper.mjs'; import Goals from '~/domain/Goals.mjs'; import SemVer from '~/lib/SemVer.mjs'; -export interface GoalsJson extends PEGSJson { - functionalBehaviors: Uuid[]; +export interface GoalsJson extends EntityJson { + functionalBehaviorIds: Uuid[]; objective: string; outcomes: string; situation: string; - stakeholders: Uuid[]; - useCases: Uuid[]; - limits: Uuid[]; + stakeholderIds: Uuid[]; + useCaseIds: Uuid[]; + limitIds: Uuid[]; } -export default class GoalsToJsonMapper extends PEGSToJsonMapper { +export default class GoalsToJsonMapper extends EntityToJsonMapper { override mapFrom(target: GoalsJson): Goals { const version = new SemVer(target.serializationVersion); if (version.gte('0.3.0')) return new Goals({ ...target, - useCases: target.useCases ?? [], - limits: target.limits ?? [] + useCaseIds: target.useCaseIds ?? [], + limitIds: target.limitIds ?? [] }); throw new Error(`Unsupported serialization version: ${version}`); @@ -30,13 +30,13 @@ export default class GoalsToJsonMapper extends PEGSToJsonMapper { override mapTo(source: Goals): GoalsJson { return { ...super.mapTo(source), - functionalBehaviors: source.functionalBehaviors, + functionalBehaviorIds: source.functionalBehaviorIds, objective: source.objective, outcomes: source.outcomes, situation: source.situation, - stakeholders: source.stakeholders, - useCases: source.useCases, - limits: source.limits + stakeholderIds: source.stakeholderIds, + useCaseIds: source.useCaseIds, + limitIds: source.limitIds }; } } \ No newline at end of file diff --git a/src/mappers/ProjectToJsonMapper.mts b/src/mappers/ProjectToJsonMapper.mts index 4f262cd0..7230e6b1 100644 --- a/src/mappers/ProjectToJsonMapper.mts +++ b/src/mappers/ProjectToJsonMapper.mts @@ -1,10 +1,10 @@ import Project from '~/domain/Project.mjs'; -import PEGSToJsonMapper, { type PEGSJson } from './PEGSToJsonMapper.mjs'; +import EntityToJsonMapper, { type EntityJson } from './EntityToJsonMapper.mjs'; import SemVer from '~/lib/SemVer.mjs'; -export interface ProjectJson extends PEGSJson { } +export interface ProjectJson extends EntityJson { } -export default class ProjectToJsonMapper extends PEGSToJsonMapper { +export default class ProjectToJsonMapper extends EntityToJsonMapper { override mapFrom(target: ProjectJson): Project { const version = new SemVer(target.serializationVersion); diff --git a/src/mappers/PEGSToJsonMapper.mts b/src/mappers/SlugEntityToJsonMapper.mts similarity index 59% rename from src/mappers/PEGSToJsonMapper.mts rename to src/mappers/SlugEntityToJsonMapper.mts index 8b970b1c..4c7a5da3 100644 --- a/src/mappers/PEGSToJsonMapper.mts +++ b/src/mappers/SlugEntityToJsonMapper.mts @@ -1,23 +1,23 @@ -import PEGS from '~/domain/PEGS.mjs'; +import SlugEntity from '~/domain/SlugEntity.mjs'; import EntityToJsonMapper, { type EntityJson } from './EntityToJsonMapper.mjs'; import SemVer from '~/lib/SemVer.mjs'; -export interface PEGSJson extends EntityJson { +export interface SlugEntityJson extends EntityJson { name: string; description: string; } -export default class PEGSToJsonMapper extends EntityToJsonMapper { - override mapFrom(target: PEGSJson): PEGS { +export default class SlugEntityToJsonMapper extends EntityToJsonMapper { + override mapFrom(target: SlugEntityJson): SlugEntity { const version = new SemVer(target.serializationVersion); if (version.gte('0.3.0')) - return new PEGS(target); + return new SlugEntity(target); throw new Error(`Unsupported serialization version: ${version}`); } - override mapTo(source: PEGS): PEGSJson { + override mapTo(source: SlugEntity): SlugEntityJson { return { ...super.mapTo(source), name: source.name, diff --git a/src/mappers/SolutionToJsonMapper.mts b/src/mappers/SolutionToJsonMapper.mts new file mode 100644 index 00000000..3c3d5bf0 --- /dev/null +++ b/src/mappers/SolutionToJsonMapper.mts @@ -0,0 +1,37 @@ +import type { Uuid } from '~/types/Uuid.mjs'; +import type { EntityJson } from './EntityToJsonMapper.mjs'; +import EntityToJsonMapper from './EntityToJsonMapper.mjs'; +import Solution from '~/domain/Solution.mjs'; +import SemVer from '~/lib/SemVer.mjs'; + +export interface SolutionJson extends EntityJson { + name: string; + description: string; + projectId: Uuid; + environmentId: Uuid; + goalsId: Uuid; + systemId: Uuid; +} + +export default class SolutionToJsonMapper extends EntityToJsonMapper { + override mapFrom(target: SolutionJson): Solution { + const version = new SemVer(target.serializationVersion); + + if (version.gte('0.4.0')) + return new Solution(target); + + throw new Error(`Unsupported serialization version: ${version}`); + } + + override mapTo(source: Solution): SolutionJson { + return { + ...super.mapTo(source), + name: source.name, + description: source.description, + projectId: source.projectId, + environmentId: source.environmentId, + goalsId: source.goalsId, + systemId: source.systemId + }; + } +} \ No newline at end of file diff --git a/src/mappers/SystemToJsonMapper.mts b/src/mappers/SystemToJsonMapper.mts new file mode 100644 index 00000000..154efd93 --- /dev/null +++ b/src/mappers/SystemToJsonMapper.mts @@ -0,0 +1,23 @@ +import System from '~/domain/System.mjs'; +import type { EntityJson } from './EntityToJsonMapper.mjs'; +import EntityToJsonMapper from './EntityToJsonMapper.mjs'; +import SemVer from '~/lib/SemVer.mjs'; + +export interface SystemJson extends EntityJson { } + +export default class SystemToJsonMapper extends EntityToJsonMapper { + override mapFrom(target: SystemJson): System { + const version = new SemVer(target.serializationVersion); + + if (version.gte('0.4.0')) + return new System({ + ...target + }); + + throw new Error(`Unsupported serialization version: ${version}`); + } + + override mapTo(source: System): SystemJson { + return super.mapTo(source); + } +} \ No newline at end of file diff --git a/src/presentation/Application.mts b/src/presentation/Application.mts index 41ddf769..d1527485 100644 --- a/src/presentation/Application.mts +++ b/src/presentation/Application.mts @@ -64,19 +64,18 @@ export default class Application extends Container { this.#pages = [ (await import('./pages/HomePage.mjs')).default, NotFoundPage, - (await import('./pages/projects/ProjectsPage.mjs')).default, - (await import('./pages/environments/NewEnvironmentPage.mjs')).default, - (await import('./pages/environments/EnvironmentPage.mjs')).default, - (await import('./pages/environments/EnvironmentsPage.mjs')).default, - (await import('./pages/environments/GlossaryPage.mjs')).default, - (await import('./pages/goals/NewGoalsPage.mjs')).default, - (await import('./pages/goals/GoalPage.mjs')).default, - (await import('./pages/goals/GoalsPage.mjs')).default, - (await import('./pages/goals/RationalePage.mjs')).default, - (await import('./pages/goals/FunctionalityPage.mjs')).default, - (await import('./pages/goals/StakeholdersPage.mjs')).default, - (await import('./pages/goals/UseCasesPage.mjs')).default, - (await import('./pages/goals/LimitationsPage.mjs')).default, + (await import('./pages/solution/project/ProjectsIndexPage.mjs')).default, + (await import('./pages/solution/environment/EnvironmentsIndexPage.mjs')).default, + (await import('./pages/solution/environment/GlossaryPage.mjs')).default, + (await import('./pages/solution/goals/GoalsIndexPage.mjs')).default, + (await import('./pages/solution/goals/RationalePage.mjs')).default, + (await import('./pages/solution/goals/FunctionalityPage.mjs')).default, + (await import('./pages/solution/goals/StakeholdersPage.mjs')).default, + (await import('./pages/solution/goals/UseCasesPage.mjs')).default, + (await import('./pages/solution/goals/LimitationsPage.mjs')).default, + (await import('./pages/solution/NewSolutionPage.mjs')).default, + (await import('./pages/solution/SolutionIndexPage.mjs')).default, + (await import('./pages/solution/SolutionPage.mjs')).default ]; self.navigation.addEventListener('navigate', this); diff --git a/src/presentation/components/GlobalNav.mts b/src/presentation/components/GlobalNav.mts index a2ada540..61d9b4b5 100644 --- a/src/presentation/components/GlobalNav.mts +++ b/src/presentation/components/GlobalNav.mts @@ -10,9 +10,7 @@ import type { Properties } from '~/types/Properties.mjs'; * @returns True if the path is the current path, false otherwise. */ const isActive = (path: string, target: string): boolean => - path === '/' ? - target === '/' : - target.includes(path); + target.includes(path); export class GlobalNav extends Component { static { @@ -49,6 +47,7 @@ export class GlobalNav extends Component { fontSize: 'large', height: '0.7in', textShadow: '0 -1px 0px var(--shadow-color)', + width: '0.8in' }, 'x-feather-icon': { '--size': '1.5em', @@ -81,10 +80,7 @@ export class GlobalNav extends Component { const { nav, ul, template } = html; return template(nav({ className: 'global-nav' }, ul([ - this._routerLink('/', 'home', 'Home'), - this._routerLink('/projects', 'package', 'Projects'), - this._routerLink('/environments', 'cloud', 'Environments'), - this._routerLink('/goals', 'target', 'Goals'), + this._routerLink('/', 'home', 'Home') ]))); } diff --git a/src/presentation/components/PegsCards.mts b/src/presentation/components/PegsCards.mts index fe8ebce6..39b48346 100644 --- a/src/presentation/components/PegsCards.mts +++ b/src/presentation/components/PegsCards.mts @@ -1,4 +1,4 @@ -import PEGS from '~/domain/PEGS.mjs'; +import SlugEntity from '~/domain/SlugEntity.mjs'; import Entity from '~/domain/Entity.mjs'; import Repository from '~/usecases/Repository.mjs'; import { Container, PegsCard } from './index.mjs'; @@ -12,7 +12,7 @@ export class PegsCards extends Container { customElements.define('x-pegs-cards', this); } - #repo?: Repository; + #repo?: Repository; constructor({ repository }: Properties, children: (Element | string)[]) { super({}, children); @@ -44,7 +44,7 @@ export class PegsCards extends Container { elNewCard = new PegsCard({ heading: 'New Entry', description: 'Create a new entry', - href: `${curPath}/new-entry`, + href: `${curPath === '/' ? '' : curPath}/new-entry`, allowDelete: false }); elNewCard.dataset.id = Entity.emptyId; @@ -59,7 +59,7 @@ export class PegsCards extends Container { allowDelete: true, heading: item.name, description: item.description, - href: `${curPath}/${item.slug()}` + href: `${curPath === '/' ? '' : curPath}/${item.slug()}` }); card.dataset.id = item.id; @@ -70,11 +70,11 @@ export class PegsCards extends Container { } } - get repository(): Repository | undefined { + get repository(): Repository | undefined { return this.#repo; } - set repository(value: Repository | undefined) { + set repository(value: Repository | undefined) { this.#repo = value; this._renderCards(); } @@ -82,7 +82,7 @@ export class PegsCards extends Container { async onDelete(e: CustomEvent) { const card = e.detail; if (confirm(`Are you sure you want to delete "${card.heading}"?`)) { - await this.#repo!.delete(card.dataset.id as PEGS['id']); + await this.#repo!.delete(card.dataset.id as SlugEntity['id']); this._renderCards(); } } diff --git a/src/presentation/pages/HomePage.mts b/src/presentation/pages/HomePage.mts index 0dcf42b3..be00671f 100644 --- a/src/presentation/pages/HomePage.mts +++ b/src/presentation/pages/HomePage.mts @@ -1,16 +1,26 @@ -import html from '../lib/html.mjs'; +import SolutionRepository from '~/data/SolutionRepository.mjs'; +import { PegsCards } from '../components/PegsCards.mjs'; import Page from './Page.mjs'; +import html from '../lib/html.mjs'; -const { p } = html; +const { h2 } = html; export default class HomePage extends Page { static override route = '/'; static { customElements.define('x-page-home', this); } + + #repository = new SolutionRepository(localStorage); + constructor() { - super({ title: 'Home' }, [ - p('{Home}') - ]); + super({ title: 'Home' }, []); + + this.append( + h2('Solutions'), + new PegsCards({ + repository: this.#repository + }, []) + ); } } \ No newline at end of file diff --git a/src/presentation/pages/NotFoundPage.mts b/src/presentation/pages/NotFoundPage.mts index c84b215f..217cd5df 100644 --- a/src/presentation/pages/NotFoundPage.mts +++ b/src/presentation/pages/NotFoundPage.mts @@ -4,7 +4,7 @@ import html from '../lib/html.mjs'; const { h1, p, a } = html; export default class NotFoundPage extends Page { - static override route = '/not-found'; + static override route = '/-not-found-'; static { customElements.define('x-page-not-found', this); } diff --git a/src/presentation/pages/environments/EnvironmentPage.mts b/src/presentation/pages/environments/EnvironmentPage.mts deleted file mode 100644 index a034aa6b..00000000 --- a/src/presentation/pages/environments/EnvironmentPage.mts +++ /dev/null @@ -1,21 +0,0 @@ -import { MiniCards, MiniCard } from '~/presentation/components/index.mjs'; -import Page from '../Page.mjs'; - -export default class EnvironmentPage extends Page { - static override route = '/environments/:slug'; - static { - customElements.define('x-environment-page', this); - } - - constructor() { - super({ title: 'Environment' }, [ - new MiniCards({}, [ - new MiniCard({ - title: 'Glossary', - icon: 'list', - href: `${location.pathname}/glossary` - }), - ]) - ]); - } -} \ No newline at end of file diff --git a/src/presentation/pages/environments/EnvironmentsPage.mts b/src/presentation/pages/environments/EnvironmentsPage.mts deleted file mode 100644 index 452fef1a..00000000 --- a/src/presentation/pages/environments/EnvironmentsPage.mts +++ /dev/null @@ -1,26 +0,0 @@ -import { PegsCards } from '~components/index.mjs'; -import html from '../../lib/html.mjs'; -import Page from '../Page.mjs'; -import EnvironmentRepository from '~/data/EnvironmentRepository.mjs'; - -const { p } = html; - -export default class EnvironmentsPage extends Page { - static override route = '/environments'; - static { - customElements.define('x-environments-page', this); - } - - constructor() { - super({ title: 'Environments' }, [ - p(` - An environment is the set of entities (people, organizations, regulations, - devices and other material objects, other systems) external to the project - or system but with the potential to affect it or be affected by it. - `), - new PegsCards({ - repository: new EnvironmentRepository(localStorage) - }, []) - ]); - } -} \ No newline at end of file diff --git a/src/presentation/pages/goals/GoalsPage.mts b/src/presentation/pages/goals/GoalsPage.mts deleted file mode 100644 index 0c2c493c..00000000 --- a/src/presentation/pages/goals/GoalsPage.mts +++ /dev/null @@ -1,23 +0,0 @@ -import { PegsCards } from '~components/index.mjs'; -import html from '../../lib/html.mjs'; -import Page from '../Page.mjs'; -import GoalsRepository from '~/data/GoalsRepository.mjs'; - -const { p } = html; - -export default class GoalsPage extends Page { - static override route = '/goals'; - static { - customElements.define('x-goals-page', this); - } - - constructor() { - super({ title: 'Goals' }, [ - p(`Goals are the desired outcomes and needs of an - organization for which a system must satisfy.`), - new PegsCards({ - repository: new GoalsRepository(localStorage) - }, []) - ]); - } -} \ No newline at end of file diff --git a/src/presentation/pages/goals/NewGoalsPage.mts b/src/presentation/pages/goals/NewGoalsPage.mts deleted file mode 100644 index 5323bdad..00000000 --- a/src/presentation/pages/goals/NewGoalsPage.mts +++ /dev/null @@ -1,108 +0,0 @@ -import Goals from '~/domain/Goals.mjs'; -import PEGS from '~/domain/PEGS.mjs'; -import GoalsRepository from '~/data/GoalsRepository.mjs'; -import html from '~/presentation/lib/html.mjs'; -import requiredTheme from '~/presentation/theme/requiredTheme.mjs'; -import formTheme from '~/presentation/theme/formTheme.mjs'; -import Page from '../Page.mjs'; - -const { form, label, input, span, button } = html; - -export default class NewGoalsPage extends Page { - static override route = '/goals/new-entry'; - static { - customElements.define('x-new-goals-page', this); - } - - #repository = new GoalsRepository(localStorage); - #form!: HTMLFormElement; - #txtName!: HTMLInputElement; - #txtSlug!: HTMLInputElement; - - constructor() { - super({ title: 'New Goals' }, [ - form({ - className: 'goals-form', - autocomplete: 'off' - }, [ - label({ htmlFor: 'name', className: 'required' }, 'Name'), - input({ - type: 'text', name: 'name', id: 'name', required: true, - placeholder: 'My Goals', maxLength: Goals.maxNameLength - }, []), - label({ htmlFor: 'slug' }, 'Slug'), - input({ type: 'text', name: 'slug', id: 'slug', readOnly: true }, []), - label({ htmlFor: 'description' }, 'Description'), - input({ - type: 'text', name: 'description', id: 'description', - placeholder: 'A description of my goals', maxLength: Goals.maxDescriptionLength - }, []), - span({ id: 'actions' }, [ - button({ type: 'submit' }, 'Create'), - button({ type: 'reset' }, 'Cancel') - ]) - ]) - ]); - this.#form = this.querySelector('form')!; - this.#form.addEventListener('submit', this); - this.#form.addEventListener('reset', this); - this.#txtName = this!.querySelector('#name')!; - this.#txtName.addEventListener('input', this); - this.#txtSlug = this.querySelector('#slug')!; - } - - override _initPageStyle() { - return { - ...super._initPageStyle(), - ...requiredTheme, - ...formTheme, - '.goals-form': { - display: 'grid', - gridTemplateColumns: '20% 1fr', - gridGap: '1rem', - margin: '1rem' - }, - 'input': { - maxWidth: '80%', - width: '40em' - }, - '#actions': { - gridColumn: '2', - display: 'flex', - justifyContent: 'space-between', - maxWidth: '4.5in' - } - }; - } - - async onInput(e: Event) { - const name = (e.target as HTMLInputElement).value, - slug = PEGS.slugify(name); - this.#txtSlug.value = slug; - } - - async onSubmit(e: SubmitEvent) { - e.preventDefault(); - const form = e.target as HTMLFormElement, - formData = new FormData(form), - goals = new Goals({ - id: self.crypto.randomUUID(), - name: formData.get('name') as string, - description: formData.get('description') as string, - objective: '', - outcomes: '', - situation: '', - stakeholders: [], - functionalBehaviors: [], - useCases: [], - limits: [] - }); - - await this.#repository.add(goals); - self.navigation.navigate(`/goals/${goals.slug()}`); - } - - onReset() { - self.navigation.navigate('/goals'); - } -} \ No newline at end of file diff --git a/src/presentation/pages/projects/ProjectsPage.mts b/src/presentation/pages/projects/ProjectsPage.mts deleted file mode 100644 index b779d5af..00000000 --- a/src/presentation/pages/projects/ProjectsPage.mts +++ /dev/null @@ -1,17 +0,0 @@ -import html from '../../lib/html.mjs'; -import Page from '../Page.mjs'; - -const { p } = html; - -export default class ProjectsPage extends Page { - static override route = '/projects'; - static { - customElements.define('x-projects-page', this); - } - - constructor() { - super({ title: 'Projects' }, [ - p('{Projects}') - ]); - } -} \ No newline at end of file diff --git a/src/presentation/pages/environments/NewEnvironmentPage.mts b/src/presentation/pages/solution/NewSolutionPage.mts similarity index 52% rename from src/presentation/pages/environments/NewEnvironmentPage.mts rename to src/presentation/pages/solution/NewSolutionPage.mts index e7da5621..a695f338 100644 --- a/src/presentation/pages/environments/NewEnvironmentPage.mts +++ b/src/presentation/pages/solution/NewSolutionPage.mts @@ -1,43 +1,55 @@ -import Environment from '~/domain/Environment.mjs'; -import EnvironmentRepository from '~/data/EnvironmentRepository.mjs'; +import Solution from '~/domain/Solution.mjs'; +import SolutionRepository from '~/data/SolutionRepository.mjs'; import Page from '../Page.mjs'; import html from '~/presentation/lib/html.mjs'; -import PEGS from '~/domain/PEGS.mjs'; -import formTheme from '~/presentation/theme/formTheme.mjs'; import requiredTheme from '~/presentation/theme/requiredTheme.mjs'; +import formTheme from '~/presentation/theme/formTheme.mjs'; +import slugify from '~/lib/slugify.mjs'; +import EnvironmentRepository from '~/data/EnvironmentRepository.mjs'; +import GoalsRepository from '~/data/GoalsRepository.mjs'; +import ProjectRepository from '~/data/ProjectRepository.mjs'; +import SystemRepository from '~/data/SystemRepository.mjs'; +import Environment from '~/domain/Environment.mjs'; +import Goals from '~/domain/Goals.mjs'; +import Project from '~/domain/Project.mjs'; +import System from '~/domain/System.mjs'; const { form, label, input, span, button } = html; -export default class NewEnvironmentPage extends Page { - static override route = '/environments/new-entry'; +export default class NewSolutionPage extends Page { + static override route = '/new-entry'; static { - customElements.define('x-new-environment-page', this); + customElements.define('x-page-new-solution', this); } - #repository = new EnvironmentRepository(localStorage); + #solutionRepository = new SolutionRepository(localStorage); + #environmentRepository = new EnvironmentRepository(localStorage); + #goalsRepository = new GoalsRepository(localStorage); + #projectRepository = new ProjectRepository(localStorage); + #systemRepository = new SystemRepository(localStorage); #form!: HTMLFormElement; #txtName!: HTMLInputElement; #txtSlug!: HTMLInputElement; constructor() { - super({ title: 'New Environment' }, []); + super({ title: 'New Solution' }, []); this.appendChild( this.#form = form({ - className: 'environment-form', + className: 'solution-form', autocomplete: 'off' }, [ label({ htmlFor: 'name', className: 'required' }, 'Name'), this.#txtName = input({ type: 'text', name: 'name', id: 'name', required: true, - placeholder: 'Sample Environment', maxLength: Environment.maxNameLength + placeholder: 'Sample Solution', maxLength: Solution.maxNameLength }, []), label({ htmlFor: 'slug' }, 'Slug'), this.#txtSlug = input({ type: 'text', name: 'slug', id: 'slug', readOnly: true }, []), label({ htmlFor: 'description' }, 'Description'), input({ type: 'text', name: 'description', id: 'description', - placeholder: 'A description of the environment', maxLength: Environment.maxDescriptionLength + placeholder: 'A description of the solution', maxLength: Solution.maxDescriptionLength }, []), span({ id: 'actions' }, [ button({ type: 'submit' }, 'Create'), @@ -56,7 +68,7 @@ export default class NewEnvironmentPage extends Page { ...super._initPageStyle(), ...requiredTheme, ...formTheme, - '.environment-form': { + '.solution-form': { display: 'grid', gridTemplateColumns: '20% 1fr', gridGap: '1rem', @@ -77,7 +89,7 @@ export default class NewEnvironmentPage extends Page { async onInput(e: Event) { const name = (e.target as HTMLInputElement).value, - slug = PEGS.slugify(name); + slug = slugify(name); this.#txtSlug.value = slug; } @@ -85,19 +97,48 @@ export default class NewEnvironmentPage extends Page { e.preventDefault(); const form = e.target as HTMLFormElement, formData = new FormData(form), - - environment = new Environment({ + solution = new Solution({ id: self.crypto.randomUUID(), name: formData.get('name') as string, description: formData.get('description') as string, - glossary: [] + environmentId: self.crypto.randomUUID(), + goalsId: self.crypto.randomUUID(), + projectId: self.crypto.randomUUID(), + systemId: self.crypto.randomUUID() + }), + environment = new Environment({ + id: solution.environmentId, + glossaryIds: [] + }), + goals = new Goals({ + id: solution.goalsId, + stakeholderIds: [], + functionalBehaviorIds: [], + limitIds: [], + objective: '', + outcomes: '', + situation: '', + useCaseIds: [] + }), + project = new Project({ + id: solution.projectId + }), + system = new System({ + id: solution.systemId, }); - this.#repository.add(environment); - self.navigation.navigate(`/environments/${environment.slug()}`); + await Promise.all([ + this.#solutionRepository.add(solution), + this.#environmentRepository.add(environment), + this.#goalsRepository.add(goals), + this.#projectRepository.add(project), + this.#systemRepository.add(system) + ]); + + self.navigation.navigate(`/${solution.slug()}`); } onReset() { - self.navigation.navigate('/goals'); + self.navigation.navigate('/'); } } \ No newline at end of file diff --git a/src/presentation/pages/solution/SolutionIndexPage.mts b/src/presentation/pages/solution/SolutionIndexPage.mts new file mode 100644 index 00000000..a7ca9b9b --- /dev/null +++ b/src/presentation/pages/solution/SolutionIndexPage.mts @@ -0,0 +1,17 @@ +import { PegsCards } from '~/presentation/components/PegsCards.mjs'; +import Page from '../Page.mjs'; +import SolutionRepository from '~/data/SolutionRepository.mjs'; + +export default class SolutionIndexPage extends Page { + static override route = '/-solutions-'; + static { + customElements.define('x-page-solution-index', this); + } + constructor() { + super({ title: 'Solutions' }, [ + new PegsCards({ + repository: new SolutionRepository(localStorage) + }, []) + ]); + } +} \ No newline at end of file diff --git a/src/presentation/pages/solution/SolutionPage.mts b/src/presentation/pages/solution/SolutionPage.mts new file mode 100644 index 00000000..15d9838b --- /dev/null +++ b/src/presentation/pages/solution/SolutionPage.mts @@ -0,0 +1,36 @@ +import { MiniCards } from '~/presentation/components/MiniCards.mjs'; +import Page from '../Page.mjs'; +import { MiniCard } from '~/presentation/components/MiniCard.mjs'; + +export default class SolutionPage extends Page { + static override route = '/:solution'; + static { + customElements.define('x-page-solution', this); + } + constructor() { + super({ title: 'Solutions' }, [ + new MiniCards({}, [ + new MiniCard({ + title: 'Project', + icon: 'package', + href: `${location.pathname}/project` + }), + new MiniCard({ + title: 'Environment', + icon: 'cloud', + href: `${location.pathname}/environment` + }), + new MiniCard({ + title: 'Goals', + icon: 'target', + href: `${location.pathname}/goals` + }), + new MiniCard({ + title: 'System', + icon: 'cpu', + href: `${location.pathname}/system` + }), + ]) + ]); + } +} \ No newline at end of file diff --git a/src/presentation/pages/solution/environment/EnvironmentsIndexPage.mts b/src/presentation/pages/solution/environment/EnvironmentsIndexPage.mts new file mode 100644 index 00000000..831a6b42 --- /dev/null +++ b/src/presentation/pages/solution/environment/EnvironmentsIndexPage.mts @@ -0,0 +1,32 @@ +import html from '~/presentation/lib/html.mjs'; +import Page from '~/presentation/pages/Page.mjs'; +import { MiniCards, MiniCard } from '~components/index.mjs'; + +const { p } = html; + +export default class EnvironmentsIndexPage extends Page { + static override route = '/:solution/environment'; + static { + customElements.define('x-page-environments-index', this); + } + + constructor() { + super({ title: 'Environments' }, [ + p(` + An environment is the set of entities (people, organizations, regulations, + devices and other material objects, other systems) external to the project + or system but with the potential to affect it or be affected by it. + `) + ]); + + this.append( + new MiniCards({}, [ + new MiniCard({ + title: 'Glossary', + icon: 'list', + href: `${location.pathname}/glossary` + }), + ]) + ); + } +} \ No newline at end of file diff --git a/src/presentation/pages/environments/GlossaryPage.mts b/src/presentation/pages/solution/environment/GlossaryPage.mts similarity index 71% rename from src/presentation/pages/environments/GlossaryPage.mts rename to src/presentation/pages/solution/environment/GlossaryPage.mts index 3d3b6f0f..4dea521c 100644 --- a/src/presentation/pages/environments/GlossaryPage.mts +++ b/src/presentation/pages/solution/environment/GlossaryPage.mts @@ -4,16 +4,19 @@ import GlossaryTerm from '~/domain/GlossaryTerm.mjs'; import EnvironmentRepository from '~/data/EnvironmentRepository.mjs'; import GlossaryRepository from '~/data/GlossaryRepository.mjs'; import type Environment from '~/domain/Environment.mjs'; -import Page from '../Page.mjs'; +import Page from '~/presentation/pages/Page.mjs'; +import SolutionRepository from '~/data/SolutionRepository.mjs'; +import type { Uuid } from '~/types/Uuid.mjs'; const { p } = html; export default class GlossaryPage extends Page { - static override route = '/environments/:slug/glossary'; + static override route = '/:solution/environment/glossary'; static { - customElements.define('x-glossary-page', this); + customElements.define('x-page-glossary', this); } + #solutionRepository = new SolutionRepository(localStorage); #environmentRepository = new EnvironmentRepository(localStorage); #glossaryRepository = new GlossaryRepository(localStorage); #environment?: Environment; @@ -31,12 +34,12 @@ export default class GlossaryPage extends Page { if (!this.#environment) return []; - return await this.#glossaryRepository.getAll(t => this.#environment!.glossary.includes(t.id)); + return await this.#glossaryRepository.getAll(t => this.#environment!.glossaryIds.includes(t.id)); }, onCreate: async item => { const term = new GlossaryTerm({ ...item, id: self.crypto.randomUUID() }); await this.#glossaryRepository.add(term); - this.#environment!.glossary.push(term.id); + this.#environment!.glossaryIds.push(term.id); await this.#environmentRepository.update(this.#environment!); }, onUpdate: async item => { @@ -46,7 +49,7 @@ export default class GlossaryPage extends Page { }, onDelete: async id => { await this.#glossaryRepository.delete(id); - this.#environment!.glossary = this.#environment!.glossary.filter(x => x !== id); + this.#environment!.glossaryIds = this.#environment!.glossaryIds.filter(x => x !== id); await this.#environmentRepository.update(this.#environment!); } }); @@ -60,9 +63,12 @@ export default class GlossaryPage extends Page { this.#environmentRepository.addEventListener('update', () => dataTable.renderData()); this.#glossaryRepository.addEventListener('update', () => dataTable.renderData()); - this.#environmentRepository.getBySlug(this.urlParams['slug']).then(environment => { - this.#environment = environment; - dataTable.renderData(); + const solutionId = this.urlParams['solution'] as Uuid; + this.#solutionRepository.getBySlug(solutionId).then(solution => { + this.#environmentRepository.get(solution!.environmentId).then(environment => { + this.#environment = environment; + dataTable.renderData(); + }); }); } } \ No newline at end of file diff --git a/src/presentation/pages/goals/FunctionalityPage.mts b/src/presentation/pages/solution/goals/FunctionalityPage.mts similarity index 73% rename from src/presentation/pages/goals/FunctionalityPage.mts rename to src/presentation/pages/solution/goals/FunctionalityPage.mts index b2a8e918..976be1b8 100644 --- a/src/presentation/pages/goals/FunctionalityPage.mts +++ b/src/presentation/pages/solution/goals/FunctionalityPage.mts @@ -4,16 +4,18 @@ import GoalsRepository from '~/data/GoalsRepository.mjs'; import BehaviorRepository from '~/data/BehaviorRepository.mjs'; import html from '~/presentation/lib/html.mjs'; import { DataTable } from '~/presentation/components/DataTable.mjs'; -import Page from '../Page.mjs'; +import Page from '~/presentation/pages/Page.mjs'; +import SolutionRepository from '~/data/SolutionRepository.mjs'; const { p, strong } = html; export default class FunctionalityPage extends Page { - static override route = '/goals/:slug/functionality'; + static override route = '/:solution/goals/functionality'; static { - customElements.define('x-functionality-page', this); + customElements.define('x-page-functionality', this); } + #solutionRepository = new SolutionRepository(localStorage); #goalsRepository = new GoalsRepository(localStorage); #behaviorRepository = new BehaviorRepository(localStorage); #goals?: Goals; @@ -36,12 +38,12 @@ export default class FunctionalityPage extends Page { if (!this.#goals) return []; - return await this.#behaviorRepository.getAll(b => this.#goals!.functionalBehaviors.includes(b.id)); + return await this.#behaviorRepository.getAll(b => this.#goals!.functionalBehaviorIds.includes(b.id)); }, onCreate: async item => { const behavior = new Behavior({ ...item, id: self.crypto.randomUUID() }); await this.#behaviorRepository.add(behavior); - this.#goals!.functionalBehaviors.push(behavior.id); + this.#goals!.functionalBehaviorIds.push(behavior.id); await this.#goalsRepository.update(this.#goals!); }, onUpdate: async item => { @@ -51,7 +53,7 @@ export default class FunctionalityPage extends Page { }, onDelete: async id => { await this.#behaviorRepository.delete(id); - this.#goals!.functionalBehaviors = this.#goals!.functionalBehaviors.filter(x => x !== id); + this.#goals!.functionalBehaviorIds = this.#goals!.functionalBehaviorIds.filter(x => x !== id); await this.#goalsRepository.update(this.#goals!); } }); @@ -59,9 +61,12 @@ export default class FunctionalityPage extends Page { this.#goalsRepository.addEventListener('update', () => dataTable.renderData()); this.#behaviorRepository.addEventListener('update', () => dataTable.renderData()); - this.#goalsRepository.getBySlug(this.urlParams['slug']).then(goals => { - this.#goals = goals; - dataTable.renderData(); + const solutionSlug = this.urlParams['solution']; + this.#solutionRepository.getBySlug(solutionSlug).then(solution => { + this.#goalsRepository.get(solution!.goalsId).then(goals => { + this.#goals = goals; + dataTable.renderData(); + }); }); } } \ No newline at end of file diff --git a/src/presentation/pages/goals/GoalPage.mts b/src/presentation/pages/solution/goals/GoalsIndexPage.mts similarity index 66% rename from src/presentation/pages/goals/GoalPage.mts rename to src/presentation/pages/solution/goals/GoalsIndexPage.mts index 9d453e45..dcbf651c 100644 --- a/src/presentation/pages/goals/GoalPage.mts +++ b/src/presentation/pages/solution/goals/GoalsIndexPage.mts @@ -1,14 +1,22 @@ -import Page from '../Page.mjs'; -import { MiniCards, MiniCard } from '~/presentation/components/index.mjs'; +import { MiniCard, MiniCards } from '~components/index.mjs'; +import html from '~/presentation/lib/html.mjs'; +import Page from '~/presentation/pages/Page.mjs'; -export default class GoalPage extends Page { - static override route = '/goals/:slug'; +const { p } = html; + +export default class GoalsIndexPage extends Page { + static override route = '/:solution/goals'; static { - customElements.define('x-goal-page', this); + customElements.define('x-page-goals-index', this); } constructor() { - super({ title: 'Goal' }, [ + super({ title: 'Goals' }, [ + p(`Goals are the desired outcomes and needs of an + organization for which a system must satisfy.`) + ]); + + this.append( new MiniCards({}, [ new MiniCard({ title: 'Rationale', @@ -36,6 +44,6 @@ export default class GoalPage extends Page { href: `${location.pathname}/limitations` }) ]) - ]); + ); } } \ No newline at end of file diff --git a/src/presentation/pages/goals/LimitationsPage.mts b/src/presentation/pages/solution/goals/LimitationsPage.mts similarity index 73% rename from src/presentation/pages/goals/LimitationsPage.mts rename to src/presentation/pages/solution/goals/LimitationsPage.mts index 5489bfae..f5e0b9e8 100644 --- a/src/presentation/pages/goals/LimitationsPage.mts +++ b/src/presentation/pages/solution/goals/LimitationsPage.mts @@ -4,16 +4,19 @@ 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 Page from '../Page.mjs'; +import Page from '~/presentation/pages/Page.mjs'; +import SolutionRepository from '~/data/SolutionRepository.mjs'; +import type { Uuid } from '~/types/Uuid.mjs'; const { p } = html; export default class LimitationsPage extends Page { - static override route = '/goals/:slug/limitations'; + static override route = '/:solution/goals/limitations'; static { - customElements.define('x-limitations-page', this); + customElements.define('x-page-limitations', this); } + #solutionRepository = new SolutionRepository(localStorage); #goalsRepository = new GoalsRepository(localStorage); #limitRepository = new LimitRepository(localStorage); #goals?: Goals; @@ -38,12 +41,12 @@ export default class LimitationsPage extends Page { if (!this.#goals) return []; - return await this.#limitRepository.getAll(l => this.#goals!.limits.includes(l.id)); + return await this.#limitRepository.getAll(l => this.#goals!.limitIds.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); + this.#goals!.limitIds.push(limit.id); await this.#goalsRepository.update(this.#goals!); }, onUpdate: async item => { @@ -53,7 +56,7 @@ export default class LimitationsPage extends Page { }, onDelete: async id => { await this.#limitRepository.delete(id); - this.#goals!.limits = this.#goals!.limits.filter(x => x !== id); + this.#goals!.limitIds = this.#goals!.limitIds.filter(x => x !== id); await this.#goalsRepository.update(this.#goals!); } }); @@ -61,9 +64,13 @@ export default class LimitationsPage extends Page { this.#goalsRepository.addEventListener('update', () => dataTable.renderData()); this.#limitRepository.addEventListener('update', () => dataTable.renderData()); - this.#goalsRepository.getBySlug(this.urlParams['slug']).then(goals => { - this.#goals = goals; - dataTable.renderData(); + const solutionId = this.urlParams['solution'] as Uuid; + + this.#solutionRepository.getBySlug(solutionId).then(solution => { + this.#goalsRepository.get(solution!.goalsId).then(goals => { + this.#goals = goals; + dataTable.renderData(); + }); }); } } \ No newline at end of file diff --git a/src/presentation/pages/goals/RationalePage.mts b/src/presentation/pages/solution/goals/RationalePage.mts similarity index 74% rename from src/presentation/pages/goals/RationalePage.mts rename to src/presentation/pages/solution/goals/RationalePage.mts index 0941c089..c73d6b9b 100644 --- a/src/presentation/pages/goals/RationalePage.mts +++ b/src/presentation/pages/solution/goals/RationalePage.mts @@ -1,29 +1,26 @@ import Goals from '~/domain/Goals.mjs'; import GoalsRepository from '~/data/GoalsRepository.mjs'; import html from '~/presentation/lib/html.mjs'; -import Page from '../Page.mjs'; +import Page from '~/presentation/pages/Page.mjs'; +import SolutionRepository from '~/data/SolutionRepository.mjs'; const { form, h3, p, textarea } = html; export default class RationalePage extends Page { - static override route = '/goals/:slug/rationale'; + static override route = '/:solution/goals/rationale'; static { - customElements.define('x-rationale-page', this); + customElements.define('x-page-rationale', this); } - #repository = new GoalsRepository(localStorage); + #solutionRepository = new SolutionRepository(localStorage); + #goalsRepository = new GoalsRepository(localStorage); #goals!: Goals; constructor() { super({ title: 'Rationale' }, []); - this.#repository.getBySlug(this.urlParams['slug'])!.then(goals => { - if (!goals) { - this.replaceChildren( - p(`No goals found for the provided slug: ${this.urlParams['slug']}`) - ); - } - else { + this.#solutionRepository.getBySlug(this.urlParams['solution'])!.then(solution => { + this.#goalsRepository.get(solution!.goalsId)!.then(goals => { const { situation, objective, outcomes } = this.#goals = goals!; this.replaceChildren( @@ -31,7 +28,7 @@ export default class RationalePage extends Page { h3('Situation'), p( `The situation is the current state of affairs that need to be - addressed by a system created by a project.` + addressed by a system created by a project.` ), textarea({ name: 'situation', @@ -39,13 +36,13 @@ export default class RationalePage extends Page { onchange: e => { const txtSituation = e.target as HTMLTextAreaElement; this.#goals.situation = txtSituation.value.trim(); - this.#repository.update(this.#goals); + this.#goalsRepository.update(this.#goals); } }, []), h3('Objective'), p( `The objective is the reason for building a system and the organization - context in which it will be used.` + context in which it will be used.` ), textarea({ name: 'objective', @@ -53,7 +50,7 @@ export default class RationalePage extends Page { onchange: e => { const txtObjective = e.target as HTMLTextAreaElement; this.#goals.objective = txtObjective.value.trim(); - this.#repository.update(this.#goals); + this.#goalsRepository.update(this.#goals); } }, []), h3('Outcomes'), @@ -66,12 +63,12 @@ export default class RationalePage extends Page { onchange: e => { const txtOutcomes = e.target as HTMLTextAreaElement; this.#goals.outcomes = txtOutcomes.value.trim(); - this.#repository.update(this.#goals); + this.#goalsRepository.update(this.#goals); } }, []) ]) ); - } + }); }); } diff --git a/src/presentation/pages/goals/StakeholdersPage.mts b/src/presentation/pages/solution/goals/StakeholdersPage.mts similarity index 85% rename from src/presentation/pages/goals/StakeholdersPage.mts rename to src/presentation/pages/solution/goals/StakeholdersPage.mts index 1c434843..528177a9 100644 --- a/src/presentation/pages/goals/StakeholdersPage.mts +++ b/src/presentation/pages/solution/goals/StakeholdersPage.mts @@ -4,23 +4,26 @@ import GoalsRepository from '~/data/GoalsRepository.mjs'; import StakeholderRepository from '~/data/StakeholderRepository.mjs'; import html from '~/presentation/lib/html.mjs'; import { DataTable } from '~/presentation/components/DataTable.mjs'; -import Page from '../Page.mjs'; +import Page from '~/presentation/pages/Page.mjs'; import { Tabs } from '~components/Tabs.mjs'; import mermaid from 'mermaid'; import groupBy from '~/lib/groupBy.mjs'; +import SolutionRepository from '~/data/SolutionRepository.mjs'; +import type { Uuid } from '~/types/Uuid.mjs'; const { h2, p, div } = html; export default class StakeholdersPage extends Page { - static override route = '/goals/:slug/stakeholders'; + static override route = '/:solution/goals/stakeholders'; static { - customElements.define('x-stakeholders-page', this); + customElements.define('x-page-stakeholders', this); mermaid.initialize({ startOnLoad: true, theme: 'dark' }); } + #solutionRepository = new SolutionRepository(localStorage); #goalsRepository = new GoalsRepository(localStorage); #stakeholderRepository = new StakeholderRepository(localStorage); #goals?: Goals; @@ -44,12 +47,12 @@ export default class StakeholdersPage extends Page { if (!this.#goals) return []; - return await this.#stakeholderRepository.getAll(s => this.#goals!.stakeholders.includes(s.id)); + return await this.#stakeholderRepository.getAll(s => this.#goals!.stakeholderIds.includes(s.id)); }, onCreate: async item => { const stakeholder = new Stakeholder({ ...item, id: self.crypto.randomUUID() }); await this.#stakeholderRepository.add(stakeholder); - this.#goals!.stakeholders.push(stakeholder.id); + this.#goals!.stakeholderIds.push(stakeholder.id); await this.#goalsRepository.update(this.#goals!); }, onUpdate: async item => { @@ -59,7 +62,7 @@ export default class StakeholdersPage extends Page { }, onDelete: async id => { await this.#stakeholderRepository.delete(id); - this.#goals!.stakeholders = this.#goals!.stakeholders.filter(x => x !== id); + this.#goals!.stakeholderIds = this.#goals!.stakeholderIds.filter(x => x !== id); await this.#goalsRepository.update(this.#goals!); } }); @@ -88,10 +91,13 @@ export default class StakeholdersPage extends Page { dataTable.renderData(); this.#renderStakeholderMap(); }); - this.#goalsRepository.getBySlug(this.urlParams['slug']).then(async goals => { - this.#goals = goals; - dataTable.renderData(); - this.#renderStakeholderMap(); + const solutionId = this.urlParams['solution'] as Uuid; + this.#solutionRepository.getBySlug(solutionId).then(solution => { + this.#goalsRepository.get(solution!.goalsId).then(goals => { + this.#goals = goals; + dataTable.renderData(); + this.#renderStakeholderMap(); + }); }); } diff --git a/src/presentation/pages/goals/UseCasesPage.mts b/src/presentation/pages/solution/goals/UseCasesPage.mts similarity index 84% rename from src/presentation/pages/goals/UseCasesPage.mts rename to src/presentation/pages/solution/goals/UseCasesPage.mts index 22decce1..b06a4dfa 100644 --- a/src/presentation/pages/goals/UseCasesPage.mts +++ b/src/presentation/pages/solution/goals/UseCasesPage.mts @@ -1,6 +1,6 @@ import html from '~/presentation/lib/html.mjs'; import { DataTable } from '~/presentation/components/DataTable.mjs'; -import Page from '../Page.mjs'; +import Page from '~/presentation/pages/Page.mjs'; import { Tabs } from '~components/Tabs.mjs'; import mermaid from 'mermaid'; import UseCase from '~/domain/UseCase.mjs'; @@ -9,19 +9,22 @@ import StakeholderRepository from '~/data/StakeholderRepository.mjs'; import type Goals from '~/domain/Goals.mjs'; import Stakeholder from '~/domain/Stakeholder.mjs'; import UseCaseRepository from '~/data/UseCaseRepository.mjs'; +import SolutionRepository from '~/data/SolutionRepository.mjs'; +import type { Uuid } from '~/types/Uuid.mjs'; const { h2, p, div, br } = html; export default class UseCasesPage extends Page { - static override route = '/goals/:slug/use-cases'; + static override route = '/:solution/goals/use-cases'; static { - customElements.define('x-use-cases-page', this); + customElements.define('x-page-use-cases', this); mermaid.initialize({ startOnLoad: true, theme: 'dark' }); } + #solutionRepository = new SolutionRepository(localStorage); #goalsRepository = new GoalsRepository(localStorage); #stakeholderRepository = new StakeholderRepository(localStorage); #useCaseRepository = new UseCaseRepository(localStorage); @@ -48,12 +51,12 @@ export default class UseCasesPage extends Page { if (!this.#goals) return []; - return this.#useCaseRepository.getAll(u => this.#goals!.useCases.includes(u.id)); + return this.#useCaseRepository.getAll(u => this.#goals!.useCaseIds.includes(u.id)); }, onCreate: async item => { const useCase = new UseCase({ ...item, id: self.crypto.randomUUID() }); await this.#useCaseRepository.add(useCase); - this.#goals!.useCases.push(useCase.id); + this.#goals!.useCaseIds.push(useCase.id); await this.#goalsRepository.update(this.#goals!); }, onUpdate: async item => { @@ -63,7 +66,7 @@ export default class UseCasesPage extends Page { }, onDelete: async id => { await this.#useCaseRepository.delete(id); - this.#goals!.useCases = this.#goals!.useCases.filter(x => x !== id); + this.#goals!.useCaseIds = this.#goals!.useCaseIds.filter(x => x !== id); await this.#goalsRepository.update(this.#goals!); } }); @@ -77,9 +80,13 @@ export default class UseCasesPage extends Page { this.#goalsRepository.addEventListener('update', update); this.#stakeholderRepository.addEventListener('update', update); - this.#goalsRepository.getBySlug(this.urlParams['slug']) + + const solutionId = this.urlParams['solution'] as Uuid; + this.#solutionRepository.getBySlug(solutionId) + .then(solution => this.#goalsRepository.get(solution!.goalsId)) .then(goals => { this.#goals = goals; }) .then(update); + this.#useCaseRepository.addEventListener('update', update); this.replaceChildren( @@ -104,7 +111,7 @@ export default class UseCasesPage extends Page { async #renderUseCaseDiagram() { const mermaidContainer = this.querySelector('#mermaid-container')!, - useCases = await this.#useCaseRepository.getAll(u => this.#goals!.useCases.includes(u.id)), + useCases = await this.#useCaseRepository.getAll(u => this.#goals!.useCaseIds.includes(u.id)), chartDefinition = ` flowchart LR ${useCases.map(u => { diff --git a/src/presentation/pages/solution/project/ProjectsIndexPage.mts b/src/presentation/pages/solution/project/ProjectsIndexPage.mts new file mode 100644 index 00000000..3dc0c125 --- /dev/null +++ b/src/presentation/pages/solution/project/ProjectsIndexPage.mts @@ -0,0 +1,17 @@ +import html from '~/presentation/lib/html.mjs'; +import Page from '~/presentation/pages/Page.mjs'; + +const { p } = html; + +export default class ProjectsIndexPage extends Page { + static override route = '/:solution/projects'; + static { + customElements.define('x-page-projects-index', this); + } + + constructor() { + super({ title: 'Projects' }, [ + p('{Projects}') + ]); + } +} \ No newline at end of file