├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.md
│ └── enhancement.md
├── pull_request_template.md
└── workflows
│ ├── check-ags-lib-version.yml
│ ├── publish-and-release-ags-lib.yml
│ └── test-ags-lib.yml
├── .gitignore
├── LICENSE.txt
├── README.md
├── ags-validator-app
├── .eslintrc.json
├── .gitignore
├── README.md
├── app
│ ├── api
│ │ └── register
│ │ │ └── route.ts
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx
│ └── privacy
│ │ └── page.tsx
├── components.json
├── components
│ ├── NavBar.tsx
│ ├── RegisterForm
│ │ ├── RegisterForm.tsx
│ │ └── index.tsx
│ ├── ThemeProvider.tsx
│ ├── ThemeSwitcher.tsx
│ ├── logo
│ │ ├── groundUpLogo.svg
│ │ └── index.tsx
│ ├── mobile-nav.tsx
│ ├── ui
│ │ ├── auto-complete.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── command.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── popover.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ └── tooltip.tsx
│ └── validator
│ │ ├── AGSUpload
│ │ ├── AGSUpload.tsx
│ │ └── index.tsx
│ │ ├── ErrorMessages
│ │ ├── ErrorMessage.tsx
│ │ ├── ErrorMessages.tsx
│ │ ├── SortErrors.tsx
│ │ └── index.tsx
│ │ ├── GridView
│ │ ├── GridView.tsx
│ │ └── index.tsx
│ │ ├── SelectTable
│ │ ├── SelectTable.tsx
│ │ └── index.tsx
│ │ ├── TextArea
│ │ ├── CodeMirrorTextArea.tsx
│ │ └── index.tsx
│ │ ├── ViewToolbar
│ │ ├── ViewToolbar.tsx
│ │ └── index.tsx
│ │ ├── index.tsx
│ │ ├── store-provider.tsx
│ │ ├── validator-provider.tsx
│ │ └── validator.tsx
├── hooks
│ └── useGridTheme.ts
├── lib
│ ├── redux
│ │ ├── ags.ts
│ │ ├── hooks.ts
│ │ └── store.ts
│ └── utils.ts
├── next.config.mjs
├── package.json
├── postcss.config.mjs
├── tailwind.config.ts
├── tsconfig.json
└── workers
│ ├── validateRawUpdateWorker.js
│ └── validateRowUpdateWorker.js
├── ags
├── .gitignore
├── .npmignore
├── .prettierrc
├── __tests__
│ └── rules
│ │ ├── rulesForParsedAgs
│ │ ├── checkDataTypes.test.ts
│ │ ├── checkGroupAndHeadings.test.ts
│ │ ├── checkHeadingsWithDict.test.ts
│ │ └── checkRequiredGroups.test.ts
│ │ └── rulesForRawData.test.ts
├── assets
│ ├── Standard_dictionary_v4_0_3.ags
│ ├── Standard_dictionary_v4_0_3.ags.json
│ ├── Standard_dictionary_v4_0_4.ags
│ ├── Standard_dictionary_v4_0_4.ags.json
│ ├── Standard_dictionary_v4_1.ags
│ ├── Standard_dictionary_v4_1.ags.json
│ ├── Standard_dictionary_v4_1_1.ags
│ └── Standard_dictionary_v4_1_1.ags.json
├── eslint.config.mjs
├── jest.config.cjs
├── package.json
├── readme.md
├── rollup.config.js
├── scripts
│ ├── buildDictionaries.ts
│ └── profile.cjs
├── src
│ ├── index.ts
│ ├── parse.ts
│ ├── rules
│ │ ├── index.ts
│ │ ├── rulesForParsedAgs
│ │ │ ├── checkDataTypes.ts
│ │ │ ├── checkGroupAndHeadings.ts
│ │ │ ├── checkHeadingsWithDict.ts
│ │ │ ├── checkRequiredGroups.ts
│ │ │ ├── index.ts
│ │ │ └── types.d.ts
│ │ ├── rulesForRawData.ts
│ │ └── types.d.ts
│ ├── standardDictionaries.ts
│ ├── types.ts
│ └── validate.ts
└── tsconfig.json
├── package-lock.json
└── package.json
/.github/ISSUE_TEMPLATE/bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Report a problem or bug in the application
4 | title: "[BUG] Brief description of the bug"
5 | labels: bug
6 | assignees: ''
7 | ---
8 |
9 | **Describe the Bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | e.g.
16 |
17 | 1. Go to '...'
18 | 2. Click on '...'
19 | 3. Scroll down to '...'
20 | 4. See error
21 |
22 | **Expected Behavior**
23 | A clear and concise description of what you expected to happen.
24 |
25 | **Screenshots**
26 | If applicable, add screenshots to help explain your problem.
27 |
28 | **Desktop (please complete the following information):**
29 | - OS: [e.g., macOS, Windows]
30 | - Browser: [e.g., Chrome, Firefox]
31 |
32 | **Smartphone (please complete the following information):**
33 | - Device: [e.g., iPhone 12]
34 | - OS: [e.g., iOS 15]
35 | - Browser: [e.g., Safari]
36 | - Version: [e.g., 15]
37 |
38 | **Additional Context**
39 | Add any other context about the problem here.
40 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/enhancement.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Enhancement Request
3 | about: Suggest an idea for improving the application
4 | title: "[ENHANCEMENT] Brief description of the enhancement"
5 | labels: enhancement
6 | assignees: ''
7 | ---
8 |
9 | **Is Your Enhancement Related to a Problem? Please Describe.**
10 | A clear and concise description of the problem or need.
11 |
12 | **Describe the Solution You'd Like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe Alternatives You've Considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional Context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ### Description
2 |
3 | Please include a summary of the change and which issue is fixed. List any dependencies that are required for this change.
4 |
5 | Please link and relevant issues.
6 |
7 | ### Type of Change
8 |
9 | Please delete options that are not relevant.
10 |
11 | - [ ] Bug fix (non-breaking change which fixes an issue)
12 | - [ ] New feature (non-breaking change which adds functionality)
13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
14 | - [ ] Documentation update
15 |
16 | ### Checklist
17 |
18 | - [ ] I have performed a self-review of my own code
19 | - [ ] I have commented my code, particularly in hard-to-understand areas
20 | - [ ] I have made corresponding changes to the documentation
21 | - [ ] My changes generate no new warnings
22 | - [ ] I have added tests that prove my fix is effective or that my feature works
23 | - [ ] New and existing unit tests pass locally with my changes
24 |
25 | ### Screenshots (if applicable)
26 |
27 | Add screenshots to help explain the changes (if applicable).
28 |
29 | ### Additional Context
30 |
31 | Add any other context or screenshots about the pull request here.
32 |
--------------------------------------------------------------------------------
/.github/workflows/check-ags-lib-version.yml:
--------------------------------------------------------------------------------
1 | name: Check AGS Package Version
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - 'ags/**' # Trigger only if files in the `ags` package have changed
7 |
8 | jobs:
9 | check_version:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v3
14 |
15 | - name: Get current branch version of ags package
16 | id: current_version
17 | run: |
18 | jq -r '.version' ags/package.json > current_version.txt
19 | echo "current_version=$(cat current_version.txt)" >> $GITHUB_ENV
20 |
21 | - name: Get main branch version of ags package
22 | run: |
23 | git fetch origin main
24 | git show origin/main:ags/package.json | jq -r '.version' > main_version.txt
25 | echo "main_version=$(cat main_version.txt)" >> $GITHUB_ENV
26 |
27 | - name: Check for changes in the ags package
28 | id: check_changes
29 | run: |
30 | if git diff --quiet origin/main -- ags/; then
31 | echo "has_changes=false" >> $GITHUB_ENV
32 | else
33 | echo "has_changes=true" >> $GITHUB_ENV
34 | fi
35 |
36 | - name: Fail if version is not updated
37 | if: env.has_changes == 'true' && env.current_version == env.main_version
38 | run: |
39 | echo "Code has changed in the ags package, but the version has not been updated."
40 | exit 1
41 | env:
42 | current_version: ${{ env.current_version }}
43 | main_version: ${{ env.main_version }}
44 |
--------------------------------------------------------------------------------
/.github/workflows/publish-and-release-ags-lib.yml:
--------------------------------------------------------------------------------
1 | name: Publish and Release AGS Package
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - 'ags/**'
9 | - '.github/workflows/publish-and-release-ags-lib.yml'
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | permissions:
14 | contents: read
15 | id-token: write
16 | steps:
17 | - uses: actions/checkout@v4
18 | # Setup .npmrc file to publish to npm
19 | - uses: actions/setup-node@v4
20 | with:
21 | node-version: '20.x'
22 | registry-url: 'https://registry.npmjs.org'
23 | - run: npm install --workspace ags
24 | - run: npm publish --workspace ags
25 | env:
26 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
27 |
28 |
--------------------------------------------------------------------------------
/.github/workflows/test-ags-lib.yml:
--------------------------------------------------------------------------------
1 | name: Run Jest Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | test:
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | node-version: [18.x, 20.x] # Test on multiple versions of Node.js
18 |
19 | steps:
20 | - name: Checkout repository
21 | uses: actions/checkout@v3
22 |
23 | - name: Set up Node.js
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | cache: 'npm'
28 |
29 | # Use a working directory for the rest of the steps
30 | - name: Install dependencies
31 | run: npm install
32 | working-directory: ./ags # Set the working directory
33 |
34 | - name: Run Jest Tests
35 | run: npm run test
36 | working-directory: ./ags # Set the working directory
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **node_modules
2 | package-lock.json
3 |
4 | **.env
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
14 |
21 | [![Contributors][contributors-shield]][contributors-url]
22 | [![Forks][forks-shield]][forks-url]
23 | [![Stargazers][stars-shield]][stars-url]
24 | [![Issues][issues-shield]][issues-url]
25 |
26 |
27 |
28 |
29 |
46 |
47 |
48 |
49 |
50 |
51 | Table of Contents
52 |
53 |
54 | About The Project
55 |
56 |
57 | Contributing
58 | License
59 |
60 |
61 |
62 |
63 |
64 |
65 | ## About The Project
66 |
67 | Validate, fix, and edit your AGS data with ease.
68 | All your AGS data stays on your device and never leaves your browser.
69 |
70 | This repository is home to two typescript projects:
71 |
72 | ### @groundup/ags
73 |
74 | This is the npm package that performs parsing and validation of AGS4 files.
75 | Source code and docs are available [here](https://github.com/groundup-dev/ags-validator/tree/main/ags).
76 |
77 | ### Ags Editor Web App
78 |
79 | This is the web application that lives [here](https://validator.groundup.cloud).
80 |
81 | Source code is available [here](https://github.com/groundup-dev/ags-validator/tree/main/ags-validator-app).
82 |
83 | (back to top )
84 |
85 |
86 | ## What is GroundUp?
87 |
88 | GroundUp is an open source platform that makes geotechnical analysis and data management, easier, quicker, more transparent, and more collaborative.
89 |
90 | We are currently working hard on building the public version of GroundUp, which will be available soon.
91 |
92 | Register for early access to GroundUp and be the first to know when it's available.
93 |
94 | GroundUp will **always be open-source, and free to get started**.
95 |
96 |
97 |
98 |
99 | ## Contributing
100 |
101 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
102 |
103 | You don't have to write code to contribute. Creating issues, providing feedback, and writing documentation are all invaluable contributions.
104 |
105 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
106 | Don't forget to give the project a star! Thanks again!
107 |
108 | 1. Fork the Project
109 | 2. Create your Feature Branch (`git checkout -b feat/AmazingFeature`)
110 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
111 | 4. Push to the Branch (`git push origin feat/AmazingFeature`)
112 | 5. Open a Pull Request
113 |
114 | (back to top )
115 |
116 | ### Top contributors:
117 |
118 |
119 |
120 |
121 |
122 |
123 | ## License
124 |
125 | Distributed under the Apache v2 License. See `LICENSE.txt` for more information.
126 |
127 | (back to top )
128 |
129 |
130 |
131 | (back to top )
132 |
133 |
134 |
135 |
136 |
137 | [contributors-shield]: https://img.shields.io/github/contributors/groundup-dev/ags-validator.svg?style=for-the-badge
138 | [contributors-url]: https://github.com/groundup-dev/ags-validator/graphs/contributors
139 | [forks-shield]: https://img.shields.io/github/forks/groundup-dev/ags-validator.svg?style=for-the-badge
140 | [forks-url]: https://github.com/groundup-dev/ags-validator/network/members
141 | [stars-shield]: https://img.shields.io/github/stars/groundup-dev/ags-validator.svg?style=for-the-badge
142 | [stars-url]: https://github.com/groundup-dev/ags-validator/stargazers
143 | [issues-shield]: https://img.shields.io/github/issues/groundup-dev/ags-validator.svg?style=for-the-badge
144 | [issues-url]: https://github.com/groundup-dev/ags-validator/issues
145 | [license-shield]: https://img.shields.io/github/license/groundup-dev/ags-validator.svg?style=for-the-badge
146 | [license-url]: https://github.com/groundup-dev/ags-validator/blob/main/LICENSE.txt
147 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
148 | [linkedin-url]: https://linkedin.com/in/linkedin_username
149 | [product-screenshot]: images/screenshot.png
150 | [Next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
151 | [Next-url]: https://nextjs.org/
152 | [React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB
153 | [React-url]: https://reactjs.org/
154 | [Vue.js]: https://img.shields.io/badge/Vue.js-35495E?style=for-the-badge&logo=vuedotjs&logoColor=4FC08D
155 | [Vue-url]: https://vuejs.org/
156 | [Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white
157 | [Angular-url]: https://angular.io/
158 | [Svelte.dev]: https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte&logoColor=FF3E00
159 | [Svelte-url]: https://svelte.dev/
160 | [Laravel.com]: https://img.shields.io/badge/Laravel-FF2D20?style=for-the-badge&logo=laravel&logoColor=white
161 | [Laravel-url]: https://laravel.com
162 | [Bootstrap.com]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white
163 | [Bootstrap-url]: https://getbootstrap.com
164 | [JQuery.com]: https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery&logoColor=white
165 | [JQuery-url]: https://jquery.com
--------------------------------------------------------------------------------
/ags-validator-app/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "next/typescript"]
3 | }
4 |
--------------------------------------------------------------------------------
/ags-validator-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/ags-validator-app/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 |
10 | ```
11 |
12 |
--------------------------------------------------------------------------------
/ags-validator-app/app/api/register/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | // Mailchimp API URL template
4 | const MAILCHIMP_API_URL = `https://${process.env.MAILCHIMP_SERVER_PREFIX}.api.mailchimp.com/3.0`;
5 |
6 | export async function POST(req: Request) {
7 | const { email, firstName, lastName } = await req.json();
8 |
9 | const audienceId = process.env.MAILCHIMP_AUDIENCE_ID;
10 | const apiKey = process.env.MAILCHIMP_API_KEY;
11 |
12 | if (!audienceId) {
13 | return NextResponse.json({ status: 500 });
14 | }
15 |
16 | if (!email || !firstName || !lastName) {
17 | return NextResponse.json(
18 | { error: "Email and name are required" },
19 | { status: 400 }
20 | );
21 | }
22 |
23 | // Construct the Mailchimp API URL
24 | const url = `${MAILCHIMP_API_URL}/lists/${audienceId}/members`;
25 |
26 | // Mailchimp requires the API key to be passed as a Base64-encoded string
27 | const auth = Buffer.from(`anystring:${apiKey}`).toString("base64");
28 |
29 | // Make the POST request to Mailchimp API
30 | const response = await fetch(url, {
31 | method: "POST",
32 | headers: {
33 | Authorization: `Basic ${auth}`,
34 | "Content-Type": "application/json",
35 | },
36 | body: JSON.stringify({
37 | email_address: email,
38 | status: "subscribed",
39 | merge_fields: {
40 | FNAME: firstName,
41 | LNAME: lastName,
42 | },
43 | }),
44 | });
45 |
46 | if (!response.ok) {
47 | const errorData = await response.json();
48 |
49 | if (errorData.title === "Member Exists") {
50 | return NextResponse.json(
51 | { error: "User is already subscribed" },
52 | { status: response.status }
53 | );
54 | }
55 | return NextResponse.json(
56 | { error: "Failed to subscribe user" },
57 | { status: response.status }
58 | );
59 | }
60 |
61 | return NextResponse.json({
62 | message: "User subscribed successfully",
63 | });
64 | }
65 |
--------------------------------------------------------------------------------
/ags-validator-app/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/groundup-dev/ags-validator/46efe868d8231e960f29d14507d861cbc8487e95/ags-validator-app/app/favicon.ico
--------------------------------------------------------------------------------
/ags-validator-app/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
6 |
7 | @layer base {
8 | :root {
9 | --background: 0 0% 100%;
10 | --foreground: 0 0% 0%;
11 | --card: 0 0% 100%;
12 | --card-foreground: 0 0% 0%;
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 0 0% 0%;
15 | --primary: 130 100% 25%;
16 | --primary-foreground: 0 0% 100%;
17 | --secondary: 0 0% 94%;
18 | --secondary-foreground: 0 0% 0%;
19 | --alternative: 30 100% 30%;
20 | --alternative-foreground: 0 0% 100%;
21 | --muted: 207 86 95%;
22 | --muted-foreground: 0 0% 0%;
23 | --accent: 0 0% 96%;
24 | --accent-foreground: 0 0% 0%;
25 | --destructive: 11 93% 55%;
26 | --destructive-foreground: 11 93% 85%;
27 | --warning: 39 97% 50%;
28 | --warning-foreground: 39 97% 83%;
29 | --border: 0 0% 75%;
30 | --input: 0 0% 81%;
31 | --ring: 0 0% 100% / 0;
32 | --success: 140 100% 32%;
33 | --radius: 0.5rem;
34 |
35 | }
36 |
37 | .dark {
38 | --background: 0 0% 7%;
39 | --foreground: 0 0% 98%;
40 | --card: 0 0% 9%;
41 | --card-foreground: 0 0% 98%;
42 | --popover: 0 0% 9%;
43 | --popover-foreground: 0 0% 98%;
44 | --primary: 130 100% 25%;
45 | --primary-foreground: 0 0% 100%;
46 | --secondary: 0 0% 15%;
47 | --secondary-foreground: 0 0% 98%;
48 | --alternative: 30 100% 30%;
49 | --alternative-foreground: 0 0% 100%;
50 | --muted: 207 90 3%;
51 | --muted-foreground: 0 0% 85%;
52 | --accent: 0 0% 20%;
53 | --accent-foreground: 0 0% 98%;
54 | --destructive: 11 93% 45%;
55 | --destructive-foreground: 11 93% 95%;
56 | --warning: 39 97% 40%;
57 | --warning-foreground: 39 97% 93%;
58 | --border: 0 0% 25%;
59 | --input: 0 0% 25%;
60 | --warning: 39 97% 50%;
61 | --warning-foreground: 39 97% 83%;
62 |
63 | --success: 140 100% 42%;
64 | }
65 | }
66 |
67 | @layer base {
68 | * {
69 | @apply border-border;
70 |
71 | }
72 | body {
73 | @apply bg-background text-foreground;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/ags-validator-app/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import { Analytics } from "@vercel/analytics/react";
4 | import { ThemeProvider } from "@/components/ThemeProvider";
5 | const inter = Inter({
6 | subsets: ["latin"],
7 | variable: "--font-inter",
8 | });
9 |
10 | import "./globals.css";
11 | import React from "react";
12 | import Navbar from "@/components/NavBar";
13 |
14 | export const metadata: Metadata = {
15 | title: "GroundUp | AGS4 Editor",
16 | description: "AGS4 Editor by GroundUp",
17 | };
18 |
19 | export default function RootLayout({
20 | children,
21 | }: Readonly<{
22 | children: React.ReactNode;
23 | }>) {
24 | return (
25 |
26 |
30 |
36 |
37 | {children}
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/ags-validator-app/app/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import {
4 | Dialog,
5 | DialogTrigger,
6 | DialogContent,
7 | DialogHeader,
8 | DialogTitle,
9 | DialogDescription,
10 | } from "@/components/ui/dialog";
11 | import {
12 | CheckSquare,
13 | CornerLeftDown,
14 | CornerRightDown,
15 | DownloadCloud,
16 | Edit,
17 | ExternalLink,
18 | HelpCircle,
19 | UploadCloudIcon,
20 | } from "lucide-react";
21 | import Validator from "@/components/validator";
22 | import { Button } from "@/components/ui/button";
23 | import Link from "next/link";
24 | import { Logo } from "@/components/logo";
25 |
26 | export default function Page() {
27 | return (
28 |
29 |
30 |
31 |
32 |
AGS Editor
33 |
37 |
38 |
39 |
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 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | Data Privacy
55 |
56 |
57 | Your AGS files are processed entirely within your browser.
58 | This means that your data never leaves your device, and
59 | you can use this tool without worrying about any data
60 | privacy and residency requirements.
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | Upload
75 |
76 |
77 | Upload or paste your AGS data using the tools below
78 |
79 |
80 |
84 |
85 |
86 |
87 |
91 |
92 |
93 |
94 | Validate
95 |
96 |
97 | Validate your data against any AGS4 version, and inspect
98 | issues
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | Edit
108 |
109 |
Edit your data in table or text view
110 |
111 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | Export
123 |
124 |
Export your data as AGS4
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | Register for{" "}
135 |
136 | early access
137 |
138 |
139 |
140 | We're currently in closed beta, so join the waitlist to get
141 | early access, and help shape GroundUp.
142 |
143 |
144 |
145 |
149 | Explore GroundUp
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 | );
160 | }
161 |
--------------------------------------------------------------------------------
/ags-validator-app/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/ags-validator-app/components/NavBar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { Logo } from "@/components/logo";
5 |
6 | import { FaGithub } from "react-icons/fa";
7 | import Link from "next/link";
8 | import {
9 | DropdownMenu,
10 | DropdownMenuContent,
11 | DropdownMenuItem,
12 | DropdownMenuLabel,
13 | DropdownMenuSeparator,
14 | DropdownMenuTrigger,
15 | } from "./ui/dropdown-menu";
16 | import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
17 | import { Separator } from "./ui/separator";
18 | import { ExternalLink } from "lucide-react";
19 | import { ThemeSwitcher } from "./ThemeSwitcher";
20 | import { MobileNav } from "./mobile-nav";
21 | import { Button } from "./ui/button";
22 |
23 | export default function Navbar() {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 | Contact
32 |
33 |
34 |
35 |
We are always happy to chat.
36 |
Feel free to reach out to us at
37 |
38 |
hello@groundup.cloud
39 |
40 |
41 |
42 |
43 |
44 |
45 | Feedback
46 |
47 |
48 |
49 | Feedback
50 |
51 |
52 |
57 | Report an issue
58 |
59 |
60 |
61 |
62 |
67 | Suggest a change
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | GroundUp
82 |
83 |
84 |
85 |
86 |
87 | GitHub
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/ags-validator-app/components/RegisterForm/RegisterForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { z } from "zod";
4 | import { zodResolver } from "@hookform/resolvers/zod";
5 | import { useForm } from "react-hook-form";
6 |
7 | import { Button } from "@/components/ui/button";
8 | import {
9 | Form,
10 | FormControl,
11 | FormField,
12 | FormItem,
13 | FormLabel,
14 | FormMessage,
15 | } from "@/components/ui/form";
16 | import { Input } from "@/components/ui/input";
17 | import { Check, LoaderCircle } from "lucide-react";
18 | import { useState } from "react";
19 | import { track } from "@vercel/analytics";
20 |
21 | const formSchema = z.object({
22 | firstName: z.string().min(1, "First name is required"),
23 | lastName: z.string().min(1, "Last name is required"),
24 | email: z.string().email("Invalid email address"),
25 | });
26 |
27 | export function RegisterForm() {
28 | const [isLoading, setIsLoading] = useState(false);
29 | const [isSuccess, setIsSuccess] = useState(false);
30 | const [error, setError] = useState(null);
31 |
32 | const form = useForm>({
33 | resolver: zodResolver(formSchema),
34 | defaultValues: {
35 | firstName: "",
36 | lastName: "",
37 | email: "",
38 | },
39 | });
40 |
41 | async function onSubmit(values: z.infer) {
42 | setIsLoading(true);
43 | setError(null);
44 | const response = await fetch("/api/register", {
45 | method: "POST",
46 | body: JSON.stringify(values),
47 | });
48 | setIsLoading(false);
49 |
50 | if (!response.ok) {
51 | console.error("Failed to submit form");
52 | const data = await response.json();
53 | setError(data.error);
54 | track("failed-registration", { error: data.error });
55 |
56 | return;
57 | } else {
58 | track("completed-registration");
59 | setIsSuccess(true);
60 | }
61 | }
62 |
63 | return (
64 |
131 |
132 | );
133 | }
134 |
--------------------------------------------------------------------------------
/ags-validator-app/components/RegisterForm/index.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/groundup-dev/ags-validator/46efe868d8231e960f29d14507d861cbc8487e95/ags-validator-app/components/RegisterForm/index.tsx
--------------------------------------------------------------------------------
/ags-validator-app/components/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import {
5 | ThemeProvider as NextThemesProvider,
6 | ThemeProviderProps,
7 | } from "next-themes";
8 |
9 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
10 | return {children} ;
11 | }
12 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ThemeSwitcher.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Moon, Sun, Laptop } from "lucide-react";
5 | import { useTheme } from "next-themes";
6 |
7 | import { Button } from "@/components/ui/button";
8 | import {
9 | DropdownMenu,
10 | DropdownMenuContent,
11 | DropdownMenuItem,
12 | DropdownMenuTrigger,
13 | } from "@/components/ui/dropdown-menu";
14 |
15 | export function ThemeSwitcher() {
16 | const { setTheme } = useTheme();
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | Toggle theme
25 |
26 |
27 |
28 | setTheme("light")}>
29 |
30 | Light
31 |
32 | setTheme("dark")}>
33 |
34 | Dark
35 |
36 | setTheme("system")}>
37 |
38 | System
39 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/ags-validator-app/components/logo/groundUpLogo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/ags-validator-app/components/logo/index.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import Image from "next/image";
3 | import groundUpLogo from "./groundUpLogo.svg";
4 |
5 | const sizeClassNames = {
6 | sm: { logo: "h-8", text: "text-md" },
7 | md: { logo: "h-10", text: "text-lg" },
8 | lg: { logo: "h-12", text: "text-xl" },
9 | };
10 |
11 | interface Props {
12 | size: keyof typeof sizeClassNames;
13 | showText?: boolean;
14 | }
15 |
16 | export function Logo({ size, showText = true }: Props) {
17 | const heights = {
18 | sm: 32, // h-8
19 | md: 40, // h-10
20 | lg: 48, // h-12
21 | };
22 |
23 | return (
24 |
25 |
32 | {showText && (
33 |
34 | GroundUp
35 |
36 | )}
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/ags-validator-app/components/mobile-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Sheet,
6 | SheetContent,
7 | SheetHeader,
8 | SheetTitle,
9 | SheetTrigger,
10 | } from "@/components/ui/sheet";
11 | import { ExternalLink, Menu } from "lucide-react";
12 | import Link from "next/link";
13 | import { FaGithub } from "react-icons/fa";
14 | import { useState } from "react";
15 | import { Separator } from "./ui/separator";
16 |
17 | export function MobileNav() {
18 | const [open, setOpen] = useState(false);
19 |
20 | return (
21 |
22 |
23 |
28 |
29 |
30 |
31 |
32 |
33 | Menu
34 |
35 |
36 |
37 |
38 | Visit our website to learn more about GroundUp.
39 |
40 |
41 |
42 |
43 |
44 | GroundUp
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
Contact
53 |
54 | We are always happy to chat.
55 |
56 |
57 | Feel free to reach out to us at
58 |
59 |
hello@groundup.cloud
60 |
61 |
62 |
63 |
64 |
65 |
Feedback
66 |
67 | setOpen(false)}
70 | className="flex items-center gap-2 text-sm hover:text-primary transition-colors"
71 | >
72 | Report an issue
73 |
74 |
75 | setOpen(false)}
78 | className="flex items-center gap-2 text-sm hover:text-primary transition-colors"
79 | >
80 | Suggest a change
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | setOpen(false)}
92 | className="flex flex-row items-center gap-2"
93 | >
94 |
95 | Get the code
96 |
97 |
98 |
99 |
100 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/auto-complete.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Check, ChevronsUpDown } from "lucide-react";
5 |
6 | import { cn } from "@/lib/utils";
7 | import { Button } from "@/components/ui/button";
8 | import {
9 | Command,
10 | CommandEmpty,
11 | CommandGroup,
12 | CommandInput,
13 | CommandItem,
14 | CommandList,
15 | } from "@/components/ui/command";
16 | import {
17 | Popover,
18 | PopoverContent,
19 | PopoverTrigger,
20 | } from "@/components/ui/popover";
21 | import { Label } from "@/components/ui/label";
22 |
23 | type Props = {
24 | options: { value: string; label: string }[];
25 | label: string;
26 | placeholder?: string;
27 | selectedOption: string;
28 | setSelectedOption: React.Dispatch>;
29 | };
30 |
31 | export default function AutoComplete({
32 | options,
33 | selectedOption,
34 | setSelectedOption,
35 | label,
36 | placeholder,
37 | }: Props) {
38 | const [open, setOpen] = React.useState(false);
39 |
40 | return (
41 |
42 |
{label}
43 |
44 |
45 |
46 | {selectedOption
47 | ? options.find((option) => option.value === selectedOption)?.label
48 | : ""}
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | No option found.
57 |
58 | {options.map((option) => (
59 | {
63 | setSelectedOption(currentValue);
64 | setOpen(false);
65 | }}
66 | >
67 |
75 | {option.label}
76 |
77 | ))}
78 |
79 |
80 |
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarImage, AvatarFallback };
51 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-white hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | warning: "border-transparent bg-warning text-white hover:bg-warning/80",
19 | },
20 | },
21 | defaultVariants: {
22 | variant: "default",
23 | },
24 | }
25 | );
26 |
27 | export interface BadgeProps
28 | extends React.HTMLAttributes,
29 | VariantProps {}
30 |
31 | function Badge({ className, variant, ...props }: BadgeProps) {
32 | return (
33 |
34 | );
35 | }
36 |
37 | export { Badge, badgeVariants };
38 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-9 w-9",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | );
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean;
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button";
45 | return (
46 |
51 | );
52 | }
53 | );
54 | Button.displayName = "Button";
55 |
56 | export { Button, buttonVariants };
57 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { type DialogProps } from "@radix-ui/react-dialog"
5 | import { Command as CommandPrimitive } from "cmdk"
6 | import { Search } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | interface CommandDialogProps extends DialogProps {}
27 |
28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29 | return (
30 |
31 |
32 |
33 | {children}
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | const CommandInput = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
45 |
46 |
54 |
55 | ))
56 |
57 | CommandInput.displayName = CommandPrimitive.Input.displayName
58 |
59 | const CommandList = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, ...props }, ref) => (
63 |
68 | ))
69 |
70 | CommandList.displayName = CommandPrimitive.List.displayName
71 |
72 | const CommandEmpty = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >((props, ref) => (
76 |
81 | ))
82 |
83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
84 |
85 | const CommandGroup = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
97 | ))
98 |
99 | CommandGroup.displayName = CommandPrimitive.Group.displayName
100 |
101 | const CommandSeparator = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
112 |
113 | const CommandItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
125 | ))
126 |
127 | CommandItem.displayName = CommandPrimitive.Item.displayName
128 |
129 | const CommandShortcut = ({
130 | className,
131 | ...props
132 | }: React.HTMLAttributes) => {
133 | return (
134 |
141 | )
142 | }
143 | CommandShortcut.displayName = "CommandShortcut"
144 |
145 | export {
146 | Command,
147 | CommandDialog,
148 | CommandInput,
149 | CommandList,
150 | CommandEmpty,
151 | CommandGroup,
152 | CommandItem,
153 | CommandShortcut,
154 | CommandSeparator,
155 | }
156 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
6 | import { Check, ChevronRight, Circle } from "lucide-react";
7 |
8 | import { cn } from "@/lib/utils";
9 |
10 | const DropdownMenu = DropdownMenuPrimitive.Root;
11 |
12 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
13 |
14 | const DropdownMenuGroup = DropdownMenuPrimitive.Group;
15 |
16 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
17 |
18 | const DropdownMenuSub = DropdownMenuPrimitive.Sub;
19 |
20 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
21 |
22 | const DropdownMenuSubTrigger = React.forwardRef<
23 | React.ElementRef,
24 | React.ComponentPropsWithoutRef & {
25 | inset?: boolean;
26 | }
27 | >(({ className, inset, children, ...props }, ref) => (
28 |
37 | {children}
38 |
39 |
40 | ));
41 | DropdownMenuSubTrigger.displayName =
42 | DropdownMenuPrimitive.SubTrigger.displayName;
43 |
44 | const DropdownMenuSubContent = React.forwardRef<
45 | React.ElementRef,
46 | React.ComponentPropsWithoutRef
47 | >(({ className, ...props }, ref) => (
48 |
56 | ));
57 | DropdownMenuSubContent.displayName =
58 | DropdownMenuPrimitive.SubContent.displayName;
59 |
60 | const DropdownMenuContent = React.forwardRef<
61 | React.ElementRef,
62 | React.ComponentPropsWithoutRef
63 | >(({ className, sideOffset = 4, ...props }, ref) => (
64 |
65 |
74 |
75 | ));
76 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
77 |
78 | const DropdownMenuItem = React.forwardRef<
79 | React.ElementRef,
80 | React.ComponentPropsWithoutRef & {
81 | inset?: boolean;
82 | }
83 | >(({ className, inset, ...props }, ref) => (
84 |
93 | ));
94 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
95 |
96 | const DropdownMenuCheckboxItem = React.forwardRef<
97 | React.ElementRef,
98 | React.ComponentPropsWithoutRef & {
99 | checked: boolean;
100 | }
101 | >(({ className, children, checked, ...props }, ref) => (
102 |
111 |
112 |
113 |
114 |
115 |
116 | {children}
117 |
118 | ));
119 | DropdownMenuCheckboxItem.displayName =
120 | DropdownMenuPrimitive.CheckboxItem.displayName;
121 |
122 | const DropdownMenuRadioItem = React.forwardRef<
123 | React.ElementRef,
124 | React.ComponentPropsWithoutRef
125 | >(({ className, children, ...props }, ref) => (
126 |
134 |
135 |
136 |
137 |
138 |
139 | {children}
140 |
141 | ));
142 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
143 |
144 | const DropdownMenuLabel = React.forwardRef<
145 | React.ElementRef,
146 | React.ComponentPropsWithoutRef & {
147 | inset?: boolean;
148 | }
149 | >(({ className, inset, ...props }, ref) => (
150 |
159 | ));
160 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
161 |
162 | const DropdownMenuSeparator = React.forwardRef<
163 | React.ElementRef,
164 | React.ComponentPropsWithoutRef
165 | >(({ className, ...props }, ref) => (
166 |
171 | ));
172 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
173 |
174 | const DropdownMenuShortcut = ({
175 | className,
176 | ...props
177 | }: React.HTMLAttributes) => {
178 | return (
179 |
183 | );
184 | };
185 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
186 |
187 | export {
188 | DropdownMenu,
189 | DropdownMenuTrigger,
190 | DropdownMenuContent,
191 | DropdownMenuItem,
192 | DropdownMenuCheckboxItem,
193 | DropdownMenuRadioItem,
194 | DropdownMenuLabel,
195 | DropdownMenuSeparator,
196 | DropdownMenuShortcut,
197 | DropdownMenuGroup,
198 | DropdownMenuPortal,
199 | DropdownMenuSub,
200 | DropdownMenuSubContent,
201 | DropdownMenuSubTrigger,
202 | DropdownMenuRadioGroup,
203 | };
204 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext,
13 | } from "react-hook-form"
14 |
15 | import { cn } from "@/lib/utils"
16 | import { Label } from "@/components/ui/label"
17 |
18 | const Form = FormProvider
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath
23 | > = {
24 | name: TName
25 | }
26 |
27 | const FormFieldContext = React.createContext(
28 | {} as FormFieldContextValue
29 | )
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext)
46 | const itemContext = React.useContext(FormItemContext)
47 | const { getFieldState, formState } = useFormContext()
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState)
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ")
53 | }
54 |
55 | const { id } = itemContext
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | }
65 | }
66 |
67 | type FormItemContextValue = {
68 | id: string
69 | }
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue
73 | )
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId()
80 |
81 | return (
82 |
83 |
84 |
85 | )
86 | })
87 | FormItem.displayName = "FormItem"
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField()
94 |
95 | return (
96 |
102 | )
103 | })
104 | FormLabel.displayName = "FormLabel"
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111 |
112 | return (
113 |
124 | )
125 | })
126 | FormControl.displayName = "FormControl"
127 |
128 | const FormDescription = React.forwardRef<
129 | HTMLParagraphElement,
130 | React.HTMLAttributes
131 | >(({ className, ...props }, ref) => {
132 | const { formDescriptionId } = useFormField()
133 |
134 | return (
135 |
141 | )
142 | })
143 | FormDescription.displayName = "FormDescription"
144 |
145 | const FormMessage = React.forwardRef<
146 | HTMLParagraphElement,
147 | React.HTMLAttributes
148 | >(({ className, children, ...props }, ref) => {
149 | const { error, formMessageId } = useFormField()
150 | const body = error ? String(error?.message) : children
151 |
152 | if (!body) {
153 | return null
154 | }
155 |
156 | return (
157 |
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-validator-app/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | }
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ));
17 | Table.displayName = "Table";
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ));
25 | TableHeader.displayName = "TableHeader";
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ));
37 | TableBody.displayName = "TableBody";
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className,
48 | )}
49 | {...props}
50 | />
51 | ));
52 | TableFooter.displayName = "TableFooter";
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ));
67 | TableRow.displayName = "TableRow";
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 |
81 | ));
82 | TableHead.displayName = "TableHead";
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 |
93 | ));
94 | TableCell.displayName = "TableCell";
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ));
106 | TableCaption.displayName = "TableCaption";
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | };
118 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TabsPrimitive from "@radix-ui/react-tabs";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent };
56 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | }
21 | );
22 | Textarea.displayName = "Textarea";
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/ags-validator-app/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/AGSUpload/AGSUpload.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { Input } from "@/components/ui/input";
5 | import { Label } from "@/components/ui/label";
6 | import { useAppDispatch } from "@/lib/redux/hooks";
7 | import { applySetRawDataEffect, setRawData } from "@/lib/redux/ags";
8 | import { track } from "@vercel/analytics";
9 |
10 | interface Props {
11 | setTabsViewValue: React.Dispatch>;
12 | }
13 |
14 | export default function AGSUpload({ setTabsViewValue }: Props) {
15 | const dispatch = useAppDispatch();
16 |
17 | // Handle file input change
18 | const handleFileChange = (event: React.ChangeEvent) => {
19 | const selectedFile = event.target.files?.[0];
20 |
21 | if (selectedFile) {
22 | const reader = new FileReader();
23 | reader.onload = (e) => {
24 | setTabsViewValue("text");
25 | const content = e.target?.result;
26 |
27 | // get size of file
28 | const fileSize = selectedFile.size;
29 | track("File Uploaded", { fileSize });
30 |
31 | if (typeof content === "string") {
32 | dispatch(setRawData(content));
33 | dispatch(applySetRawDataEffect());
34 | }
35 | };
36 | reader.readAsText(selectedFile); // Read file as text
37 | }
38 | };
39 |
40 | return (
41 |
42 | Upload AGS4 File
43 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/AGSUpload/index.tsx:
--------------------------------------------------------------------------------
1 | import AGSUpload from "./AGSUpload";
2 |
3 | export default AGSUpload;
4 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/ErrorMessages/ErrorMessage.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { AgsError } from "@groundup/ags";
3 | import { Button } from "@/components/ui/button";
4 | import { CircleAlert, CircleX } from "lucide-react";
5 | import { MdOutlineOpenInNew } from "react-icons/md";
6 | import { capitalize, cn } from "@/lib/utils";
7 |
8 | const severityColor: Record = {
9 | error: "text-destructive",
10 | warning: "text-warning",
11 | };
12 |
13 | interface ErrorMessageProps {
14 | error: AgsError;
15 | onView: () => void;
16 | }
17 |
18 | export default function ErrorMessage({ error, onView }: ErrorMessageProps) {
19 | return (
20 |
21 |
22 |
28 | {error.severity === "error" ? (
29 |
30 | ) : (
31 |
32 | )}
33 |
34 |
35 | {capitalize(error.severity)} at line {error.lineNumber}
36 |
37 |
38 |
{
43 | onView();
44 | }}
45 | >
46 |
47 | View
48 |
49 |
50 |
51 |
52 |
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 |
65 |
66 | Message:
67 | {error.message}
68 |
69 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/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] = useState(
30 | null
31 | );
32 | const sortedErrors = useMemo(() => {
33 | return sortOptionKey
34 | ? [...filteredErrors].sort(sortOptions[sortOptionKey].compareFn)
35 | : filteredErrors;
36 | }, [sortOptionKey, filteredErrors]);
37 |
38 | const parentRef = useRef(null);
39 |
40 | const rowVirtualizer = useVirtualizer({
41 | count: sortedErrors.length,
42 | getScrollElement: () => parentRef.current,
43 | estimateSize: () => 150,
44 | overscan: 5,
45 | });
46 |
47 | const virtualizedRows = rowVirtualizer.getVirtualItems();
48 |
49 | return (
50 |
51 |
52 |
Errors
53 |
54 | {isLoading &&
}
55 |
56 |
58 | setSeverityFilter(severityFilter === "error" ? null : "error")
59 | }
60 | >
61 |
70 |
71 | {errors.filter((error) => error.severity === "error").length}
72 |
73 |
74 |
76 | setSeverityFilter(
77 | severityFilter === "warning" ? null : "warning"
78 | )
79 | }
80 | >
81 |
90 |
91 | {errors.filter((error) => error.severity === "warning").length}
92 |
93 |
94 |
95 |
99 |
100 |
101 |
102 |
103 | {virtualizedRows.length > 0 ? (
104 |
110 |
116 | {virtualizedRows.map((virtualRow) => {
117 | const error = sortedErrors[virtualRow.index];
118 | return (
119 |
125 | goToError(error)}
128 | />
129 |
130 | );
131 | })}
132 |
133 |
134 | ) : (
135 |
No errors or warnings found
136 | )}
137 |
138 |
139 | );
140 | }
141 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/ErrorMessages/SortErrors.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | DropdownMenu,
4 | DropdownMenuCheckboxItem,
5 | DropdownMenuContent,
6 | DropdownMenuTrigger,
7 | } from "@/components/ui/dropdown-menu";
8 | import { Button } from "@/components/ui/button";
9 | import { ArrowUpDown } from "lucide-react";
10 | import { AgsError } from "@groundup/ags";
11 |
12 | export type SortOptionKey = "rule" | "lineNumberAsc" | "lineNumberDesc";
13 |
14 | export type SortOption = {
15 | name: string;
16 | compareFn: (_e1: AgsError, _e2: AgsError) => number;
17 | };
18 |
19 | function parseRule(rule: string | number): number {
20 | return parseInt(rule.toString().match(/\d+/)![0]);
21 | }
22 |
23 | export const sortOptions: Record = {
24 | rule: {
25 | name: "Rule",
26 | compareFn: (e1, e2) => parseRule(e1.rule) - parseRule(e2.rule),
27 | },
28 | lineNumberAsc: {
29 | name: "Line number (Asc)",
30 | compareFn: (e1, e2) => e1.lineNumber - e2.lineNumber,
31 | },
32 | lineNumberDesc: {
33 | name: "Line number (Desc)",
34 | compareFn: (e1, e2) => e2.lineNumber - e1.lineNumber,
35 | },
36 | };
37 |
38 | interface SortErrorsProps {
39 | activeSortOptionKey: SortOptionKey | null;
40 | onChange: (_sortOptionKey: SortOptionKey | null) => void;
41 | }
42 |
43 | export default function SortErrors({
44 | activeSortOptionKey,
45 | onChange,
46 | }: SortErrorsProps) {
47 | return (
48 |
49 |
50 |
51 | Sort
52 |
53 |
54 |
55 | {Object.entries(sortOptions).map(([sortOptionKey, sortOption]) => {
56 | const checked = sortOptionKey === activeSortOptionKey;
57 | return (
58 |
62 | onChange(checked ? null : (sortOptionKey as SortOptionKey))
63 | }
64 | >
65 | {sortOption.name}
66 |
67 | );
68 | })}
69 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/ErrorMessages/index.tsx:
--------------------------------------------------------------------------------
1 | import ErrorMessages from "./ErrorMessages";
2 |
3 | export default ErrorMessages;
4 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/GridView/GridView.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useRef, useState } from "react";
2 | import DataGrid, {
3 | GridCell,
4 | GridCellKind,
5 | GridColumn,
6 | Item,
7 | GridSelection,
8 | DataEditorProps,
9 | EditableGridCell,
10 | EditListItem,
11 | DataEditorRef,
12 | } from "@glideapps/glide-data-grid";
13 | import { AgsError } from "@groundup/ags";
14 | import "@glideapps/glide-data-grid/dist/index.css";
15 | import { useAppDispatch, useAppSelector } from "@/lib/redux/hooks";
16 | import {
17 | addRow,
18 | applySetRowDataEffect,
19 | GroupRawNormalized,
20 | redo,
21 | setRowsData,
22 | undo,
23 | } from "@/lib/redux/ags";
24 | import { useGridTheme } from "@/hooks/useGridTheme";
25 |
26 | // Props for the table component
27 | interface Props {
28 | groupName: string;
29 | selection?: GridSelection;
30 | setSelection?: React.Dispatch<
31 | React.SetStateAction
32 | >;
33 |
34 | setGoToErrorCallback: (callback: (error: AgsError) => void) => void;
35 | setSelectedRows: (rows: number[]) => void;
36 | }
37 |
38 | const GridView: React.FC = ({
39 | setGoToErrorCallback,
40 | groupName,
41 | setSelectedRows,
42 | selection,
43 | setSelection,
44 | }) => {
45 | const group = useAppSelector(
46 | (state) => state.ags.parsedAgsNormalized?.[groupName]
47 | ) as GroupRawNormalized;
48 |
49 | const canUndo = useAppSelector((state) => state.ags.past.length > 0);
50 | const canRedo = useAppSelector((state) => state.ags.future.length > 0);
51 |
52 | const errors = useAppSelector((state) => state.ags.errors);
53 |
54 | const dispatch = useAppDispatch();
55 |
56 | const ref = useRef(null);
57 |
58 | const { theme: customTheme } = useGridTheme();
59 |
60 | const onPaste = useCallback(
61 | (target: Item, values: readonly (readonly string[])[]): boolean => {
62 | const lineNumber = group.lineNumber + 4 + target[1];
63 | const headings = group.headings
64 | .map((heading) => heading.name)
65 | .slice(target[0], target[0] + values[0].length);
66 |
67 | const updates = values.map((value, index) => {
68 | const updateForRow = value.reduce((acc, val, idx) => {
69 | acc[headings[idx]] = val;
70 | return acc;
71 | }, {} as Record);
72 |
73 | return {
74 | group: group.name,
75 | lineNumber: lineNumber + index,
76 | update: updateForRow,
77 | };
78 | });
79 |
80 | dispatch(setRowsData(updates));
81 | dispatch(applySetRowDataEffect());
82 |
83 | return false;
84 | },
85 | [dispatch, group.headings, group.lineNumber, group.name]
86 | );
87 |
88 | useEffect(() => {
89 | if (selection?.current) {
90 | setSelectedRows([]);
91 | } else {
92 | if (selection) setSelectedRows(selection.rows.toArray());
93 | }
94 | }, [selection, setSelectedRows]);
95 |
96 | console.log("theme", customTheme);
97 |
98 | const scrollToError = useCallback(
99 | (error: AgsError) => {
100 | if (group.name !== error.group) {
101 | return;
102 | }
103 |
104 | const rowIndex = error.lineNumber - group.lineNumber - 4;
105 |
106 | const colIndex = group.headings.findIndex(
107 | (heading) => heading.name === error.field
108 | );
109 |
110 | const colNum = colIndex > -1 ? colIndex : 0; // if no field is found, default to the first column
111 |
112 | if (rowIndex < 0) {
113 | // if less than 0, then the error is in the group heading or units
114 | return;
115 | }
116 |
117 | if (setSelection)
118 | setSelection((cv: GridSelection | undefined) => {
119 | if (!cv) {
120 | return cv;
121 | }
122 | return {
123 | ...cv,
124 | current: {
125 | cell: [colNum, rowIndex],
126 | range: {
127 | x: colNum,
128 | y: rowIndex,
129 | width: 1,
130 | height: 1,
131 | },
132 | rangeStack: [],
133 | },
134 | };
135 | });
136 |
137 | ref.current?.scrollTo(colIndex, rowIndex, "both", 0, 0, {
138 | vAlign: "center",
139 | hAlign: "center",
140 | });
141 | },
142 | [group.headings, group.lineNumber, group.name, setSelection]
143 | );
144 |
145 | useEffect(() => {
146 | setGoToErrorCallback(() => scrollToError);
147 | }, [setGoToErrorCallback, scrollToError]);
148 |
149 | const [columns, setColumns] = useState([]);
150 |
151 | const highlights = React.useMemo(() => {
152 | return errors
153 | .filter((error) => error.group === group.name)
154 | .map((error) => {
155 | // minus 4 as there are 4 lines before the table starts
156 | const rowIndex = error.lineNumber - group.lineNumber - 4;
157 | return {
158 | color: error.severity === "error" ? "#DC262622" : "#F59E0B22",
159 | range: {
160 | x: 0,
161 | y: rowIndex,
162 | width: group.headings.length,
163 | height: 1,
164 | },
165 | style: "solid",
166 | };
167 | });
168 | }, [errors, group.name, group.lineNumber, group.headings.length]);
169 |
170 | useEffect(() => {
171 | setColumns(
172 | group.headings.map((heading) => ({
173 | title: heading.name,
174 | width: 100,
175 | }))
176 | );
177 | }, [group.headings]);
178 |
179 | const onCellsEdited = (newValues: readonly EditListItem[]) => {
180 | const cells: Item[] = newValues.map((cell) => cell.location);
181 | const values = newValues.map((cell) => cell.value) as EditableGridCell[];
182 |
183 | dispatch(
184 | setRowsData(
185 | cells.map((cell, index) => {
186 | const colNum = cell[0];
187 | const rowNum = cell[1];
188 | const heading = group.headings[colNum];
189 | const data =
190 | values[index]?.kind === GridCellKind.Text ? values[index].data : "";
191 |
192 | return {
193 | group: group.name,
194 | lineNumber: group.lineNumber + 4 + rowNum,
195 | update: {
196 | [heading.name]: data,
197 | },
198 | };
199 | })
200 | )
201 | );
202 |
203 | dispatch(applySetRowDataEffect());
204 | };
205 |
206 | const onCellEdited = () => {};
207 |
208 | const getData = useCallback(
209 | ([colNum, rowNum]: Item): GridCell => {
210 | const lineNumber = group.lineNumber + 4 + rowNum;
211 | const row = group.rows[lineNumber];
212 | const col = group.headings[colNum];
213 | const data = row && col ? row.data[col.name] : "";
214 |
215 | return {
216 | kind: GridCellKind.Text,
217 | data: data,
218 | allowOverlay: true,
219 | displayData: data,
220 | readonly: false,
221 | };
222 | },
223 | [group]
224 | );
225 |
226 | const onColumnResize = useCallback(
227 | (column: GridColumn, newSize: number) => {
228 | setColumns((prevColsMap) => {
229 | const index = prevColsMap.findIndex((ci) => ci.title === column.title);
230 | const newArray = [...prevColsMap];
231 | newArray.splice(index, 1, {
232 | ...prevColsMap[index],
233 | width: newSize,
234 | title: column.title,
235 | });
236 | return newArray;
237 | });
238 | },
239 | [setColumns]
240 | );
241 |
242 | const onRowAppended = useCallback(() => {
243 | dispatch(addRow({ group: group.name }));
244 | dispatch(applySetRowDataEffect());
245 | }, [dispatch, group.name]);
246 |
247 | useEffect(() => {
248 | const onKeyDown = (e: KeyboardEvent) => {
249 | if (e.key === "z" && (e.metaKey || e.ctrlKey)) {
250 | if (e.shiftKey && canRedo) {
251 | dispatch(redo());
252 | dispatch(applySetRowDataEffect());
253 | } else if (canUndo) {
254 | dispatch(undo());
255 | dispatch(applySetRowDataEffect());
256 | }
257 | }
258 |
259 | if (canRedo && e.key === "y" && (e.metaKey || e.ctrlKey)) {
260 | dispatch(redo());
261 | dispatch(applySetRowDataEffect());
262 | }
263 | };
264 | window.addEventListener("keydown", onKeyDown);
265 | return () => {
266 | window.removeEventListener("keydown", onKeyDown);
267 | };
268 | }, [dispatch, canUndo, canRedo]);
269 |
270 | return (
271 |
272 |
302 |
303 | );
304 | };
305 |
306 | export default GridView;
307 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/GridView/index.tsx:
--------------------------------------------------------------------------------
1 | import GridView from "./GridView";
2 |
3 | export default GridView;
4 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/SelectTable/SelectTable.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import AutoComplete from "@/components/ui/auto-complete";
6 | import { useAppDispatch } from "@/lib/redux/hooks";
7 | import { clearHistory } from "@/lib/redux/ags";
8 |
9 | type Props = {
10 | groups: string[];
11 | selectedGroup: string;
12 | setSelectedGroup: React.Dispatch>;
13 | };
14 |
15 | export default function SelectTable({
16 | groups,
17 | selectedGroup,
18 | setSelectedGroup,
19 | }: Props) {
20 | const options = groups.map((group) => ({ value: group, label: group }));
21 |
22 | const dispatch = useAppDispatch();
23 |
24 | const handleSelect = (value: React.SetStateAction) => {
25 | dispatch(clearHistory());
26 | setSelectedGroup(value);
27 | };
28 |
29 | return (
30 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/SelectTable/index.tsx:
--------------------------------------------------------------------------------
1 | import SelectTable from "./SelectTable";
2 |
3 | export default SelectTable;
4 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/TextArea/CodeMirrorTextArea.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useRef } from "react";
2 | import CodeMirror from "@uiw/react-codemirror";
3 | import { classname } from "@uiw/codemirror-extensions-classname";
4 | import { basicSetup } from "codemirror";
5 | import { EditorView } from "@codemirror/view";
6 | import { AgsError } from "@groundup/ags";
7 | import { useAppDispatch, useAppSelector } from "@/lib/redux/hooks";
8 | import { applySetRawDataEffect, setRawData } from "@/lib/redux/ags";
9 | import { useTheme } from "next-themes";
10 |
11 | type CodeMirrorTextAreaProps = {
12 | setGoToErrorCallback: (callback: (error: AgsError) => void) => void;
13 | };
14 |
15 | const CodeMirrorTextArea: React.FC = ({
16 | setGoToErrorCallback,
17 | }) => {
18 | const { theme } = useTheme();
19 | const agsData = useAppSelector((state) => state.ags.rawData);
20 | const errors = useAppSelector((state) => state.ags.errors);
21 | const dispatch = useAppDispatch();
22 |
23 | const isDark = theme === "dark";
24 |
25 | const errorLines = errors?.map((error) => error.lineNumber);
26 |
27 | const editorRef = useRef(null);
28 |
29 | const handleEditorChange = (value: string) => {
30 | dispatch(setRawData(value));
31 | dispatch(applySetRawDataEffect());
32 | };
33 |
34 | useEffect(() => {
35 | setGoToErrorCallback(() => goToErrorInText);
36 | }, [setGoToErrorCallback]);
37 |
38 | const goToErrorInText = (error: AgsError) => {
39 | if (editorRef.current) {
40 | editorRef.current.focus();
41 | editorRef.current.dispatch({
42 | selection: {
43 | anchor: editorRef.current.state.doc.line(error.lineNumber).from,
44 | head: editorRef.current.state.doc.line(error.lineNumber).to,
45 | },
46 | scrollIntoView: true,
47 | });
48 | }
49 | };
50 |
51 | const classNameExt = useMemo(
52 | () =>
53 | classname({
54 | add: (lineNumber) => {
55 | if (errorLines.includes(lineNumber)) {
56 | return "error-line"; // Highlight error lines
57 | }
58 | return undefined;
59 | },
60 | }),
61 | [errorLines]
62 | );
63 |
64 | const themeDemo = useMemo(
65 | () =>
66 | EditorView.baseTheme({
67 | "&": {
68 | backgroundColor: "transparent",
69 | },
70 | ".cm-content": {
71 | caretColor: "hsl(var(--foreground))",
72 | color: "hsl(var(--foreground))",
73 | },
74 | ".cm-cursor": {
75 | borderLeftColor: "hsl(var(--foreground))",
76 | },
77 | ".cm-selectionBackground, .cm-content ::selection": {
78 | backgroundColor: "hsl(var(--muted) / 0.3)",
79 | },
80 | ".error-line": {
81 | backgroundColor: "hsl(var(--destructive) / 0.15)",
82 | },
83 | ".cm-activeLine": {
84 | backgroundColor: "hsl(var(--muted) / 0.1)",
85 | },
86 | ".cm-line": {
87 | color: "hsl(var(--foreground))",
88 | },
89 | ".cm-activeLine.error-line": {
90 | backgroundColor: "hsl(var(--destructive) / 0.25)",
91 | },
92 | ".hover-line": {
93 | backgroundColor: "hsl(var(--warning) / 0.15) !important",
94 | },
95 | ".cm-scroller": {
96 | fontFamily: "monospace",
97 | borderRadius: "var(--radius)",
98 | border: "1px solid hsl(var(--border))",
99 | padding: "0.5rem",
100 | },
101 | ".cm-gutters": {
102 | backgroundColor: "hsl(var(--secondary))",
103 | color: "hsl(var(--secondary-foreground))",
104 | border: "none",
105 | borderTopLeftRadius: "var(--radius)",
106 | borderBottomLeftRadius: "var(--radius)",
107 | },
108 | ".cm-gutter": {
109 | backgroundColor: "transparent",
110 | color: "hsl(var(--muted-foreground))",
111 | },
112 | ".cm-activeLineGutter": {
113 | backgroundColor: "hsl(var(--muted) / 0.1)",
114 | },
115 | ".cm-lineNumbers": {
116 | color: "hsl(var(--muted-foreground))",
117 | },
118 | ".cm-placeholder": {
119 | color: "hsl(var(--muted-foreground))",
120 | },
121 | // Syntax highlighting
122 | ".cm-keyword": { color: "hsl(var(--primary))" },
123 | ".cm-property": { color: "hsl(var(--alternative))" },
124 | ".cm-string": { color: "hsl(var(--success))" },
125 | ".cm-number": { color: "hsl(var(--warning))" },
126 | ".cm-comment": { color: "hsl(var(--muted-foreground))" },
127 | }),
128 | []
129 | );
130 |
131 | return (
132 |
133 | {
143 | if (view?.view) {
144 | editorRef.current = view.view;
145 | }
146 | }}
147 | />
148 |
149 | );
150 | };
151 |
152 | export default CodeMirrorTextArea;
153 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/TextArea/index.tsx:
--------------------------------------------------------------------------------
1 | import CodeMirrorTextArea from "./CodeMirrorTextArea";
2 |
3 | export default CodeMirrorTextArea;
4 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/ViewToolbar/ViewToolbar.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { deleteRows, applySetRowDataEffect, undo, redo } from "@/lib/redux/ags";
3 | import { CompactSelection, GridSelection } from "@glideapps/glide-data-grid";
4 | import { Trash2 } from "lucide-react";
5 | import { FaRedo, FaUndo } from "react-icons/fa";
6 | import SelectTable from "../SelectTable";
7 | import { useAppDispatch, useAppSelector } from "@/lib/redux/hooks";
8 |
9 | interface Props {
10 | selectedRows: number[];
11 | setSelectedGroup: React.Dispatch>;
12 | selectedGroup: string;
13 | setSelection: (selection: GridSelection) => void;
14 | }
15 |
16 | export default function ViewToolbar({
17 | selectedRows,
18 | setSelectedGroup,
19 | selectedGroup,
20 | setSelection,
21 | }: Props) {
22 | const dispatch = useAppDispatch();
23 | const parsedAgs = useAppSelector((state) => state.ags.parsedAgsNormalized);
24 |
25 | const canUndo = useAppSelector((state) => state.ags.past.length > 0);
26 | const canRedo = useAppSelector((state) => state.ags.future.length > 0);
27 |
28 | if (parsedAgs === undefined) return null;
29 |
30 | const handleUndo = () => {
31 | dispatch(undo());
32 | dispatch(applySetRowDataEffect());
33 | };
34 |
35 | const handleRedo = () => {
36 | dispatch(redo());
37 | dispatch(applySetRowDataEffect());
38 | };
39 |
40 | return (
41 |
42 |
47 |
48 |
54 |
55 |
56 |
62 |
63 |
64 |
65 | {selectedRows.length > 0 && (
66 |
{
69 | setSelection({
70 | columns: CompactSelection.empty(),
71 | rows: CompactSelection.empty(),
72 | current: undefined,
73 | });
74 | dispatch(
75 | deleteRows({
76 | group: selectedGroup,
77 | rows: selectedRows,
78 | })
79 | );
80 |
81 | dispatch(applySetRowDataEffect());
82 | }}
83 | >
84 |
85 | {`Delete ${selectedRows.length} rows`}
86 |
87 | )}
88 |
89 | );
90 | }
91 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/ViewToolbar/index.tsx:
--------------------------------------------------------------------------------
1 | import ViewToolbar from "./ViewToolbar";
2 |
3 | export default ViewToolbar;
4 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/index.tsx:
--------------------------------------------------------------------------------
1 | import ValidatorProvider from "./validator-provider";
2 |
3 | export default ValidatorProvider;
4 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/store-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useRef } from "react";
3 | import { Provider } from "react-redux";
4 | import { makeStore, AppStore } from "@/lib/redux/store";
5 |
6 | export default function StoreProvider({
7 | children,
8 | }: {
9 | children: React.ReactNode;
10 | }) {
11 | const storeRef = useRef();
12 | if (!storeRef.current) {
13 | // Create the store instance the first time this renders
14 | storeRef.current = makeStore();
15 | }
16 |
17 | return {children} ;
18 | }
19 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/validator-provider.tsx:
--------------------------------------------------------------------------------
1 | import StoreProvider from "./store-provider";
2 | import Validator from "./validator";
3 |
4 | export default function ValidatorProvider() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/ags-validator-app/components/validator/validator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import ErrorMessages from "./ErrorMessages";
3 | import React, { SetStateAction, useCallback, useEffect, useState } from "react";
4 | import TextArea from "./TextArea";
5 | import AGSUpload from "./AGSUpload";
6 | import { Card, CardContent, CardTitle } from "@/components/ui/card";
7 | import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
8 | import { BsFileEarmarkText, BsTable } from "react-icons/bs";
9 | import GridView from "./GridView";
10 | import { AgsDictionaryVersion, AgsError } from "@groundup/ags";
11 | import AutoComplete from "../ui/auto-complete";
12 | import { Button } from "../ui/button";
13 | import { Download } from "lucide-react";
14 | import { downloadFile } from "@/lib/utils";
15 | import { useAppSelector } from "@/lib/redux/hooks";
16 | import { GridSelection } from "@glideapps/glide-data-grid";
17 | import ViewToolbar from "./ViewToolbar";
18 | import { track } from "@vercel/analytics";
19 | import { useAppDispatch } from "@/lib/redux/hooks";
20 | import { applySetRowDataEffect, setDictionaryVersion } from "@/lib/redux/ags";
21 | import {
22 | Tooltip,
23 | TooltipContent,
24 | TooltipProvider,
25 | TooltipTrigger,
26 | } from "../ui/tooltip";
27 |
28 | const agsDictOptions = [
29 | { value: "v4_0_3", label: "4.0.3" },
30 | { value: "v4_0_4", label: "4.0.4" },
31 | { value: "v4_1", label: "4.1" },
32 | { value: "v4_1_1", label: "4.1.1" },
33 | ];
34 |
35 | export default function Validator() {
36 | const dispatch = useAppDispatch();
37 |
38 | const agsDictVersion = useAppSelector(
39 | (state) => state.ags.agsDictionaryVersion
40 | );
41 |
42 | const [tabsViewValue, setTabsViewValue] = useState("text");
43 | const [selectedRows, setSelectedRows] = useState([]);
44 | const [selectedGroup, setSelectedGroup] = useState("");
45 |
46 | const [selection, setSelection] = useState(
47 | undefined
48 | );
49 |
50 | const parsedAgs = useAppSelector((state) => state.ags.parsedAgsNormalized);
51 | const agsData = useAppSelector((state) => state.ags.rawData);
52 |
53 | const [goToErrorCallback, setGoToErrorCallback] = useState<
54 | (error: AgsError) => void
55 | >(() => {});
56 |
57 | const goToError = useCallback(
58 | (error: AgsError) => {
59 | if (error.group) {
60 | setSelectedGroup(error.group);
61 | }
62 |
63 | goToErrorCallback(error);
64 | },
65 | [setSelectedGroup, goToErrorCallback]
66 | );
67 |
68 | // we need to populate the selectedGroup state when tables view is selected the first time
69 | useEffect(() => {
70 | if (parsedAgs && !selectedGroup && Object.keys(parsedAgs).length > 0) {
71 | const firstKey = Object.keys(parsedAgs)[0];
72 |
73 | if (firstKey) {
74 | setSelectedGroup(firstKey);
75 | }
76 | }
77 | }, [parsedAgs, selectedGroup, setSelectedGroup, setTabsViewValue]);
78 |
79 | return (
80 |
81 |
82 |
83 | AGS Options
84 |
85 |
86 | ) => {
90 | dispatch(setDictionaryVersion(version as AgsDictionaryVersion));
91 | dispatch(applySetRowDataEffect());
92 | }}
93 | label="Select AGS Version"
94 | placeholder="Select AGS Version"
95 | />
96 | {
100 | track("clicked export");
101 | downloadFile(agsData, "export.ags");
102 | }}
103 | >
104 |
105 | Export
106 |
107 |
108 |
109 |
110 |
111 | setTabsViewValue(value)}
114 | className="flex flex-col h-full"
115 | >
116 |
117 |
121 |
125 |
126 | Text
127 |
128 |
129 |
130 |
131 |
132 | {/* wrapping in div so can have tooltip on hover when disabled */}
133 |
134 |
139 |
140 | Tables
141 |
142 |
143 |
144 |
145 | {agsData !== "" && parsedAgs === undefined && (
146 |
147 | Current errors must be resolved before viewing as
148 | tables
149 |
150 | )}
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
169 |
170 | {parsedAgs && (
171 |
178 | )}
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 | );
192 | }
193 |
--------------------------------------------------------------------------------
/ags-validator-app/hooks/useGridTheme.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useMemo, useState } from "react";
2 | import { Theme } from "@glideapps/glide-data-grid";
3 | import { useTheme } from "next-themes";
4 |
5 | export const useGridTheme = () => {
6 | const { resolvedTheme } = useTheme();
7 | const isDark = resolvedTheme === "dark";
8 | const [cssUpdated, setCssUpdated] = useState(0);
9 |
10 | // watch for class changes on document.documentElement
11 | // this is a hack to force a recalculation of CSS variables
12 | useEffect(() => {
13 | const observer = new MutationObserver(() => {
14 | setCssUpdated((prev) => prev + 1);
15 | });
16 |
17 | observer.observe(document.documentElement, {
18 | attributes: true,
19 | attributeFilter: ["class"],
20 | });
21 |
22 | return () => observer.disconnect();
23 | }, []);
24 |
25 | const getCSSVariable = useCallback(
26 | (variableName: string, opacity?: number) => {
27 | const value = getComputedStyle(document.documentElement)
28 | .getPropertyValue(variableName)
29 | .trim();
30 |
31 | if (value.match(/^\d/)) {
32 | const [h, s, l] = value.split(" ");
33 |
34 | const formattedH = h;
35 | const formattedS = s.endsWith("%") ? s : `${s}%`;
36 | const formattedL = l.endsWith("%") ? l : `${l}%`;
37 |
38 | const result =
39 | opacity !== undefined
40 | ? `hsla(${formattedH}, ${formattedS}, ${formattedL}, ${opacity})`
41 | : `hsl(${formattedH}, ${formattedS}, ${formattedL})`;
42 |
43 | console.log(`Getting CSS var ${variableName}:`, {
44 | rawValue: value,
45 | opacity,
46 | result,
47 | components: { h: formattedH, s: formattedS, l: formattedL },
48 | });
49 |
50 | return result;
51 | }
52 |
53 | return value;
54 | },
55 | [cssUpdated]
56 | );
57 |
58 | const gridTheme: Partial = useMemo(() => {
59 | const theme = {
60 | // base colors and backgrounds
61 | bgCell: getCSSVariable("--background"),
62 | bgCellMedium: getCSSVariable("--muted", 0.1),
63 | bgHeader: getCSSVariable("--secondary"),
64 | bgHeaderHasFocus: getCSSVariable("--secondary"),
65 | bgHeaderHovered: getCSSVariable("--muted", 0.2),
66 | bgSearchResult: getCSSVariable("--warning", 0.2),
67 | bgIconHeader: getCSSVariable("--secondary"),
68 |
69 | // text
70 | textDark: getCSSVariable("--foreground"),
71 | textMedium: getCSSVariable("--muted-foreground"),
72 | textLight: getCSSVariable("--muted-foreground"),
73 | textBubble: getCSSVariable("--foreground"),
74 | textHeader: getCSSVariable("--secondary-foreground"),
75 | textHeaderSelected: getCSSVariable("--primary-foreground"),
76 | textGroupHeader: getCSSVariable("--secondary-foreground"),
77 |
78 | // icon colors
79 | fgIconHeader: getCSSVariable("--secondary-foreground"),
80 |
81 | // accent and selection
82 | accentColor: getCSSVariable("--primary"),
83 | accentLight: getCSSVariable("--primary", 0.2),
84 | accentFg: getCSSVariable("--primary-foreground"),
85 |
86 | // borders
87 | borderColor: getCSSVariable("--border"),
88 | horizontalBorderColor: getCSSVariable("--border"),
89 | drilldownBorder: getCSSVariable("--border"),
90 |
91 | // links
92 | linkColor: getCSSVariable("--primary"),
93 |
94 | // fonts and sizing
95 | // fontFamily: "inherit",
96 | };
97 |
98 | return theme;
99 | }, [isDark, getCSSVariable, cssUpdated]);
100 |
101 | return {
102 | theme: gridTheme,
103 | };
104 | };
105 |
--------------------------------------------------------------------------------
/ags-validator-app/lib/redux/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector, useStore } from "react-redux";
2 | import type { AppDispatch, AppStore, RootState } from "./store";
3 |
4 | // Use throughout your app instead of plain `useDispatch` and `useSelector`
5 | export const useAppDispatch = useDispatch.withTypes();
6 | export const useAppSelector = useSelector.withTypes();
7 | export const useAppStore = useStore.withTypes();
8 |
--------------------------------------------------------------------------------
/ags-validator-app/lib/redux/store.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import { agsSlice } from "./ags";
3 |
4 | export const makeStore = () => {
5 | return configureStore({
6 | reducer: {
7 | ags: agsSlice.reducer,
8 | },
9 | // middleware: (getDefaultMiddleware) =>
10 | // getDefaultMiddleware({
11 | // serializableCheck: false,
12 | // immutableCheck: false,
13 | // actionCreatorCheck: false,
14 | // }),
15 | });
16 | };
17 |
18 | // Infer the type of makeStore
19 | export type AppStore = ReturnType;
20 | // Infer the `RootState` and `AppDispatch` types from the store itself
21 | export type RootState = ReturnType;
22 | export type AppDispatch = AppStore["dispatch"];
23 |
--------------------------------------------------------------------------------
/ags-validator-app/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
8 | export function capitalize(str: string) {
9 | return str.charAt(0).toUpperCase() + str.slice(1);
10 | }
11 |
12 | export function downloadFile(data: string, filename: string) {
13 | const dataStr = "data:text/plain;charset=utf-8," + encodeURIComponent(data);
14 | const downloadAnchorNode = document.createElement("a");
15 | downloadAnchorNode.setAttribute("href", dataStr);
16 | downloadAnchorNode.setAttribute("download", filename);
17 | document.body.appendChild(downloadAnchorNode); // required for firefox
18 | downloadAnchorNode.click();
19 | downloadAnchorNode.remove();
20 | }
21 |
--------------------------------------------------------------------------------
/ags-validator-app/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/ags-validator-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@groundup/ags-validator-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@codemirror/view": "^6.33.0",
13 | "@glideapps/glide-data-grid": "^6.0.3",
14 | "@groundup-dev/ags": "^0.0.1",
15 | "@hookform/resolvers": "^3.9.0",
16 | "@kinde-oss/kinde-auth-nextjs": "^2.3.8",
17 | "@radix-ui/react-avatar": "^1.1.0",
18 | "@radix-ui/react-dialog": "^1.1.6",
19 | "@radix-ui/react-dropdown-menu": "^2.1.1",
20 | "@radix-ui/react-label": "^2.1.0",
21 | "@radix-ui/react-popover": "^1.1.1",
22 | "@radix-ui/react-separator": "^1.1.0",
23 | "@radix-ui/react-slot": "^1.1.0",
24 | "@radix-ui/react-tabs": "^1.1.0",
25 | "@radix-ui/react-tooltip": "^1.1.2",
26 | "@reduxjs/toolkit": "^2.3.0",
27 | "@tanstack/react-virtual": "^3.10.8",
28 | "@uiw/codemirror-extensions-classname": "^4.23.2",
29 | "@uiw/react-codemirror": "^4.23.2",
30 | "@vercel/analytics": "^1.3.1",
31 | "class-variance-authority": "^0.7.0",
32 | "clsx": "^2.1.1",
33 | "cmdk": "^1.0.0",
34 | "lucide-react": "^0.440.0",
35 | "next": "14.2.8",
36 | "next-themes": "^0.4.4",
37 | "react": "^18",
38 | "react-dom": "^18",
39 | "react-hook-form": "^7.53.0",
40 | "react-icons": "^5.3.0",
41 | "react-redux": "^9.1.2",
42 | "tailwind-merge": "^2.5.2",
43 | "tailwindcss-animate": "^1.0.7",
44 | "zod": "^3.23.8"
45 | },
46 | "devDependencies": {
47 | "@eslint/js": "^9.11.1",
48 | "@types/node": "^20",
49 | "@types/react": "^18",
50 | "@types/react-dom": "^18",
51 | "eslint": "^8.57.1",
52 | "eslint-config-next": "14.2.8",
53 | "postcss": "^8",
54 | "tailwindcss": "^3.4.1",
55 | "typescript": "^5.6.2"
56 | },
57 | "description": "AGS4 Validator by GroundUp",
58 | "main": "index.js",
59 | "directories": {
60 | "lib": "lib"
61 | },
62 | "keywords": [],
63 | "author": "",
64 | "license": "ISC"
65 | }
66 |
--------------------------------------------------------------------------------
/ags-validator-app/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/ags-validator-app/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | colors: {
13 | background: "hsl(var(--background))",
14 | foreground: "hsl(var(--foreground))",
15 | alternative: "hsl(var(--alternative))",
16 | card: {
17 | DEFAULT: "hsl(var(--card))",
18 | foreground: "hsl(var(--card-foreground))",
19 | },
20 | popover: {
21 | DEFAULT: "hsl(var(--popover))",
22 | foreground: "hsl(var(--popover-foreground))",
23 | },
24 | primary: {
25 | DEFAULT: "hsl(var(--primary))",
26 | foreground: "hsl(var(--primary-foreground))",
27 | },
28 | secondary: {
29 | DEFAULT: "hsl(var(--secondary))",
30 | foreground: "hsl(var(--secondary-foreground))",
31 | },
32 | muted: {
33 | DEFAULT: "hsl(var(--muted))",
34 | foreground: "hsl(var(--muted-foreground))",
35 | },
36 | accent: {
37 | DEFAULT: "hsl(var(--accent))",
38 | foreground: "hsl(var(--accent-foreground))",
39 | },
40 | destructive: {
41 | DEFAULT: "hsl(var(--destructive))",
42 | foreground: "hsl(var(--destructive-foreground))",
43 | },
44 | warning: {
45 | DEFAULT: "hsl(var(--warning))",
46 | foreground: "hsl(var(--warning-foreground))",
47 | },
48 | border: "hsl(var(--border))",
49 | input: "hsl(var(--input))",
50 | ring: "hsl(var(--ring))",
51 | chart: {
52 | "1": "hsl(var(--chart-1))",
53 | "2": "hsl(var(--chart-2))",
54 | "3": "hsl(var(--chart-3))",
55 | "4": "hsl(var(--chart-4))",
56 | "5": "hsl(var(--chart-5))",
57 | },
58 | },
59 | borderRadius: {
60 | lg: "var(--radius)",
61 | md: "calc(var(--radius) - 2px)",
62 | sm: "calc(var(--radius) - 4px)",
63 | },
64 | keyframes: {
65 | "accordion-down": {
66 | from: {
67 | height: "0",
68 | },
69 | to: {
70 | height: "var(--radix-accordion-content-height)",
71 | },
72 | },
73 | "accordion-up": {
74 | from: {
75 | height: "var(--radix-accordion-content-height)",
76 | },
77 | to: {
78 | height: "0",
79 | },
80 | },
81 | float: {
82 | "0%, 100%": { transform: "translateY(0)" },
83 | "50%": { transform: "translateY(-10px)" },
84 | },
85 | "pulse-slow": {
86 | "0%, 100%": { opacity: "1" },
87 | "50%": { opacity: "0.8" },
88 | },
89 | },
90 | animation: {
91 | "accordion-down": "accordion-down 0.2s ease-out",
92 | "accordion-up": "accordion-up 0.2s ease-out",
93 | float: "float 6s ease-in-out infinite",
94 | "pulse-slow": "pulse-slow 4s ease-in-out infinite",
95 | },
96 | },
97 | },
98 | plugins: [require("tailwindcss-animate")],
99 | };
100 |
101 | export default config;
102 |
--------------------------------------------------------------------------------
/ags-validator-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "jsx": "preserve",
5 | "target": "ES2015",
6 | "module": "ESNext",
7 | "strict": true,
8 | "esModuleInterop": true,
9 | // Strict Checks
10 | // "alwaysStrict": true,
11 | // "noImplicitAny": true,
12 | // "strictNullChecks": true,
13 | // "useUnknownInCatchVariables": true,
14 | // "strictPropertyInitialization": true,
15 | // "strictFunctionTypes": true,
16 | // "noImplicitThis": true,
17 | // "strictBindCallApply": true,
18 | // "noPropertyAccessFromIndexSignature": true,
19 | // "noUncheckedIndexedAccess": true,
20 | // "exactOptionalPropertyTypes": true,
21 | // // Linter Checks
22 | // "noImplicitReturns": true,
23 | // "noImplicitOverride": true,
24 | // "forceConsistentCasingInFileNames": true,
25 | // // https://eslint.org/docs/rules/consistent-return ?
26 | // "noFallthroughCasesInSwitch": true,
27 | // // https://eslint.org/docs/rules/no-fallthrough
28 | // "noUnusedLocals": true,
29 | // // https://eslint.org/docs/rules/no-unused-vars
30 | // "noUnusedParameters": true,
31 | // // https://eslint.org/docs/rules/no-unused-vars#args
32 | // "allowUnreachableCode": false,
33 | // // https://eslint.org/docs/rules/no-unreachable ?
34 | // "allowUnusedLabels": false,
35 | // // https://eslint.org/docs/rules/no-unused-labels
36 | // // Base Strict Checks
37 | // "noImplicitUseStrict": false,
38 | // "suppressExcessPropertyErrors": false,
39 | // "suppressImplicitAnyIndexErrors": false,
40 | // "noStrictGenericChecks": false,
41 | "incremental": true,
42 | "plugins": [
43 | {
44 | "name": "next"
45 | }
46 | ],
47 | "paths": {
48 | "@/*": ["./*"],
49 |
50 | "@groundup/ags": ["../ags/src"]
51 | },
52 | "allowJs": true,
53 | "skipLibCheck": true,
54 | "noEmit": true,
55 | "moduleResolution": "node",
56 | "resolveJsonModule": true,
57 | "isolatedModules": true
58 | },
59 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
60 | "exclude": ["node_modules"]
61 | }
62 |
--------------------------------------------------------------------------------
/ags-validator-app/workers/validateRawUpdateWorker.js:
--------------------------------------------------------------------------------
1 | import { validateAgsData } from "@groundup/ags";
2 |
3 |
4 | self.onmessage = (event) => {
5 |
6 | const {rawData, rulesConfig, agsDictionaryVersion } = event.data;
7 |
8 |
9 | const {errors, parsedAgs} = validateAgsData(rawData,agsDictionaryVersion, rulesConfig);
10 |
11 |
12 | const parsedAgsNormalized = parsedAgs ? Object.fromEntries(
13 | Object.entries(parsedAgs).map(([label, group]) => [
14 | label,
15 | {
16 | ...group,
17 | rows: Object.fromEntries(
18 | group.rows.map((row) => [row.lineNumber, row])
19 | ),
20 | },
21 | ])) : undefined;
22 |
23 | self.postMessage({ parsedAgsNormalized, errors });
24 | };
25 |
--------------------------------------------------------------------------------
/ags-validator-app/workers/validateRowUpdateWorker.js:
--------------------------------------------------------------------------------
1 | // validationWorker.js
2 | import {
3 | validateAgsDataParsed,
4 | validateAgsDataParsedWithDict,
5 | parsedAgsToString,
6 | } from "@groundup/ags";
7 |
8 | self.onmessage = (event) => {
9 | const {parsedAgsNormalized, rulesConfig, agsDictionaryVersion} = event.data;
10 |
11 | const parsedAgs = Object.fromEntries(
12 | Object.entries(parsedAgsNormalized).map(([label, group]) => [
13 | label,
14 | {
15 | ...group,
16 | rows: Object.values(group.rows),
17 | },
18 | ])
19 | );
20 |
21 | const errors = [
22 | ...validateAgsDataParsed(parsedAgs, rulesConfig),
23 | ...validateAgsDataParsedWithDict(parsedAgs, agsDictionaryVersion, rulesConfig),
24 | ];
25 | const rawData = parsedAgsToString(parsedAgs);
26 |
27 |
28 | self.postMessage({ rawData, errors });
29 | };
30 |
--------------------------------------------------------------------------------
/ags/.gitignore:
--------------------------------------------------------------------------------
1 | coverage
2 | dist
--------------------------------------------------------------------------------
/ags/.npmignore:
--------------------------------------------------------------------------------
1 |
2 | /node_modules
3 | /src
4 | rollup.config.js
5 | tsconfig.json
6 | .gitignore
7 |
--------------------------------------------------------------------------------
/ags/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "semi": true,
4 | "printWidth": 80,
5 | "tabWidth": 2,
6 | "endOfLine": "auto"
7 |
8 | }
--------------------------------------------------------------------------------
/ags/__tests__/rules/rulesForParsedAgs/checkDataTypes.test.ts:
--------------------------------------------------------------------------------
1 | import { AgsRaw, AgsError } from "../../../src/types";
2 | import { rule8 } from "../../../src/rules/rulesForParsedAgs/checkDataTypes";
3 |
4 | // Sample data for testing various data types
5 | const validData: AgsRaw = {
6 | LOCA: {
7 | name: "LOCA",
8 | lineNumber: 1,
9 | headings: [
10 | { name: "LOCA_ID", type: "ID", unit: "" },
11 | { name: "LOCA_DESC", type: "PA", unit: "" },
12 | { name: "LOCA_X", type: "X", unit: "m" },
13 | { name: "LOCA_Y", type: "X", unit: "m" },
14 | { name: "LOCA_DP", type: "2DP", unit: "m" },
15 | { name: "LOCA_DT", type: "DT", unit: "yyyy-mm-dd" },
16 | { name: "LOCA_SCI", type: "2SCI", unit: "" },
17 | { name: "LOCA_SF", type: "3SF", unit: "" },
18 | { name: "LOCA_YN", type: "YN", unit: "" },
19 | ],
20 | rows: [
21 | {
22 | data: {
23 | LOCA_ID: "LOC001",
24 | LOCA_DESC: "Description of Location",
25 | LOCA_X: "100.123",
26 | LOCA_Y: "200.456",
27 | LOCA_DP: "50.12", // 2DP
28 | LOCA_DT: "2023-09-01", // Valid date
29 | LOCA_SCI: "1.23e4", // 2 decimal scientific notation
30 | LOCA_SF: "123", // 3 significant figures
31 | LOCA_YN: "Y", // Yes/No valid value
32 | },
33 | lineNumber: 2,
34 | },
35 | ],
36 | },
37 | };
38 |
39 | describe("AGS Rule 8: Data types validation", () => {
40 | // Test for a valid case with all data types matching their respective schemas
41 | it("should return no errors when all data types are valid", () => {
42 | const errors: AgsError[] = rule8.validate(validData);
43 | expect(errors).toHaveLength(0);
44 | });
45 |
46 | // Test for invalid decimal places (2DP but value has 3 decimal places)
47 | it("should return an error for an invalid 2DP value", () => {
48 | const invalidData = {
49 | ...validData,
50 | LOCA: {
51 | ...validData.LOCA,
52 | rows: [
53 | {
54 | data: {
55 | ...validData.LOCA.rows[0].data,
56 | LOCA_DP: "50.123", // Invalid, should have only 2 decimal places
57 | },
58 | lineNumber: 2,
59 | },
60 | ],
61 | },
62 | };
63 |
64 | const errors: AgsError[] = rule8.validate(invalidData);
65 | expect(errors).toHaveLength(1);
66 | expect(errors[0].message).toContain(
67 | "Data variable '50.123' does not match the UNIT 'm' and TYPE '2DP'",
68 | );
69 | });
70 |
71 | // Test for invalid datetime format (YYYY-MM-DD expected)
72 | it("should return an error for an invalid DT value", () => {
73 | const invalidData = {
74 | ...validData,
75 | LOCA: {
76 | ...validData.LOCA,
77 | rows: [
78 | {
79 | data: {
80 | ...validData.LOCA.rows[0].data,
81 | LOCA_DT: "09-01-2023", // Invalid, expected YYYY-MM-DD
82 | },
83 | lineNumber: 2,
84 | },
85 | ],
86 | },
87 | };
88 |
89 | const errors: AgsError[] = rule8.validate(invalidData);
90 | expect(errors).toHaveLength(1);
91 | expect(errors[0].message).toContain(
92 | "Data variable '09-01-2023' does not match the UNIT 'yyyy-mm-dd' and TYPE 'DT'",
93 | );
94 | });
95 |
96 | // Test for invalid scientific notation (2SCI, expecting 2 decimal places)
97 | it("should return an error for an invalid 2SCI value", () => {
98 | const invalidData = {
99 | ...validData,
100 | LOCA: {
101 | ...validData.LOCA,
102 | rows: [
103 | {
104 | data: {
105 | ...validData.LOCA.rows[0].data,
106 | LOCA_SCI: "1.234e4", // Invalid, should only have 2 decimal places
107 | },
108 | lineNumber: 2,
109 | },
110 | ],
111 | },
112 | };
113 |
114 | const errors: AgsError[] = rule8.validate(invalidData);
115 | expect(errors).toHaveLength(1);
116 | expect(errors[0].message).toContain(
117 | "Data variable '1.234e4' does not match the UNIT '' and TYPE '2SCI'",
118 | );
119 | });
120 |
121 | // Test for invalid significant figures (3SF, value should have exactly 3 significant figures)
122 | it("should return an error for an invalid 3SF value", () => {
123 | const invalidData = {
124 | ...validData,
125 | LOCA: {
126 | ...validData.LOCA,
127 | rows: [
128 | {
129 | data: {
130 | ...validData.LOCA.rows[0].data,
131 | LOCA_SF: "0.12", // Invalid, should have 3 significant figures
132 | },
133 | lineNumber: 2,
134 | },
135 | ],
136 | },
137 | };
138 |
139 | const errors: AgsError[] = rule8.validate(invalidData);
140 | expect(errors).toHaveLength(1);
141 | expect(errors[0].message).toContain(
142 | "Data variable '0.12' does not match the UNIT '' and TYPE '3SF'",
143 | );
144 | });
145 | // Test for invalid significant figures (3SF, value should have exactly 3 significant figures)
146 | it("should return an error for an invalid 3SF value", () => {
147 | const invalidData = {
148 | ...validData,
149 | LOCA: {
150 | ...validData.LOCA,
151 | rows: [
152 | {
153 | data: {
154 | ...validData.LOCA.rows[0].data,
155 | LOCA_SF: "121234", // Invalid, should have 3 significant figures
156 | },
157 | lineNumber: 2,
158 | },
159 | ],
160 | },
161 | };
162 |
163 | const errors: AgsError[] = rule8.validate(invalidData);
164 | expect(errors).toHaveLength(1);
165 | expect(errors[0].message).toContain(
166 | "Data variable '121234' does not match the UNIT '' and TYPE '3SF'",
167 | );
168 | });
169 |
170 | // Test for invalid Yes/No value (YN, expecting Y or N)
171 | it("should return an error for an invalid YN value", () => {
172 | const invalidData = {
173 | ...validData,
174 | LOCA: {
175 | ...validData.LOCA,
176 | rows: [
177 | {
178 | data: {
179 | ...validData.LOCA.rows[0].data,
180 | LOCA_YN: "Maybe", // Invalid, only Y or N allowed
181 | },
182 | lineNumber: 2,
183 | },
184 | ],
185 | },
186 | };
187 |
188 | const errors: AgsError[] = rule8.validate(invalidData);
189 | expect(errors).toHaveLength(1);
190 | expect(errors[0].message).toContain(
191 | "Data variable 'Maybe' does not match the UNIT '' and TYPE 'YN'",
192 | );
193 | });
194 |
195 | // Test for an empty value (allowed case)
196 | it("should return no errors for an empty string in a field", () => {
197 | const emptyFieldData = {
198 | ...validData,
199 | LOCA: {
200 | ...validData.LOCA,
201 | rows: [
202 | {
203 | data: {
204 | ...validData.LOCA.rows[0].data,
205 | LOCA_DESC: "", // Empty value, which is allowed
206 | },
207 | lineNumber: 2,
208 | },
209 | ],
210 | },
211 | };
212 |
213 | const errors: AgsError[] = rule8.validate(emptyFieldData);
214 | expect(errors).toHaveLength(0);
215 | });
216 |
217 | // Test for a valid text field (ID, PA, PT, PU, X, XN, U)
218 | it("should return no errors for valid text field values", () => {
219 | const textFieldData = {
220 | ...validData,
221 | LOCA: {
222 | ...validData.LOCA,
223 | rows: [
224 | {
225 | data: {
226 | ...validData.LOCA.rows[0].data,
227 | LOCA_ID: "LOC001", // Valid ID
228 | LOCA_DESC: "Valid description", // Valid PA
229 | LOCA_X: "100.123", // Valid X
230 | },
231 | lineNumber: 2,
232 | },
233 | ],
234 | },
235 | };
236 |
237 | const errors: AgsError[] = rule8.validate(textFieldData);
238 | expect(errors).toHaveLength(0);
239 | });
240 | });
241 |
--------------------------------------------------------------------------------
/ags/__tests__/rules/rulesForParsedAgs/checkGroupAndHeadings.test.ts:
--------------------------------------------------------------------------------
1 | // import { rule7, rule19, rule19a, rule19b } from '../../src/rules/rulesForParsedAgs/checkGroupAndHeadings';
2 |
3 | import { AgsRaw } from "../../../src/types";
4 | import {
5 | rule7,
6 | rule19,
7 | rule19a,
8 | rule19b,
9 | } from "../../../src/rules/rulesForParsedAgs/checkGroupAndHeadings";
10 |
11 | // Example data for testing
12 | const validData: AgsRaw = {
13 | LOCA: {
14 | name: "LOCA",
15 | lineNumber: 1,
16 | headings: [
17 | { name: "LOCA_ID", type: "ID", unit: "" },
18 | { name: "LOCA_DESC", type: "X", unit: "" },
19 | ],
20 | rows: [
21 | { data: { LOCA_ID: "001", LOCA_DESC: "Location 1" }, lineNumber: 2 },
22 | ],
23 | },
24 | SAMP: {
25 | name: "SAMP",
26 | lineNumber: 10,
27 | headings: [
28 | { name: "LOCA_ID", type: "ID", unit: "" },
29 | { name: "SAMP_ID", type: "ID", unit: "" },
30 | { name: "SAMP_DPTH", type: "1DP", unit: "m" },
31 | ],
32 | rows: [
33 | {
34 | data: { SAMP_ID: "S001", SAMP_DPTH: "5.5", LOCA_ID: "001" },
35 | lineNumber: 11,
36 | },
37 | ],
38 | },
39 | };
40 |
41 | // Test for Rule 7
42 | describe("AGS Rule 7: Order of data fields must follow the HEADING row", () => {
43 | it("should return no errors when the heading order is correct and no duplicates are present", () => {
44 | const errors = rule7.validate(validData);
45 | expect(errors).toHaveLength(0);
46 | });
47 |
48 | it("should return an error if there are duplicate headings in a group", () => {
49 | const invalidData = {
50 | ...validData,
51 | LOCA: {
52 | ...validData.LOCA,
53 | headings: [
54 | { name: "LOCA_ID", type: "string", unit: "" },
55 | { name: "LOCA_ID", type: "string", unit: "" }, // Duplicate heading
56 | ],
57 | },
58 | };
59 |
60 | const errors = rule7.validate(invalidData);
61 | expect(errors).toHaveLength(1);
62 | expect(errors[0].message).toContain(
63 | "Duplicate headings found in the group",
64 | );
65 | });
66 | });
67 |
68 | // Test for Rule 19
69 | describe("AGS Rule 19: GROUP names should be <= 4 characters and uppercase letters/numbers only", () => {
70 | it("should return no errors for valid GROUP names", () => {
71 | const errors = rule19.validate(validData);
72 | expect(errors).toHaveLength(0);
73 | });
74 |
75 | it("should return an error for GROUP names that exceed 4 characters", () => {
76 | const invalidData = {
77 | INVALID: {
78 | name: "INVALID",
79 | lineNumber: 1,
80 | headings: [{ name: "INVALID_ID", type: "string", unit: "" }],
81 | rows: [{ data: { INVALID_ID: "001" }, lineNumber: 2 }],
82 | },
83 | };
84 |
85 | const errors = rule19.validate(invalidData);
86 | expect(errors).toHaveLength(1);
87 | expect(errors[0].message).toContain("Invalid GROUP name format");
88 | });
89 |
90 | it("should return an error for GROUP names with invalid characters", () => {
91 | const invalidData = {
92 | "LO@A": {
93 | name: "LO@A",
94 | lineNumber: 1,
95 | headings: [{ name: "LOCA_ID", type: "string", unit: "" }],
96 | rows: [{ data: { LOCA_ID: "001" }, lineNumber: 2 }],
97 | },
98 | };
99 |
100 | const errors = rule19.validate(invalidData);
101 | expect(errors).toHaveLength(1);
102 | expect(errors[0].message).toContain("Invalid GROUP name format");
103 | });
104 | });
105 |
106 | // Test for Rule 19a
107 | describe("AGS Rule 19a: HEADING names should be <= 9 characters and consist of uppercase letters, numbers, or underscores", () => {
108 | it("should return no errors for valid HEADING names", () => {
109 | const errors = rule19a.validate(validData);
110 | expect(errors).toHaveLength(0);
111 | });
112 |
113 | it("should return a warning for HEADING names that exceed 9 characters", () => {
114 | const invalidData = {
115 | ...validData,
116 | LOCA: {
117 | ...validData.LOCA,
118 | headings: [
119 | { name: "LOCA_VERYLONGNAME", type: "string", unit: "" }, // Too long
120 | { name: "LOCA_DESC", type: "string", unit: "" },
121 | ],
122 | },
123 | };
124 |
125 | const errors = rule19a.validate(invalidData);
126 | expect(errors).toHaveLength(1);
127 | expect(errors[0].message).toContain("Invalid HEADING name format");
128 | });
129 |
130 | it("should return a warning for HEADING names with invalid characters", () => {
131 | const invalidData = {
132 | ...validData,
133 | LOCA: {
134 | ...validData.LOCA,
135 | headings: [
136 | { name: "LOCA_DESC$", type: "string", unit: "" }, // Invalid character
137 | ],
138 | },
139 | };
140 |
141 | const errors = rule19a.validate(invalidData);
142 | expect(errors).toHaveLength(1);
143 | expect(errors[0].message).toContain("Invalid HEADING name format");
144 | });
145 | });
146 |
147 | // Test for Rule 19b
148 | describe("AGS Rule 19b: HEADING names must start with the GROUP name followed by an underscore", () => {
149 | it("should return no errors for valid HEADING names starting with GROUP name", () => {
150 | const errors = rule19b.validate(validData);
151 | expect(errors).toHaveLength(0);
152 | });
153 |
154 | it("should return an error for HEADING names that don't start with the GROUP name", () => {
155 | const invalidData = {
156 | LOCA: {
157 | ...validData.LOCA,
158 | headings: [
159 | { name: "LOC_ID", type: "string", unit: "" }, // Should be LOCA_ID
160 | ],
161 | },
162 | };
163 |
164 | const errors = rule19b.validate(invalidData);
165 | expect(errors).toHaveLength(1);
166 | expect(errors[0].message).toContain(
167 | "HEADING name does not start with the GROUP name",
168 | );
169 | });
170 |
171 | it("should return no errors if the HEADING name matches an existing heading in another group", () => {
172 | const matchingHeadingData = {
173 | LOCA: {
174 | name: "LOCA",
175 | lineNumber: 1,
176 | headings: [
177 | { name: "SAMP_DPTH", type: "1DP", unit: "m" }, // Same heading as in SAMP
178 | { name: "LOCA_ID", type: "ID", unit: "ID" },
179 | ],
180 | rows: [{ data: { SAMP_DPTH: "5.5" }, lineNumber: 2 }],
181 | },
182 | SAMP: validData.SAMP,
183 | };
184 |
185 | const errors = rule19b.validate(matchingHeadingData);
186 | expect(errors).toHaveLength(0);
187 | });
188 | });
189 |
--------------------------------------------------------------------------------
/ags/__tests__/rules/rulesForRawData.test.ts:
--------------------------------------------------------------------------------
1 | import { rulesForRawString } from "../../src/rules/rulesForRawData";
2 |
3 | describe("AGS Data Validation Rules", () => {
4 | describe("Rule 1: ASCII characters only", () => {
5 | it("should return no errors for a file with only ASCII characters", () => {
6 | const validAsciiData = 'GROUP, "UNIT", "DATA"\r\n';
7 | const errors = rulesForRawString.rule1.validate(validAsciiData);
8 | expect(errors).toHaveLength(0);
9 | });
10 |
11 | it("should return errors for a file with non-ASCII characters", () => {
12 | const invalidAsciiData = 'GROUP, "UNIT", "DÄTA"\r\n'; // Contains non-ASCII character Ä
13 | const errors = rulesForRawString.rule1.validate(invalidAsciiData);
14 | expect(errors).toHaveLength(1);
15 | });
16 | });
17 |
18 | describe("Rule 2a: Line ends with CR and LF", () => {
19 | it("should return no errors for lines properly terminated with CR and LF", () => {
20 | const validData = 'GROUP, "UNIT"\r\n"DATA", "VALUE"\r\n';
21 | const errors = rulesForRawString.rule2a.validate(validData);
22 | expect(errors).toHaveLength(0);
23 | });
24 |
25 | it("should return an error for lines not terminated with CR and LF", () => {
26 | const invalidData = 'GROUP, "UNIT"\n"DATA", "VALUE"'; // LF without CR
27 | const errors = rulesForRawString.rule2a.validate(invalidData);
28 | expect(errors).toHaveLength(1);
29 | });
30 | });
31 |
32 | describe("Rule 3: Each line starts with a valid data descriptor", () => {
33 | it("should return no errors for valid data descriptors", () => {
34 | const validData =
35 | '"GROUP","GROUP_NAME"\r\n"HEADING","Field1","Field2"\r\n';
36 | const errors = rulesForRawString.rule3.validate(validData);
37 | expect(errors).toHaveLength(0);
38 | });
39 |
40 | it("should return errors for lines without valid data descriptors", () => {
41 | const invalidData =
42 | '"INVALID","GROUP_NAME"\r\n"HEADING","Field1","Field2"\r\n';
43 | const errors = rulesForRawString.rule3.validate(invalidData);
44 | expect(errors).toHaveLength(1);
45 | });
46 | });
47 |
48 | describe("Rule 4.1: GROUP row should only contain the GROUP name", () => {
49 | it("should return no errors for valid GROUP rows", () => {
50 | const validData = '"GROUP","GROUP_NAME"\r\n';
51 | const errors = rulesForRawString.rule4_1.validate(validData);
52 | expect(errors).toHaveLength(0);
53 | });
54 |
55 | it("should return an error for GROUP rows with more than one field", () => {
56 | const invalidData = '"GROUP","GROUP_NAME","EXTRA_FIELD"\r\n';
57 | const errors = rulesForRawString.rule4_1.validate(invalidData);
58 | expect(errors).toHaveLength(1);
59 | });
60 |
61 | it("should return an error for malformed GROUP rows", () => {
62 | const invalidData = '"GROUP"\r\n';
63 | const errors = rulesForRawString.rule4_1.validate(invalidData);
64 | expect(errors).toHaveLength(1);
65 | });
66 | it("should return an error for malformed GROUP rows", () => {
67 | const invalidData = '"GROUP",';
68 | const errors = rulesForRawString.rule4_1.validate(invalidData);
69 | expect(errors).toHaveLength(1);
70 | });
71 | });
72 |
73 | describe("Rule 4.2: UNIT, TYPE, and DATA rows should have entries defined by the HEADING row", () => {
74 | it("should return no errors for UNIT, TYPE, DATA rows that match HEADING", () => {
75 | const validData =
76 | '"GROUP","GROUP_NAME"\r\n"HEADING","Field1","Field2"\r\n"UNIT","Unit1","Unit2"\r\n"TYPE","X","X"\r\n"DATA","2","1"\r\n';
77 | const errors = rulesForRawString.rule4_2.validate(validData);
78 | expect(errors).toHaveLength(0);
79 | });
80 |
81 | it("should return an error if HEADING row is missing for UNIT/TYPE/DATA rows", () => {
82 | const invalidData =
83 | '"GROUP","GROUP_NAME"\r\n"UNIT","Unit1","Unit2"\r\n"TYPE","X","X"\r\n"DATA","2","1"\r\n';
84 | const errors = rulesForRawString.rule4_2.validate(invalidData);
85 | expect(errors).toHaveLength(1);
86 | });
87 |
88 | it("should return an error if UNIT/TYPE/DATA rows do not match the HEADING row length", () => {
89 | const invalidData =
90 | '"GROUP","GROUP_NAME"\r\n"HEADING","Field1","Field2"\r\n"UNIT","Unit1"\r\n"TYPE","X","X"\r\n"DATA","2","1"\r\n';
91 | const errors = rulesForRawString.rule4_2.validate(invalidData);
92 | expect(errors).toHaveLength(1);
93 | });
94 |
95 | it("should return an error is not all unit type and data rows are present", () => {
96 | const invalidData =
97 | '"GROUP","GROUP_NAME"\r\n"HEADING","Field1","Field2"\r\n"DATA","Data1","Data2"\r\n';
98 | const errors = rulesForRawString.rule4_2.validate(invalidData);
99 | expect(errors).toHaveLength(1);
100 | });
101 | });
102 |
103 | describe("Rule 5: Fields must be enclosed in double quotes and handle internal quotes correctly", () => {
104 | it("should return no errors for properly quoted fields", () => {
105 | const validData =
106 | '"GROUP","GROUP_NAME"\r\n"DATA","Field1","he said ""hello"""\r\n';
107 | const errors = rulesForRawString.rule5.validate(validData);
108 | expect(errors).toHaveLength(0);
109 | });
110 | it("should return no errors for properly quoted fields", () => {
111 | const validData =
112 | '"DATA","831024","36A Halsey Street, London","Kensington, London","Drake Investments Limited","Ground Engineering Limited","Unknown","",""';
113 | const errors = rulesForRawString.rule5.validate(validData);
114 | expect(errors).toHaveLength(0);
115 | });
116 |
117 | it("should return an error if fields are not properly enclosed in double quotes", () => {
118 | const invalidData = 'GROUP,"GROUP_NAME"\r\n"DATA","Field1","Field2"\r\n'; // Missing quotes around GROUP
119 | const errors = rulesForRawString.rule5.validate(invalidData);
120 | expect(errors).toHaveLength(1);
121 | // expect(errors[0].message).toContain(
122 | // "Fields are not properly enclosed in double quotes",
123 | // );
124 | });
125 |
126 | it("should return an error if quotes within fields are not handled correctly", () => {
127 | const invalidData =
128 | '"GROUP","GROUP_NAME"\r\n"DATA","Field1","He said "He"llo"\r\n'; // Improper quote handling
129 | const errors = rulesForRawString.rule5.validate(invalidData);
130 | expect(errors).toHaveLength(1);
131 | });
132 | });
133 | });
134 |
--------------------------------------------------------------------------------
/ags/assets/Standard_dictionary_v4_0_3.ags:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/groundup-dev/ags-validator/46efe868d8231e960f29d14507d861cbc8487e95/ags/assets/Standard_dictionary_v4_0_3.ags
--------------------------------------------------------------------------------
/ags/assets/Standard_dictionary_v4_0_4.ags:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/groundup-dev/ags-validator/46efe868d8231e960f29d14507d861cbc8487e95/ags/assets/Standard_dictionary_v4_0_4.ags
--------------------------------------------------------------------------------
/ags/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from "globals";
2 | import pluginJs from "@eslint/js";
3 | import tseslint from "typescript-eslint";
4 |
5 |
6 | export default [
7 | {files: ["**/*.{js,mjs,cjs,ts}"]},
8 | {languageOptions: { globals: globals.browser }},
9 | pluginJs.configs.recommended,
10 | ...tseslint.configs.recommended,
11 | ];
--------------------------------------------------------------------------------
/ags/jest.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | moduleFileExtensions: ['ts', 'js'],
5 | transform: {
6 | '^.+\\.(ts|tsx)$': 'ts-jest', // Use ts-jest to transform TypeScript files
7 | },
8 |
9 | // setupFilesAfterEnv: ['./jest.setup.js'], // Adjust if you have setup files
10 | };
11 |
--------------------------------------------------------------------------------
/ags/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@groundup-dev/ags",
3 | "version": "0.2.2",
4 | "type": "module",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.js",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "import": "./dist/index.js",
11 | "types": "./dist/index.d.ts"
12 | }
13 | },
14 | "scripts": {
15 | "build:types": "tsc --emitDeclarationOnly",
16 | "build:js": "rollup -c",
17 | "build": "rimraf ./dist && npm run build:types && npm run build:js",
18 | "prepublishOnly": "npm run build",
19 | "test": "jest",
20 | "lint": "eslint src/"
21 | },
22 | "files": [
23 | "dist"
24 | ],
25 | "dependencies": {
26 | "zod": "^3.23.8"
27 | },
28 | "devDependencies": {
29 | "@eslint/js": "^9.11.1",
30 | "@rollup/plugin-json": "^6.0.0",
31 | "@rollup/plugin-node-resolve": "^15.0.0",
32 | "@rollup/plugin-typescript": "^12.1.0",
33 | "@types/jest": "^29.5.13",
34 | "@types/node": "^22.5.4",
35 | "eslint": "^9.11.1",
36 | "globals": "^15.9.0",
37 | "jest": "^29.7.0",
38 | "rollup": "^4.24.0",
39 | "ts-jest": "^29.2.5",
40 | "typescript": "^5.6.2",
41 | "typescript-eslint": "^8.7.0"
42 | },
43 | "keywords": [],
44 | "author": "",
45 | "license": "ISC",
46 | "description": ""
47 | }
48 |
--------------------------------------------------------------------------------
/ags/readme.md:
--------------------------------------------------------------------------------
1 | # Ags Validation Library
2 |
3 | This is a TypeScript library for validating AGS (Association of Geotechnical & Geoenvironmental Specialists) data files.
4 | ## Features
5 |
6 | - Validates AGS4 data files
7 | - Provides detailed error reporting
8 | - Supports all versions of AGS4
9 |
10 | ## Installation
11 |
12 | To install the AGS Validation Library, use npm:
13 |
14 | ```bash
15 | npm install @groundup-dev/ags
16 | ```
17 |
18 | ## Usage
19 |
20 | ```javascript
21 |
22 | import {
23 | validateAgsData,
24 |
25 | } from "@groundup-dev/ags";
26 |
27 | const agsData = "" // your ags data as string
28 |
29 | const dictVersion = "v4_0_4"
30 | // options:
31 | // "v4_0_3" | "v4_1_1" | "v4_1" | "v4_0_4";
32 |
33 | const { errors, parsedData } = validateAgsData(agsData, dictVersion)
34 |
35 |
36 | ```
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/ags/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript';
2 | import json from '@rollup/plugin-json';
3 | import { nodeResolve } from '@rollup/plugin-node-resolve';
4 |
5 | export default {
6 | input: 'src/index.ts',
7 | output: {
8 | dir: 'dist',
9 | format: 'esm',
10 | sourcemap: true,
11 | },
12 | plugins: [
13 | nodeResolve(),
14 | json({
15 | compact: true,
16 | preferConst: true,
17 | namedExports: true,
18 | }),
19 | typescript({
20 | tsconfig: './tsconfig.json',
21 | declaration: true,
22 | declarationMap: true,
23 | }),
24 | ],
25 | external: ['zod'],
26 | };
--------------------------------------------------------------------------------
/ags/scripts/buildDictionaries.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 | import { parseAgs } from "../src/parse";
4 |
5 | const assetsDir = path.join(__dirname, "../assets");
6 |
7 | fs.readdir(assetsDir, (err, files) => {
8 | if (err) {
9 | console.error("Error reading directory:", err);
10 | return;
11 | }
12 |
13 | files.forEach((file) => {
14 | if (!file.endsWith(".ags")) {
15 | return;
16 | }
17 |
18 | const filePath = path.join(assetsDir, file);
19 | fs.readFile(filePath, "utf8", (err, data) => {
20 | if (err) {
21 | console.error(`Error reading file ${file}:`, err);
22 | return;
23 | }
24 |
25 | const parsed = parseAgs(data);
26 | console.log(parsed);
27 | // save the parsed data to a file
28 | fs.writeFileSync(
29 | path.join(__dirname, `../assets/${file}.json`),
30 | JSON.stringify(parsed, null),
31 | );
32 | });
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/ags/scripts/profile.cjs:
--------------------------------------------------------------------------------
1 | // Importing fs module using CommonJS
2 | const fs = require('fs');
3 |
4 | const filepath = '/Users/jamesholcombe/Documents/git/ags-validator/B MONAGS_output_9.ags';
5 |
6 | // Define an async function to handle the dynamic import of validateAgsData
7 | async function main() {
8 | // Dynamically import the validateAgsData function from the ES module
9 | const { validateAgsData } = await import('../dist/index.js');
10 |
11 | // Read the file
12 | const data = fs.readFileSync(filepath, 'utf8');
13 |
14 | // Validate the data
15 | const { errors, parsedAgs } = validateAgsData(data);
16 |
17 | console.log(errors);
18 | }
19 |
20 | // Execute the main function
21 | main().catch(err => console.error(err));
22 |
--------------------------------------------------------------------------------
/ags/src/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | validateAgsData,
3 | validateAgsDataParsed,
4 | validateAgsDataParsedWithDict,
5 | defaultRulesConfig,
6 | } from "./validate";
7 | import type { RulesConfig } from "./validate";
8 |
9 | import type {
10 | AgsError,
11 | AgsRaw,
12 | AgsDictionaryVersion,
13 | GroupRaw,
14 | HeadingRaw,
15 | RowRaw,
16 | } from "./types";
17 | import { parsedAgsToString } from "./parse";
18 |
19 | export {
20 | defaultRulesConfig,
21 | validateAgsData,
22 | validateAgsDataParsed,
23 | validateAgsDataParsedWithDict,
24 | AgsError,
25 | AgsRaw,
26 | AgsDictionaryVersion,
27 | GroupRaw,
28 | parsedAgsToString,
29 | HeadingRaw,
30 | RowRaw,
31 | RulesConfig,
32 | };
33 |
--------------------------------------------------------------------------------
/ags/src/parse.ts:
--------------------------------------------------------------------------------
1 | import { GroupRaw, AgsRaw } from "./types";
2 |
3 | // Function to parse a single line into an array of strings
4 | export function parseLine(input: string): string[] {
5 | // Each entry in input should be surrounded by double quotes
6 | // Each entry should be separated by a comma
7 | // Input should be parsed into an array of strings
8 |
9 | const regex = /"([^"]*)"/g;
10 |
11 | const parsedLine: string[] = [];
12 | let match: RegExpExecArray | null = regex.exec(input); // Initial assignment
13 |
14 | // Extract all quoted strings from the current line
15 | while (match !== null) {
16 | parsedLine.push(match[1]); // Match group 1 contains the value inside quotes
17 | match = regex.exec(input); // Update match for the next iteration
18 | }
19 |
20 | return parsedLine;
21 | }
22 |
23 | // Function to parse a single group
24 | export function parseGroup(input: string, startLineNumber: number): GroupRaw {
25 | // Splitting lines with regex assuming newline characters split lines correctly
26 | const newLineRegex = /\r?\n/; // Handles both Windows and Unix newlines
27 | const parsedInput = input.split(newLineRegex);
28 | const lines = parsedInput.map((line) => parseLine(line));
29 |
30 | // Extracting key parts of the group
31 | const [
32 | [, groupName],
33 | [, ...headings],
34 | [, ...units],
35 | [, ...types],
36 | ...dataLines
37 | ] = lines;
38 |
39 | // remove datalines that are empty, or do not start WITH DATA
40 | const dataLinesEmptyRemoved = dataLines.filter(
41 | (line) => line.length > 0 && line[0] === "DATA",
42 | );
43 |
44 | // assign data,
45 | const data = dataLinesEmptyRemoved.map((line, index) => ({
46 | data: Object.fromEntries(
47 | line.slice(1).map((value, i) => [headings[i], value]),
48 | ),
49 | lineNumber: startLineNumber + index + 4, // Data lines start after the first three lines (group name, headings, units, types)
50 | }));
51 |
52 | const columns = headings.map((heading, index) => ({
53 | name: heading,
54 | type: types[index],
55 | unit: units[index],
56 | }));
57 |
58 | return {
59 | name: groupName,
60 | headings: columns,
61 | rows: data,
62 | lineNumber: startLineNumber,
63 | };
64 | }
65 |
66 | // Function to parse the entire AGS input into a structured AgsRaw object
67 | export const parseAgs = (input: string): AgsRaw => {
68 | const splitter = /\n"GROUP",/;
69 |
70 | const parsedInput = input
71 | .split(splitter)
72 | .map((group, index) => (index === 0 ? group : `"GROUP",${group}`));
73 |
74 | // Accumulate parsed groups with their starting line numbers
75 | let currentLineNumber = 1; // Tracks the current line number
76 | const groups = parsedInput.map((group) => {
77 | const groupLines = group.split(/\r?\n/).length; // Count lines in the group
78 | const parsedGroup = parseGroup(group, currentLineNumber);
79 | currentLineNumber += groupLines; // Update the current line number for the next group
80 | return parsedGroup;
81 | });
82 |
83 | const ags: AgsRaw = {};
84 | groups.forEach((group) => {
85 | ags[group.name] = group;
86 | });
87 |
88 | return ags;
89 | };
90 |
91 | export const parsedAgsToString = (parsedAgs: AgsRaw): string => {
92 | const groups = Object.values(parsedAgs);
93 | const groupStrings = groups.map((group) => {
94 | const headings = group.headings.map((heading) => `"${heading.name}"`);
95 | const units = group.headings.map((heading) => `"${heading.unit}"`);
96 | const types = group.headings.map((heading) => `"${heading.type}"`);
97 | const data = group.rows.map((row) => {
98 | const values = group.headings.map(
99 | (heading) => `"${row.data[heading.name]}"`,
100 | );
101 | return `"DATA",${values.join(",")}`;
102 | });
103 |
104 | return [
105 | `"GROUP","${group.name}"`,
106 | `"HEADING",${headings.join(",")}`,
107 | `"UNIT",${units.join(",")}`,
108 | `"TYPE",${types.join(",")}`,
109 | ...data,
110 | ].join("\n");
111 | });
112 |
113 | return groupStrings.join("\n\n");
114 | };
115 |
--------------------------------------------------------------------------------
/ags/src/rules/index.ts:
--------------------------------------------------------------------------------
1 | import { rulesForRawString } from "./rulesForRawData";
2 | import {
3 | rulesForParsedAgs,
4 | rulesForParsedAgsWithDict,
5 | } from "./rulesForParsedAgs";
6 |
7 | export { rulesForRawString, rulesForParsedAgs, rulesForParsedAgsWithDict };
8 |
--------------------------------------------------------------------------------
/ags/src/rules/rulesForParsedAgs/checkDataTypes.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { HeadingRaw, AgsRaw, AgsError } from "../../types";
3 |
4 | import { AgsValidationStepParsed } from "./types";
5 |
6 | function createDpHeadingSchema(heading: HeadingRaw): z.ZodType {
7 | const decimalPlaces = parseInt(heading.type[0]);
8 |
9 | if (decimalPlaces == 0) {
10 | return z.string().regex(/^-?\d+$/);
11 | } else {
12 | const schema = z
13 | .string()
14 | .regex(new RegExp(`^-?\\d+\\.\\d{${decimalPlaces}}$`));
15 |
16 | return schema;
17 | }
18 | }
19 |
20 | function createDtHeadingSchema(heading: HeadingRaw): z.ZodType {
21 | // yyyy-mm-ddThh:mm:ss.sssZ(+hh:mm) or yyyy-mm-dd or
22 | // hh:mm:ss or yyyy
23 |
24 | let pattern = "";
25 | for (const char of heading.unit) {
26 | if (["y", "m", "d", "h", "s"].includes(char)) {
27 | // if char is a valid date character, we assume it is placeholder for value
28 | pattern += "\\d";
29 | } else if (char === "+") {
30 | // if + then + or minus is allowed
31 | pattern += "[+-]";
32 | } else {
33 | // if not a date character, then it is a separator, or label like T or Z
34 | pattern += char;
35 | }
36 | }
37 |
38 | return z.string().regex(new RegExp(pattern));
39 | }
40 |
41 | function createTHeadingSchema(heading: HeadingRaw): z.ZodType {
42 | // hh:mm:ss
43 | let pattern = "";
44 | for (const char of heading.unit) {
45 | if (["h", "m", "s"].includes(char)) {
46 | // if char is a valid date character, we assume it is placeholder for value
47 | pattern += "\\d";
48 | } else {
49 | // if not a date character, then it is a separator
50 | pattern += char;
51 | }
52 | }
53 |
54 | return z.string().regex(new RegExp(pattern));
55 | }
56 |
57 | function checkSignificantFigures(input: string, n: number): boolean {
58 | // first handle integers and make sure there are not too many significant figures
59 | if (!input.includes(".")) {
60 | // count all leading non-zero digits
61 | const leadingNonZero = input.match(/^[1-9]+/);
62 |
63 | if (leadingNonZero === null) return true; // if no leading non-zero digits, then it is 0
64 |
65 | return leadingNonZero[0].length <= n;
66 | }
67 |
68 | const num = parseFloat(input);
69 | const precise = num.toPrecision(n);
70 |
71 | return precise === input;
72 | }
73 |
74 | function createSigFigHeadingSchema(heading: {
75 | type: string;
76 | }): z.ZodType {
77 | const nSigFig = parseInt(heading.type.replace(/SF/g, ""));
78 |
79 | return z.string().refine((value) => checkSignificantFigures(value, nSigFig), {
80 | message: `Must have exactly ${nSigFig} significant figures`,
81 | });
82 | }
83 |
84 | function createNSCIHeadingSchema(heading: HeadingRaw): z.ZodType {
85 | const nInt = parseInt(heading.type.replace(/SCI/g, ""));
86 |
87 | // match scientific notation with e or E
88 | const pattern = `[-+]?\\d+\\.\\d{${nInt}}[eE][-+]?\\d+`;
89 |
90 | return z.string().regex(new RegExp(pattern));
91 | }
92 |
93 | function createZodSchemasForHeadings(
94 | ags: AgsRaw,
95 | ): Record>> {
96 | const groupSchemas: Record>> = {};
97 |
98 | for (const [groupName, group] of Object.entries(ags)) {
99 | groupSchemas[groupName] = {};
100 |
101 | // at this point we are assuming that headings are of suitable type,
102 | // other validation should have been done before this point
103 | // we perform a switch on the heading type to determine the schema
104 |
105 | // decimal place headings
106 | const xDPHeadings = group.headings.filter((heading) =>
107 | heading.type.endsWith("DP"),
108 | );
109 | for (const heading of xDPHeadings) {
110 | groupSchemas[groupName][heading.name] = createDpHeadingSchema(heading);
111 | }
112 |
113 | // datetime headings
114 | const DTHeadings = group.headings.filter(
115 | (heading) => heading.type === "DT",
116 | );
117 | for (const heading of DTHeadings) {
118 | groupSchemas[groupName][heading.name] = createDtHeadingSchema(heading);
119 | }
120 |
121 | // scientific notation headings
122 | const NSCIHeadings = group.headings.filter((heading) =>
123 | heading.type.endsWith("SCI"),
124 | );
125 |
126 | for (const heading of NSCIHeadings) {
127 | groupSchemas[groupName][heading.name] = createNSCIHeadingSchema(heading);
128 | }
129 |
130 | // significant figures headings
131 | const SFHeadings = group.headings.filter((heading) =>
132 | heading.type.endsWith("SF"),
133 | );
134 | for (const heading of SFHeadings) {
135 | groupSchemas[groupName][heading.name] =
136 | createSigFigHeadingSchema(heading);
137 | }
138 |
139 | const YNHeadings = group.headings.filter((heading) =>
140 | heading.type.endsWith("YN"),
141 | );
142 |
143 | // yes no headings
144 | for (const heading of YNHeadings) {
145 | // r'^(Y|N|y|n)$'
146 | groupSchemas[groupName][heading.name] = z.string().regex(/^[YNyn]$/);
147 | }
148 |
149 | // time headings
150 | const THeadings = group.headings.filter((heading) => heading.type === "T");
151 | for (const heading of THeadings) {
152 | groupSchemas[groupName][heading.name] = createTHeadingSchema(heading);
153 | }
154 |
155 | // all others are treated as strings
156 | // note we are not checking picklist values here, they are just treated as text
157 | const textHeadings = group.headings.filter((heading) =>
158 | ["ID", "PA", "PT", "PU", "X", "XN", "U"].includes(heading.type),
159 | );
160 | for (const heading of textHeadings) {
161 | groupSchemas[groupName][heading.name] = z.string();
162 | }
163 | }
164 |
165 | // allow empty strings for all fields, as they are treated as optional
166 | for (const [groupName, group] of Object.entries(ags)) {
167 | for (const heading of group.headings) {
168 | groupSchemas[groupName][heading.name] = z.union([
169 | groupSchemas[groupName][heading.name] || z.string(),
170 | z.string().length(0),
171 | ]);
172 | }
173 | }
174 |
175 | return groupSchemas;
176 | }
177 |
178 | export const rule8: AgsValidationStepParsed = {
179 | rule: 8,
180 | description:
181 | "Data variables shall be presented in units of measurements\
182 | and type that are described by the appropriate data field UNIT and data\
183 | field TYPE defined at the start of the GROUP.",
184 |
185 | validate: function (ags: AgsRaw): AgsError[] {
186 | const errors: AgsError[] = [];
187 |
188 | const schemas = createZodSchemasForHeadings(ags);
189 |
190 | for (const [groupName, group] of Object.entries(ags)) {
191 | for (const row of group.rows) {
192 | for (const heading of group.headings) {
193 | const fieldName = heading.name;
194 | const fieldValue = row.data[fieldName];
195 |
196 | try {
197 | schemas[groupName][fieldName].parse(fieldValue);
198 | } catch {
199 | errors.push({
200 | rule: this.rule,
201 | lineNumber: row.lineNumber,
202 | group: groupName,
203 | field: fieldName,
204 | severity: "error",
205 | message: `Data variable '${fieldValue}' does not match the UNIT '${heading.unit}' and TYPE '${heading.type}' defined in the HEADING.`,
206 | });
207 | }
208 | }
209 | }
210 | }
211 | return errors;
212 | },
213 | };
214 |
--------------------------------------------------------------------------------
/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/src/rules/rulesForParsedAgs/checkHeadingsWithDict.ts:
--------------------------------------------------------------------------------
1 | import { AgsError } from "../../types";
2 | import { AgsValidationStepParsedWithDict } from "./types";
3 | import { combineDicts, getDictForVersion } from "../../standardDictionaries";
4 |
5 | // this also convers
6 | // Rule 12 Data does not have to be included against each HEADING unless REQUIRED (Rule 10b). The
7 | // data FIELD can be null; a null entry is defined as "" (two quotes together).
8 |
9 | export const rule10b: AgsValidationStepParsedWithDict = {
10 | rule: "10b",
11 | description:
12 | "Some HEADINGs are marked as REQUIRED. REQUIRED fields must appear in the data GROUPs where they are indicated in the AGS FORMAT DATA DICTIONARY. These fields require data entry and cannot be null (i.e., left blank or empty).",
13 | validate: function (ags, dictVersion): AgsError[] {
14 | const dict = getDictForVersion(dictVersion);
15 | const groups = Object.keys(ags);
16 |
17 | const dictCombined = combineDicts(dict.DICT, ags.DICT);
18 |
19 | const errors: AgsError[] = [];
20 |
21 | for (const group of groups) {
22 | // Get the dictionary entries for the current group
23 | const requiredHeadings = dictCombined.rows.filter(
24 | (entry) =>
25 | entry.data.DICT_GRP === group &&
26 | entry.data.DICT_STAT.includes("REQUIRED"),
27 | );
28 |
29 | if (requiredHeadings.length === 0) {
30 | // No required fields for this group, skip
31 | continue;
32 | }
33 |
34 | const requiredHeadingNames = requiredHeadings.map(
35 | (heading) => heading.data.DICT_HDNG,
36 | );
37 |
38 | for (const row of ags[group].rows) {
39 | for (const requiredHeading of requiredHeadingNames) {
40 | const value = row.data[requiredHeading];
41 |
42 | if (value === undefined || value === null || value.trim() === "") {
43 | errors.push({
44 | rule: "10b",
45 | severity: "error",
46 | lineNumber: row.lineNumber,
47 | group: group,
48 | field: requiredHeading,
49 | message: `The required field "${requiredHeading}" in group ${group} cannot be null or empty.`,
50 | });
51 | }
52 | }
53 | }
54 | }
55 |
56 | return errors;
57 | },
58 | };
59 |
60 | export const rule10c: AgsValidationStepParsedWithDict = {
61 | rule: "10c",
62 | description:
63 | "Links are made between data rows in GROUPs by the KEY fields. Every entry made in the KEY fields in any GROUP must have an equivalent entry in its PARENT GROUP. The PARENT GROUP must be included within the data file.",
64 | validate: function (ags, dictVersion): AgsError[] {
65 | const dict = getDictForVersion(dictVersion);
66 | const groups = Object.keys(ags);
67 |
68 | const errors: AgsError[] = [];
69 |
70 | const dictCombined = combineDicts(dict.DICT, ags.DICT);
71 |
72 | // get dictionary entries for relevant groups
73 | const dictEntries = dictCombined.rows.filter((row) => {
74 | return groups.includes(row.data.DICT_GRP);
75 | });
76 |
77 | for (const group of groups) {
78 | // extract parent group for the current group from the dictionary
79 |
80 | const parentGroup = dictEntries.find(
81 | (entry) =>
82 | entry.data.DICT_GRP === group && entry.data.DICT_TYPE === "GROUP",
83 | )?.data.DICT_PGRP;
84 |
85 | if (!parentGroup || parentGroup === "-" || parentGroup === "PROJ") {
86 | // If parent group is not included in the file, skip the check, or if group is LOCA
87 |
88 | continue;
89 | }
90 |
91 | if (!ags[parentGroup]) {
92 | errors.push({
93 | lineNumber: ags[group].lineNumber,
94 | rule: "10c",
95 | severity: "error",
96 | group: group,
97 | message: `Parent group ${parentGroup} is not present in the AGS file for group ${group}`,
98 | });
99 | continue;
100 | }
101 |
102 | const parentKeyHeadings = dictEntries.filter(
103 | (entry) =>
104 | entry.data.DICT_GRP === parentGroup &&
105 | entry.data.DICT_STAT.includes("KEY"),
106 | );
107 |
108 | const parentKeySet = new Set();
109 |
110 | for (const parentRow of ags[parentGroup].rows) {
111 | const parentKey = parentKeyHeadings
112 | .map((heading) => parentRow.data[heading.data.DICT_HDNG])
113 | .join("|");
114 | parentKeySet.add(parentKey);
115 | }
116 |
117 | // Check if every key in the current group has a match in the parent group
118 | for (const row of ags[group].rows) {
119 | const rowKey = parentKeyHeadings
120 | .map((heading) => row.data[heading.data.DICT_HDNG])
121 | .join("|");
122 |
123 | const keys = rowKey.split("|");
124 |
125 | if (!parentKeySet.has(rowKey)) {
126 | errors.push({
127 | rule: "10c",
128 | severity: "error",
129 | lineNumber: row.lineNumber,
130 | group: group,
131 | message: `Key value combination '${keys.join(
132 | ",",
133 | )}' in group ${group} does not have a corresponding entry in the parent group ${parentGroup}`,
134 | });
135 | }
136 | }
137 | }
138 |
139 | return errors;
140 | },
141 | };
142 |
143 | export const rule10a: AgsValidationStepParsedWithDict = {
144 | rule: "10a",
145 | description:
146 | "Links are made between data rows in GROUPs by the KEY fields. Every entry made in the KEY fields in any GROUP must have an equivalent entry in its PARENT GROUP. The PARENT GROUP must be included within the data file.",
147 | validate: function (ags, dictVersion): AgsError[] {
148 | const dict = getDictForVersion(dictVersion);
149 | const groups = Object.keys(ags);
150 |
151 | const dictCombined = combineDicts(dict.DICT, ags.DICT);
152 |
153 | // first filter out dict.DICT from groups
154 | const dictEntries = dictCombined.rows.filter((row) => {
155 | return groups.includes(row.data.DICT_GRP);
156 | });
157 |
158 | const errors: AgsError[] = [];
159 |
160 | for (const group of groups) {
161 | const keyHeadings = dictEntries.filter(
162 | (entry) =>
163 | entry.data.DICT_GRP === group && entry.data.DICT_STAT.includes("KEY"),
164 | );
165 |
166 | // find non-unique rows in the group
167 |
168 | const nonUniqueRows = [];
169 | const rowValues = new Map();
170 |
171 | for (const row of ags[group].rows) {
172 | const key = keyHeadings
173 | .map((heading) => row.data[heading.data.DICT_HDNG])
174 | .join("|");
175 |
176 | if (!rowValues.has(key)) {
177 | rowValues.set(key, []);
178 | }
179 |
180 | rowValues.get(key)!.push(row);
181 | }
182 |
183 | for (const [, rows] of rowValues.entries()) {
184 | if (rows.length > 1) {
185 | nonUniqueRows.push(...rows);
186 | }
187 | }
188 |
189 | for (const row of nonUniqueRows) {
190 | errors.push({
191 | rule: "10a",
192 | severity: "error",
193 | lineNumber: row.lineNumber,
194 | group: group,
195 | message: `Duplicate key values found in group ${group}`,
196 | });
197 | }
198 | }
199 |
200 | return errors;
201 | },
202 | };
203 |
204 | export const rule9: AgsValidationStepParsedWithDict = {
205 | rule: "9",
206 | description:
207 | "Data HEADING and GROUP names shall be taken from the AGS FORMAT DATA DICTIONARY. In cases where there is no suitable entry, a user-defined GROUP and/or HEADING may be used in accordance with Rule 18. Any user-defined HEADINGs shall be included at the end of the HEADING row after the standard HEADINGs in the order defined in the DICT group (see Rule18a)",
208 | validate: function (ags, dictVersion): AgsError[] {
209 | const dict = getDictForVersion(dictVersion);
210 | const groups = Object.keys(ags);
211 |
212 | const dictEntries = dict.DICT.rows.filter((row) => {
213 | return groups.includes(row.data.DICT_GRP);
214 | });
215 |
216 | const errors: AgsError[] = [];
217 |
218 | for (const group of groups) {
219 | ags[group].headings.forEach((heading) => {
220 | // if the heading is not in the dictionary, add it to the nonStandardHeadings array
221 | if (
222 | dictEntries.some(
223 | (entry) =>
224 | entry.data.DICT_GRP === group &&
225 | entry.data.DICT_HDNG === heading.name,
226 | )
227 | ) {
228 | return;
229 | // if found in standard dictionary, return
230 | }
231 |
232 | if (
233 | ags.DICT?.rows.some(
234 | (entry) =>
235 | entry.data.DICT_GRP === group &&
236 | entry.data.DICT_HDNG === heading.name,
237 | )
238 | ) {
239 | errors.push({
240 | rule: "10a",
241 | severity: "warning",
242 | lineNumber: ags[group].lineNumber,
243 | group: group,
244 | field: heading.name,
245 | message: `The heading ${heading.name} is non-standard and not found in AGS version ${dictVersion} dictionary`,
246 | });
247 | } else {
248 | errors.push({
249 | rule: "10a",
250 | severity: "error",
251 | lineNumber: ags[group].lineNumber,
252 | group: group,
253 | field: heading.name,
254 | message: `${heading.name} is not found in AGS version ${dictVersion} dictionary, and is not included in the DICT group`,
255 | });
256 | }
257 | });
258 | }
259 |
260 | return errors;
261 | },
262 | };
263 |
--------------------------------------------------------------------------------
/ags/src/rules/rulesForParsedAgs/index.ts:
--------------------------------------------------------------------------------
1 | import { rule19, rule19a, rule19b, rule7 } from "./checkGroupAndHeadings";
2 | import { rule10a, rule10b, rule10c, rule9 } from "./checkHeadingsWithDict";
3 | import {
4 | rule11,
5 | rule13,
6 | rule14,
7 | rule15,
8 | rule16,
9 | rule17,
10 | } from "./checkRequiredGroups";
11 | import { rule8 } from "./checkDataTypes";
12 |
13 | export const rulesForParsedAgs = {
14 | rule7,
15 | rule8,
16 | rule19,
17 | rule19a,
18 | rule19b,
19 | };
20 |
21 | export const rulesForParsedAgsWithDict = {
22 | rule10a,
23 | rule10b,
24 | rule10c,
25 | rule9,
26 | rule11,
27 | rule13,
28 | rule14,
29 | rule15,
30 | rule16,
31 | rule17,
32 | };
33 |
--------------------------------------------------------------------------------
/ags/src/rules/rulesForParsedAgs/types.d.ts:
--------------------------------------------------------------------------------
1 | import { AgsDictionaryVersion, AgsError, AgsRaw } from "../../types";
2 | import { AgsValidationStep } from "../types";
3 |
4 | export type AgsValidationStepParsed = AgsValidationStep;
5 |
6 | export type AgsValidationStepParsedWithDict = AgsValidationStep & {
7 | validate: (rawAgs: AgsRaw, dictVersion: AgsDictionaryVersion) => AgsError[];
8 | };
9 |
--------------------------------------------------------------------------------
/ags/src/rules/types.d.ts:
--------------------------------------------------------------------------------
1 | import { AgsRaw, AgsError, AgsDictionaryVersion } from "../types";
2 |
3 | // abstract version of the validation step
4 | export type AgsValidationStep = {
5 | rule: number | string;
6 | description: string;
7 | validate: (
8 | rawAgs: TInputType,
9 | dictVersion?: AgsDictionaryVersion,
10 | ) => AgsError[];
11 | };
12 |
--------------------------------------------------------------------------------
/ags/src/standardDictionaries.ts:
--------------------------------------------------------------------------------
1 | import v4_0_4 from "../assets/Standard_dictionary_v4_0_4.ags.json";
2 | import v4_1_1 from "../assets/Standard_dictionary_v4_1_1.ags.json";
3 | import v4_0_3 from "../assets/Standard_dictionary_v4_0_3.ags.json";
4 | import v4_1 from "../assets/Standard_dictionary_v4_1.ags.json";
5 | import { AgsDictionaryVersion, AgsDictionary, GroupRaw } from "./types";
6 |
7 | const v4_0_4_casted = v4_0_4 as unknown as AgsDictionary;
8 | const v4_1_1_casted = v4_1_1 as unknown as AgsDictionary;
9 | const v4_0_3_casted = v4_0_3 as unknown as AgsDictionary;
10 | const v4_1_casted = v4_1 as unknown as AgsDictionary;
11 |
12 | export const standardDictionaries: Record =
13 | {
14 | v4_0_4: v4_0_4_casted,
15 | v4_1_1: v4_1_1_casted,
16 | v4_0_3: v4_0_3_casted,
17 | v4_1: v4_1_casted,
18 | };
19 |
20 | export function getDictForVersion(dictVersion?: AgsDictionaryVersion) {
21 | if (!dictVersion) {
22 | throw new Error("Dictionary version is required for this rule");
23 | }
24 | return standardDictionaries[dictVersion];
25 | }
26 |
27 | function deepClone(obj: T): T {
28 | return JSON.parse(JSON.stringify(obj));
29 | }
30 |
31 | export function combineDicts(dict1: GroupRaw, dict2?: GroupRaw): GroupRaw {
32 | if (!dict2) {
33 | return deepClone(dict1);
34 | }
35 |
36 | const combinedDict = deepClone(dict1); // Deep clone of dict1
37 |
38 | dict2.rows.forEach((row) => {
39 | const existingRow = combinedDict.rows.find(
40 | (r) =>
41 | r.data.DICT_GRP === row.data.DICT_GRP &&
42 | r.data.DICT_HDNG === row.data.DICT_HDNG,
43 | );
44 |
45 | if (existingRow) {
46 | const index = combinedDict.rows.indexOf(existingRow);
47 | combinedDict.rows[index] = row;
48 | } else {
49 | combinedDict.rows.push(row);
50 | }
51 | });
52 |
53 | return combinedDict;
54 | }
55 |
--------------------------------------------------------------------------------
/ags/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface HeadingRaw {
2 | name: string;
3 | type: string;
4 | unit: string;
5 | }
6 |
7 | // Utility type to extract names from headings
8 | type HeadingNames = T[number]["name"];
9 |
10 | export interface RowRaw {
11 | // Constrain keys of data to the names from the headings
12 | data: Record, string>;
13 | lineNumber: number;
14 | }
15 |
16 | export interface GroupRaw {
17 | name: string;
18 | headings: HeadingRaw[];
19 | rows: RowRaw[];
20 | lineNumber: number;
21 | }
22 |
23 | export interface AgsRaw {
24 | [key: string]: GroupRaw;
25 | }
26 |
27 | export interface AgsDictionary extends AgsRaw {
28 | TYPE: GroupRaw;
29 | UNIT: GroupRaw;
30 | ABBR: GroupRaw;
31 | DICT: GroupRaw;
32 | PROJ: GroupRaw;
33 | TRAN: GroupRaw;
34 | }
35 |
36 | // Define the structure for error messages
37 | export type AgsError = {
38 | rule: number | string;
39 | severity: "error" | "warning";
40 | lineNumber: number;
41 | group?: string;
42 | field?: string;
43 | message?: string;
44 | };
45 |
46 | export type AgsDictionaryVersion = "v4_0_3" | "v4_1_1" | "v4_1" | "v4_0_4";
47 |
--------------------------------------------------------------------------------
/ags/src/validate.ts:
--------------------------------------------------------------------------------
1 | import { AgsDictionaryVersion, AgsError, AgsRaw } from "./types";
2 | import { parseAgs } from "./parse";
3 | import {
4 | rulesForRawString,
5 | rulesForParsedAgs,
6 | rulesForParsedAgsWithDict,
7 | } from "./rules";
8 |
9 | type RuleName =
10 | | keyof typeof rulesForRawString
11 | | keyof typeof rulesForParsedAgs
12 | | keyof typeof rulesForParsedAgsWithDict;
13 |
14 | export type RulesConfig = {
15 | [key in RuleName]: boolean;
16 | };
17 |
18 | // happy to overwrite ts here
19 |
20 | export const defaultRulesConfig = Object.fromEntries(
21 | [
22 | ...Object.keys(rulesForParsedAgs),
23 | ...Object.keys(rulesForParsedAgsWithDict),
24 | ...Object.keys(rulesForRawString),
25 | ].map((rule) => [rule, true]),
26 | ) as RulesConfig;
27 |
28 | function validateAgsDataRaw(rawAgs: string, config?: RulesConfig): AgsError[] {
29 | let allErrors: AgsError[] = [];
30 |
31 | const rulesConfig = config || defaultRulesConfig;
32 |
33 | Object.entries(rulesForRawString).forEach(([key, value]) => {
34 | if (!rulesConfig[key as RuleName]) {
35 | return;
36 | }
37 |
38 | const errors = value.validate(rawAgs);
39 | allErrors = [...allErrors, ...errors];
40 | });
41 | return allErrors;
42 | }
43 |
44 | export function validateAgsDataParsed(
45 | rawAgs: AgsRaw,
46 | config?: RulesConfig,
47 | ): AgsError[] {
48 | const rulesConfig = config || defaultRulesConfig;
49 | let allErrors: AgsError[] = [];
50 |
51 | Object.entries(rulesForParsedAgs).forEach(([key, value]) => {
52 | if (!rulesConfig[key as RuleName]) {
53 | return;
54 | }
55 |
56 | const errors = value.validate(rawAgs);
57 | allErrors = [...allErrors, ...errors];
58 | });
59 |
60 | return allErrors;
61 | }
62 |
63 | export function validateAgsDataParsedWithDict(
64 | rawAgs: AgsRaw,
65 | dictionary: AgsDictionaryVersion,
66 | config?: RulesConfig,
67 | ): AgsError[] {
68 | let allErrors: AgsError[] = [];
69 | const rulesConfig = config || defaultRulesConfig;
70 |
71 | Object.entries(rulesForParsedAgsWithDict).forEach(([key, value]) => {
72 | if (!rulesConfig[key as RuleName]) {
73 | return;
74 | }
75 |
76 | const errors = value.validate(rawAgs, dictionary);
77 | allErrors = [...allErrors, ...errors];
78 | });
79 |
80 | return allErrors;
81 | }
82 |
83 | // Function to validate raw AGS data using all validation steps
84 | export function validateAgsData(
85 | rawAgs: string,
86 | dictionary: AgsDictionaryVersion = "v4_0_4",
87 | config?: RulesConfig,
88 | ): {
89 | errors: AgsError[];
90 | parsedAgs?: AgsRaw | undefined;
91 | } {
92 | const agsErrorsForRaw = validateAgsDataRaw(rawAgs, config);
93 | if (
94 | agsErrorsForRaw.filter((error) => error.severity === "error").length > 0
95 | ) {
96 | return {
97 | errors: agsErrorsForRaw,
98 | parsedAgs: undefined,
99 | };
100 | }
101 |
102 | // now the AGS data should be safe to parse into the AgsRaw object
103 | let parsedAgs: AgsRaw;
104 | try {
105 | parsedAgs = parseAgs(rawAgs);
106 | } catch {
107 | return {
108 | errors: [
109 | {
110 | rule: "",
111 | lineNumber: 1,
112 | field: "",
113 | message: "Unknown error parsing AGS data.",
114 | severity: "error",
115 | },
116 | ],
117 | parsedAgs: undefined,
118 | };
119 | }
120 |
121 | const agsErrorsForParsed = validateAgsDataParsed(parsedAgs, config);
122 |
123 | const agsErrorsForParsedWithDict = validateAgsDataParsedWithDict(
124 | parsedAgs,
125 | dictionary,
126 | config,
127 | );
128 |
129 | return {
130 | errors: [
131 | ...agsErrorsForRaw,
132 | ...agsErrorsForParsed,
133 | ...agsErrorsForParsedWithDict,
134 | ],
135 | parsedAgs: parsedAgs,
136 | };
137 | }
138 |
--------------------------------------------------------------------------------
/ags/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "lib": ["ES2018", "DOM"],
7 | "declaration": true,
8 | "declarationMap": true,
9 | "sourceMap": true,
10 | "outDir": "./dist",
11 | "rootDir": "src",
12 | "strict": true,
13 | "esModuleInterop": true,
14 | "skipLibCheck": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "resolveJsonModule": true
17 | },
18 | "include": ["src/**/*", "assets/**/*.json"],
19 | "exclude": ["node_modules", "dist", "**/*.test.ts"]
20 | }
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ags-validator",
3 | "workspaces" : [
4 | "ags", "ags-validator-app"
5 | ],
6 | "version": "0.0.1",
7 | "main": "index.js",
8 | "scripts": {
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "author": "",
12 | "license": "",
13 | "description": ""
14 |
15 | }
16 |
--------------------------------------------------------------------------------