34 | GroundUp 35 |
36 | )} 37 |35 | {capitalize(error.severity)} at line {error.lineNumber} 36 |
37 |53 | Rule: 54 | {error.rule} 55 |
56 |57 | Group: 58 | {error.group ?? "n/a"} 59 |
60 |61 | Heading: 62 | {error.field ?? "n/a"} 63 |
64 |66 | Message: 67 | {error.message} 68 |
69 |We are always happy to chat.
36 |Feel free to reach out to us at
37 |hello@groundup.cloud
39 |163 | {body} 164 |
165 | ) 166 | }) 167 | FormMessage.displayName = "FormMessage" 168 | 169 | export { 170 | useFormField, 171 | Form, 172 | FormItem, 173 | FormLabel, 174 | FormControl, 175 | FormDescription, 176 | FormMessage, 177 | FormField, 178 | } 179 | -------------------------------------------------------------------------------- /ags/src/rules/rulesForParsedAgs/checkGroupAndHeadings.ts: -------------------------------------------------------------------------------- 1 | import { AgsRaw, AgsError } from "../../types"; 2 | 3 | import { AgsValidationStepParsed } from "./types"; 4 | 5 | // TODO: add the actual data dictionary, for all versions of AGS 6 | export const rule7: AgsValidationStepParsed = { 7 | rule: 7, 8 | description: 9 | "AGS Format Rule 7: The order of data FIELDs in each line within a GROUP is defined at the start of each GROUP inthe HEADING row. HEADINGs shall be in the order described in the AGS FORMAT DATA DICTIONARY.", 10 | validate: function (ags: AgsRaw): AgsError[] { 11 | const errors: AgsError[] = []; 12 | 13 | for (const [groupName, group] of Object.entries(ags)) { 14 | // assert no duplicates headings in each group 15 | const headings = group.headings.map((heading) => heading.name); 16 | const uniqueHeadings = new Set(headings); 17 | if (headings.length !== uniqueHeadings.size) { 18 | errors.push({ 19 | rule: this.rule, 20 | lineNumber: group.lineNumber + 2, 21 | group: groupName, 22 | message: "Duplicate headings found in the group.", 23 | severity: "error", 24 | }); 25 | } 26 | } 27 | return errors; 28 | }, 29 | }; 30 | 31 | export const rule19: AgsValidationStepParsed = { 32 | rule: 19, 33 | description: 34 | "A GROUP name shall not be more than 4 characters long and shall consist of uppercase letters and numbers only.", 35 | validate: function (ags: AgsRaw): AgsError[] { 36 | const errors: AgsError[] = []; 37 | for (const [groupName, group] of Object.entries(ags)) { 38 | if (groupName.length > 4 || !/^[A-Z0-9]+$/.test(groupName)) { 39 | errors.push({ 40 | rule: this.rule, 41 | lineNumber: group.lineNumber, 42 | group: groupName, 43 | message: "Invalid GROUP name format.", 44 | severity: "error", 45 | }); 46 | } 47 | } 48 | return errors; 49 | }, 50 | }; 51 | 52 | // Rule 19a A HEADING name shall not be more than 9 characters long and shall consist of uppercase letters, 53 | // numbers or the underscore character only. 54 | export const rule19a: AgsValidationStepParsed = { 55 | rule: 19, 56 | description: 57 | "A HEADING name shall not be more than 9 characters long and shall consist of uppercase letters, numbers or the underscore character only.", 58 | validate: function (ags: AgsRaw): AgsError[] { 59 | const errors: AgsError[] = []; 60 | for (const [groupName, group] of Object.entries(ags)) { 61 | for (const heading of group.headings) { 62 | if (heading.name.length > 9 || !/^[A-Z0-9_]+$/.test(heading.name)) { 63 | errors.push({ 64 | rule: this.rule, 65 | lineNumber: group.lineNumber + 1, 66 | group: groupName, 67 | field: heading.name, 68 | message: "Invalid HEADING name format.", 69 | severity: "warning", 70 | }); 71 | } 72 | } 73 | } 74 | return errors; 75 | }, 76 | }; 77 | 78 | // Rule 19b 79 | // HEADING names shall start with the GROUP name followed by an underscore character .e.g. 80 | // "NGRP_HED1" 81 | // Where a HEADING refers to an existing HEADING within another GROUP, the HEADING name 82 | // added to the group shall bear the same name. 83 | // e.g. "CMPG_TESN" in the "CMPT" GROUP. 84 | export const rule19b: AgsValidationStepParsed = { 85 | rule: "19b", 86 | description: 87 | "HEADING names shall start with the GROUP name followed by an underscore character.", 88 | validate: function (ags: AgsRaw): AgsError[] { 89 | const errors: AgsError[] = []; 90 | 91 | for (const [groupName, group] of Object.entries(ags)) { 92 | const groupPrefix = groupName + "_"; 93 | 94 | const allowedPrefixes = [groupPrefix, "SPEC_", "TEST_"]; 95 | 96 | // remove group headings of current group, to provide lookup if heading is in another group 97 | const allHeadingsApartFromCurrentGroup = new Set( 98 | Object.values(ags) 99 | .filter((g) => g.name !== group.name) 100 | .flatMap((g) => g.headings) 101 | .map((heading) => heading.name) 102 | .filter((name) => !name.startsWith(groupPrefix)), 103 | ); 104 | 105 | for (const heading of group.headings) { 106 | if ( 107 | !( 108 | allowedPrefixes.some((prefix) => heading.name.startsWith(prefix)) || 109 | allHeadingsApartFromCurrentGroup.has(heading.name) 110 | ) 111 | ) { 112 | errors.push({ 113 | rule: this.rule, 114 | lineNumber: group.lineNumber + 1, 115 | group: groupName, 116 | field: heading.name, 117 | severity: "error", 118 | message: 119 | "HEADING name does not start with the GROUP name, or not found in another group.", 120 | }); 121 | } 122 | } 123 | } 124 | return errors; 125 | }, 126 | }; 127 | -------------------------------------------------------------------------------- /ags-validator-app/components/validator/ErrorMessages/ErrorMessages.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useRef } from "react"; 2 | import { useVirtualizer } from "@tanstack/react-virtual"; 3 | import ErrorMessage from "./ErrorMessage"; 4 | import SortErrors, { sortOptions, SortOptionKey } from "./SortErrors"; 5 | import { AgsError } from "@groundup/ags"; 6 | import { CircleAlert, CircleX, LoaderCircle } from "lucide-react"; 7 | import { Badge } from "@/components/ui/badge"; 8 | import { useAppSelector } from "@/lib/redux/hooks"; 9 | import { cn } from "@/lib/utils"; 10 | 11 | interface ErrorTableProps { 12 | goToError: (error: AgsError) => void; 13 | } 14 | 15 | export default function ErrorMessages({ goToError }: ErrorTableProps) { 16 | const errors = useAppSelector((state) => state.ags.errors); 17 | const isLoading = useAppSelector((state) => state.ags.loading); 18 | 19 | const [severityFilter, setSeverityFilter] = useState< 20 | "error" | "warning" | null 21 | >(null); 22 | 23 | const filteredErrors = useMemo(() => { 24 | return severityFilter 25 | ? errors.filter((error) => error.severity === severityFilter) 26 | : errors; 27 | }, [errors, severityFilter]); 28 | 29 | const [sortOptionKey, setSortOptionKey] = useStateNo errors or warnings found
136 | )} 137 |by
35 |40 | Edit and check your AGS data with ease. 41 |
42 |43 | All your AGS data stays on your device and 44 | never leaves your browser. 45 | 65 |
66 |77 | Upload or paste your AGS data using the tools below 78 |
79 |97 | Validate your data against any AGS4 version, and inspect 98 | issues 99 |
100 |Edit your data in table or text view
110 |Export your data as AGS4
125 |140 | We're currently in closed beta, so join the waitlist to get 141 | early access, and help shape GroundUp. 142 |
143 |
37 | By GroundUp
38 |
39 |
40 |
41 | Report Bug
42 | ·
43 | Request Feature
44 |