{"id":16198761,"url":"https://github.com/jamescdavis/condition-editor","last_synced_at":"2026-01-20T19:32:44.786Z","repository":{"id":43159483,"uuid":"231685931","full_name":"jamescdavis/condition-editor","owner":"jamescdavis","description":"A condition editor.","archived":false,"fork":false,"pushed_at":"2022-12-11T19:16:25.000Z","size":8496,"stargazers_count":2,"open_issues_count":26,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-07T19:24:43.379Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://jamescdavis.github.io/condition-editor/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jamescdavis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-01-04T00:18:00.000Z","updated_at":"2024-05-03T11:33:21.000Z","dependencies_parsed_at":"2023-01-27T05:16:19.956Z","dependency_job_id":null,"html_url":"https://github.com/jamescdavis/condition-editor","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jamescdavis/condition-editor","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jamescdavis%2Fcondition-editor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jamescdavis%2Fcondition-editor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jamescdavis%2Fcondition-editor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jamescdavis%2Fcondition-editor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jamescdavis","download_url":"https://codeload.github.com/jamescdavis/condition-editor/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jamescdavis%2Fcondition-editor/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28610624,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-20T18:56:40.769Z","status":"ssl_error","status_checked_at":"2026-01-20T18:54:26.653Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-10T09:14:49.988Z","updated_at":"2026-01-20T19:32:44.759Z","avatar_url":"https://github.com/jamescdavis.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Condition Editor\n\nThe Condition Editor is deployed at https://jamescdavis.github.io/condition-editor/\n\n## Inroduction\n\nWelcome to my implementation of a Condition Editor UI. The goal of this app is to allow the user to filter a set of products by building a condition. The user first selects a property to filter on, then an operator (the list of which is dependent on the property type), followed by textual input for those operators that require it (or a select box for enumerated properties). Upon building a complete condition, the product list should refresh and be filtered by the user's condition. The deployed app uses [mirage.js](https://miragejs.com) to simulate an actual API (lag included), but is really just running completely in your browser.\n\n## Technologies Used\n\nFor this application, I chose to implement using the brand new Octane edition of Ember.js. I implemented all application code using TypeScript via [ember-cli-typescript](https://ember-cli-typescript.com/) and avoided any use of `any` (aside from the `any` operator defined in this exercise). In additon, I used [mirage.js](https://miragejs.com/)/[ember-cli-mirage](https://www.ember-cli-mirage.com/), [ember-power-select](https://ember-power-select.com/), [ember-concurrency](http://ember-concurrency.com/), [ember-changeset](https://github.com/poteto/ember-changeset), and [ember-css-modules](https://github.com/salsify/ember-css-modules) and a number of other addons and development tools.\n\n## Total Development Time\n\nApproximately 15-20 hours\n\n## Assumptions\n\nI really only made one assumption, regarding UI behavior. It was unclear whether the user input should clear when changing properties and/or operators. In many cases it *has* to clear because it is invalid for the input type (e.g. random string for an enumerated property) so I implemented it to clear when either the property or operator changes.\n\n## Development Process / Code Tour\n\nBefore I began writing any code, I spent some time thinking about the different types of data and how they related to each other and came up with a plan of attack. I decided to start on the models first, and build the app up from there.\n\n### Setup\n\nThe first tasks were setup. I ran `ember new` to create a shiny new Ember application. I then immediately pinned `node` and `yarn` with `volta`. Following this I installed `ember-cli-typescript` and configured `eslint` for TypeScript, based on `airbnb-base`. I knew I'd be using `ember-cli-mirage` for development and testing (and, in this case, production!), so I went ahead and installed it.\n\nTo get the provided `datastore.js` loaded into the app, I placed it in `vendor/` and added an `app.import` to my [`ember-cli-build.js`](https://github.com/jamescdavis/condition-editor/blob/master/ember-cli-build.js) to add it to my vendor bundle. Since I was using TypeScript, I created [type declarations](https://github.com/jamescdavis/condition-editor/blob/master/types/condition-editor/index.d.ts) for `window.datastore`. This would allow me to access it in a type-safe way. At this point, I also converted the remaining `.js` files in `app/` from `ember new` to TypeScript so I'd have mostly pure TypeScript.\n\nI wanted to have an easy way to run tests during development, so I configured a [GitHub Action workflow for CI](https://github.com/jamescdavis/condition-editor/blob/master/.github/workflows/ci.yml). It is triggered on evry push and [runs linters and tests](https://github.com/jamescdavis/condition-editor/actions).I finished out prerequisite setup by installing/adding various type declarations I'd need such as `jsonapi-typescript` and types for `ember-cli-mirage` as well as setting up an empty mirage config.\n\n### Building Models\n\nIt was now time to start creating `ember-data` models. As I was using the most recent stable version of `ember-data` that uses separate package imports, I quickly remembered that the type definitions haven't quite caught up, so I created basic definitions for [`@ember-data/model`](https://github.com/jamescdavis/condition-editor/blob/master/types/%40ember-data/model.d.ts).\n\nThe first model I created was [`property`](https://github.com/jamescdavis/condition-editor/blob/master/app/models/property.ts). With no relationships of it's own, this would be the simplest of three models I had determined I needed. As I was now writing application code, I began to create a [basic unit test](https://github.com/jamescdavis/condition-editor/blob/master/tests/unit/models/property-test.ts). I find TypeScript `enum`s invaluable, so I created one to represent property types (`string`, `number`, `enumerated`). By creating a string-based `enum`, I would have a nice, type-safe, way to refer to property types without having to haul strings around.\n\nNext I created the [`product`](https://github.com/jamescdavis/condition-editor/blob/master/app/models/product.ts) model. This model really just relates a set of `property-values` and contains no data on its own aside from the `id`. I stubbed out a [unit test](https://github.com/jamescdavis/condition-editor/blob/master/tests/unit/models/product-test.ts) for this model as well.\n\nThe final model I created was [`property-value`](https://github.com/jamescdavis/condition-editor/blob/master/app/models/property-value.ts). This model relates `product` and `property` and holds the product's value for the property. The [unit test](https://github.com/jamescdavis/condition-editor/blob/master/tests/unit/models/property-value-test.ts) I created for this model might be a bit superflous as it just checks that a `property-value` can hold a value.\n\nThe last type of thing that I'd need to represent is `operator`. As the set of operators are static, I chose to create a lib that exports an array of [`Operator`](https://github.com/jamescdavis/condition-editor/blob/master/app/lib/operator.ts) objects. Each `Operator` started out with just its id and a compare function. I defined the operator ids as an `enum` and also created a utility function for easily pulling an operator out of the list by id. The human-readable names of the operators live in `window.datastore`. To load them, I created an Ember [instance initializer](https://github.com/jamescdavis/condition-editor/blob/master/app/instance-initializers/load-operator-names.ts) that reads from `window.datastore.getOperators()` and loads the names into the operator lib in memory. I followed up by creating unit tests for both the [operator lib](https://github.com/jamescdavis/condition-editor/blob/master/tests/unit/lib/operator-test.ts) and the [instance intializer](https://github.com/jamescdavis/condition-editor/blob/master/tests/unit/instance-initializers/load-operator-names-test.ts). For the final part of this, I added a mapping of property types to operator to the [`property`](https://github.com/jamescdavis/condition-editor/blob/master/app/models/property.ts) model.\n\n### Mirage\n\nNow that I had created ways to represent data, I wanted to simulate an actual API rather than just loading director from the datastore. Mirage.js (formerly inseperable from `ember-cli-mirage`) was the answer. For development, I ceated a mirage [default scenario](https://github.com/jamescdavis/condition-editor/blob/master/mirage/scenarios/default.ts) that loads from the datastore and populates the mirage in-memory database. I knew I would want to do the same in an acceptance test later, so I put the code for this importable [utility functions](https://github.com/jamescdavis/condition-editor/blob/master/mirage/util.ts). This gets data *into* mirage, but to get it out, I needed to [configure endpoints](https://github.com/jamescdavis/condition-editor/blob/master/mirage/config.ts).\n\nThe `properties` endpoint required no special configuration as it is just for fetching the list of available properties. The `products` endpoint, however, was a bit more complex as it needed to handle filtering based on `property`, `operator`, and, for most operators, a `filter` input value. I decided these would be passed as query params when fetching products. In order to filter, I started with the full list of products in the mirage database and, for each, determined the `property-value` for the specified `property`. I then used the provided operator id to retrieve the specified operator and used its `compare` function to test the `property-value` against the provided `filter`. In order to safely cast the operator query param string to an operator id enum, I created a custom typeguard, `isOpId`. I now had a virtual \"back end\" on which to build the user interface.\n\n### Controllers and Components\n\nI planned to use `ember-concurrency` to handle async UI tasks, so I installed it along with `ember-concurrency-decorators` and a set of type declarations. As this is an Ember Octane app, I wanted to use `tracked` so I created a [stub type definition](https://github.com/jamescdavis/condition-editor/blob/master/types/%40glimmer/tracking.d.ts) for it. This app would only have a single route, so I just decided to use the application route. I created a [controller](https://github.com/jamescdavis/condition-editor/blob/master/app/controllers/application.ts) for the this route that uses an `ember-concurrency` task to load all the `properties` on `init`. I also created an accompanying [unit test](https://github.com/jamescdavis/condition-editor/blob/master/tests/unit/controllers/application-test.ts).\n\nNow it was time to create some components. But first, I needed to install and config a few things. This included installing and/or configuring:\n- `ember-cli-template-lint`\n- `stylelint`\n- `ember-css-modolues`\n- `@ember/render-modifiers`\n\n I decided to start with the product table first and so created a [component](https://github.com/jamescdavis/condition-editor/tree/master/app/components/product-table) for it. I quickly realized that I needed to add a few more type declarations for `@ember-data/` and so created them. To be able to access a product's property values easily by property in the component template, I added a computed property to the [`product`](https://github.com/jamescdavis/condition-editor/blob/master/app/models/product.ts) model named `propValsByPropId` that remaps `product.propertyValues` into an object for easy access. The computed property's caching prevents this from needed to be recalculated every time its accessed. The `ProductTable` component takes `@property`, `@operator`, and `@filter` as arguments and passes these along when loading products. It uses the `did-insert` render modifier to kick off the first load and `did-update` watching both `@operator` and `@filter` because some operators don't require a filter. The result is then rendered as a styled html table. I also created the first [component integration test](https://github.com/jamescdavis/condition-editor/blob/master/tests/integration/components/product-table-test.ts) for this project.\n\nThe next component I tackled was the property chooser, which is the the first step in the condition editor. For this, I decided to use `ember-power-select`. This component is fairly simple. It takes a list of properties, the current property, and an action to perform when a property is chosen. It was so simple, in fact, that it could be a [template-only component](https://github.com/jamescdavis/condition-editor/blob/master/app/components/property-chooser.hbs). The [integration test](https://github.com/jamescdavis/condition-editor/blob/master/tests/integration/components/property-chooser-test.ts), too is fairly simple.\n\nThe [`OperatorChooser`](https://github.com/jamescdavis/condition-editor/tree/master/app/components/operator-chooser) was the next component I created. This component is very similar to `PropertyChooser` (it also uses `ember-power-select`), but it also needs to look up the valid operators for a given property type. It takes `@property` as an argument and uses a native getter called `opsForProp` to retrieve the operator list for that property, which it then passes to `PowerSelect`. This component also takes the current `@operator` and an `@onChange` action. Its [integration test](https://github.com/jamescdavis/condition-editor/blob/master/tests/integration/components/operator-chooser-test.ts) is a bit more complex, but not terribly so. \n\nThe final piece of the puzzle is a component I've called `ConditionEditor`. [This component](https://github.com/jamescdavis/condition-editor/tree/master/app/components/condition-editor) brings together `PropertyChooser` and `OperatorChooser`, adds user input for the filter, and passes the built condition to `ProductTable`. It does this by invoking the choosers, passing the appropiate data and actions, conditionally invoking the correct kind of input based on property and operator, and then yielding `property`, `operator`, and `input`. This allows you invoke it as a block component and pass these values directly into the yielded block. This pattern is a little bit of a departure from Ember's traditional \"data-down, actions up\", but I find it useful in situations such as this where one component is essentially managing the state of another. A pure DDAU approach would pass in to `ConditionEditor` actions to modify `property`, `operator`, and `input` and these values would be stored on the parent that invokes both `ConditionEditor` and `ProductTable` (or passed down to a component that invokes one or both of them).\n\nFor user input, I like to use `ember-changeset` rather than modify the underlying property directly. This has a number of benefits. I've used it here as part of debouncing (the underlying property update that triggers product re-load is debounced). It is also useful for validation before updating, which I added later. The changeset is built on `did-insert` and `re-built` when `operator` changes (and `operator` is reset when `property` changes). To help determine the correct kind of input to render, I added some properties to operators and properties such as `operator.needsInput`, `property.isEnumerated`, and `operator.multi`. The `isEnumerated` property is calculated, so I added unit tests for it. The [integration test](https://github.com/jamescdavis/condition-editor/blob/master/tests/integration/components/condition-editor-test.ts) for `ConditionEditor` is quite extensive and originally contained a great deal of repetition. I created [test helpers](https://github.com/jamescdavis/condition-editor/blob/master/tests/helpers/select.ts) for selecting properties and operators, since it something I do a *lot* in that test and knew I would need for the acceptance test. I also created [some helpers](https://github.com/jamescdavis/condition-editor/blob/master/tests/helpers/input-type.ts) for testing what type of input was rendered.\n\n### Putting it all Together\n\nFinally, I was at the point of putting it all together this simply involved invoking `ConditionEditor` from the [application route template](https://github.com/jamescdavis/condition-editor/blob/master/app/templates/application.hbs) and giving it a block invoking `ProductTable` with its yielded values. At this point I had a fully functional app that met the requirements. I added an [acceptance test](https://github.com/jamescdavis/condition-editor/blob/master/tests/acceptance/application-test.ts) that uses the provided datastore via the mirage utilities I created earlier. I discovered this test ran quite slow due to havig to wait for debounced input, so I disabled debouncing in the test environment and the whole test suite now finishes in about 45 seconds.\n\n I decided, since I was already using `ember-changeset`, to add user input validation. I installed `ember-changeset-validations` for this and added some basic type declarations for it. I'm glad I added validation because it helped me discover a bug in comparing floats and integer values. I initially added validations for `string.in`, `number.equal`, `number.greater_than`, `number.less_than`, and `string.in`. I ultimately dropped the \"non-in\" validations in favor of using `type=\"number\"` for the input, thus preventing invalid data entry on most modern browsers (and giving you a numpad on mobile!). I updated the `ConditionEditor` integration test and the acceptance test to test for invalid input.\n\n### 🚢  Ship It! 🚢\n\nI wanted to deploy to a live site, so I installed and configured `ember-cli-deploy` and `ember-cli-deploy-git` to easily build and publish to GitHub pages. I enabled mirage in production so I would have data on my live site. Now with a `ember deploy production` we're live!\n\n### Make it Shiny\n\nThere's something about putting your work out there that makes you want go back and polish it more. I spent some time tweaking CSS and making it more mobile-friendly (totally usable, IMHO). Playing with my app also helped my to find some annoyances with validation (as much as I love RegExes, sometimes, they just *aren't* the answer). I ended up ripping out the `validateFormat` validators and replacing them with custom list validators that seem to work pretty well and are more true to what my fake back-end can acually handle. The final sign-off was to enable `noEmitOnError` in TypeScript's compiler settings to break the build on type errors (I mean, I had it on the whole time ;). If this were a real app being worked on by a team, I'd definitely keep this enabled from the outset.\n\n\n----\n\n# Running, Developing, Collaborating on this App\n\nThis section of the README outlines the details of collaborating on this Ember application.\n\n## Prerequisites\n\nYou will need the following things properly installed on your computer.\n\n* [Git](https://git-scm.com/)\n* [Node.js](https://nodejs.org/)\n* [Yarn](https://yarnpkg.com/)\n* [Ember CLI](https://ember-cli.com/)\n* [Google Chrome](https://google.com/chrome/)\n\n## Installation\n\n* `git clone git@github.com:jamescdavis/condition-editor.git`\n* `cd condition-editor`\n* `yarn install`\n\n## Running / Development\n\n* `ember serve`\n* Visit your app at [http://localhost:4200](http://localhost:4200).\n* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests).\n\n### Code Generators\n\nMake use of the many generators for code, try `ember help generate` for more details\n\n### Running Tests\n\n* `ember test`\n* `ember test --server`\n\n### Linting\n\n* `yarn lint:hbs`\n* `yarn lint:js`\n* `yarn lint:js --fix`\n\n### Building\n\n* `ember build` (development)\n* `ember build --environment production` (production)\n\n### Deploying\n\n`ember deploy production`\n\n## Further Reading / Useful Links\n\n* [ember.js](https://emberjs.com/)\n* [ember-cli](https://ember-cli.com/)\n* Development Browser Extensions\n  * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)\n  * [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjamescdavis%2Fcondition-editor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjamescdavis%2Fcondition-editor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjamescdavis%2Fcondition-editor/lists"}