├── .eslintignore ├── public ├── favicon.ico ├── favicon-dark.svg ├── favicon-light.svg ├── icons │ └── github.svg └── data │ └── tasks.json ├── jest.preset.js ├── src ├── app │ ├── pages │ │ ├── view │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ ├── title-row.component.ts │ │ │ │ ├── debounced-input.directive.ts │ │ │ │ ├── selection-column.component.ts │ │ │ │ └── table-filter.component.ts │ │ │ ├── types.ts │ │ │ ├── view.component.ts │ │ │ └── view.component.html │ │ ├── filters │ │ │ ├── components │ │ │ │ ├── debounced-input.directive.ts │ │ │ │ └── table-filter.component.ts │ │ │ ├── makeData.ts │ │ │ ├── filters.component.ts │ │ │ └── filters.component.html │ │ ├── row-selection │ │ │ ├── components │ │ │ │ ├── selection-column.component.ts │ │ │ │ └── filter.component.ts │ │ │ ├── makeData.ts │ │ │ ├── row-selection.component.html │ │ │ └── row-selection.component.ts │ │ ├── column-ordering │ │ │ ├── makeData.ts │ │ │ ├── column-ordering.component.html │ │ │ └── column-ordering.component.ts │ │ └── base │ │ │ ├── base.component.html │ │ │ └── base.component.ts │ ├── app.component.html │ ├── app.component.ts │ ├── app.routes.ts │ ├── app.config.ts │ └── components │ │ └── site-header.component.ts ├── main.ts ├── styles.css ├── test-setup.ts └── index.html ├── tsconfig.editor.json ├── .prettierignore ├── tsconfig.app.json ├── .editorconfig ├── tsconfig.spec.json ├── README.md ├── tailwind.config.js ├── jest.config.ts ├── LICENSE ├── .prettierrc ├── .gitignore ├── tsconfig.json ├── nx.json ├── .eslintrc.json ├── project.json └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radix-ng/datagrid/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /src/app/pages/view/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './title-row.component'; 2 | export * from './table-filter.component'; 3 | export * from './selection-column.component'; 4 | -------------------------------------------------------------------------------- /src/app/pages/view/types.ts: -------------------------------------------------------------------------------- 1 | export type Task = { 2 | id: string; 3 | title: string; 4 | status: string; 5 | label: string; 6 | priority: string; 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts"], 4 | "compilerOptions": {}, 5 | "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] 6 | } 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | .angular 4 | .husky 5 | .nx 6 | .vercel 7 | 8 | /node_modules 9 | /dist 10 | /coverage 11 | /.nx/cache 12 | 13 | /.nx/workspace-data -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch((err) => 6 | console.error(err) 7 | ); 8 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | * { 7 | @apply border-border; 8 | } 9 | 10 | body { 11 | @apply bg-background text-foreground; 12 | font-feature-settings: "rlig" 1, "calt" 1; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts"], 8 | "include": ["src/**/*.d.ts"], 9 | "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/test-setup.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment 2 | globalThis.ngJest = { 3 | testEnvironmentOptions: { 4 | errorOnUnknownElements: true, 5 | errorOnUnknownProperties: true, 6 | }, 7 | }; 8 | import 'jest-preset-angular/setup-jest'; 9 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 |
8 |
9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | max_line_length = 100 8 | indent_size = 4 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | 16 | [*.yml] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "module": "commonjs", 6 | "target": "es2016", 7 | "types": ["jest", "node"] 8 | }, 9 | "files": ["src/test-setup.ts"], 10 | "include": [ 11 | "jest.config.ts", 12 | "src/**/*.test.ts", 13 | "src/**/*.spec.ts", 14 | "src/**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Datagrid 2 | 3 | This is a shadcn table component based on TanStack table. 4 | 5 | 6 | ## Tech Stack 7 | 8 | - **Framework:** [Angular](https://angular.dev) 9 | - **Styling:** [Tailwind CSS](https://tailwindcss.com) 10 | - **UI Components:** [Angular shadcn/ui](https://shadcn-ng.com) 11 | - **Table package:** [TanStack/react-table](https://tanstack.com/table/latest) 12 | 13 | ## Features 14 | ...soon 15 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { SiteHeaderComponent } from './components/site-header.component'; 5 | 6 | @Component({ 7 | standalone: true, 8 | imports: [RouterModule, SiteHeaderComponent], 9 | selector: 'app-root', 10 | templateUrl: './app.component.html' 11 | }) 12 | export class AppComponent {} 13 | -------------------------------------------------------------------------------- /public/favicon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/favicon-light.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Datagrid 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Route } from '@angular/router'; 2 | 3 | import { BaseComponent } from './pages/base/base.component'; 4 | import { ColumnOrderingComponent } from './pages/column-ordering/column-ordering.component'; 5 | import { FiltersComponent } from './pages/filters/filters.component'; 6 | import { RowSelectionComponent } from './pages/row-selection/row-selection.component'; 7 | import { ViewComponent } from './pages/view/view.component'; 8 | 9 | export const appRoutes: Route[] = [ 10 | { path: '', component: ViewComponent }, 11 | { path: 'base', component: BaseComponent }, 12 | { path: 'view', component: ViewComponent }, 13 | { path: 'column-ordering', component: ColumnOrderingComponent }, 14 | { path: 'row-selection', component: RowSelectionComponent }, 15 | { path: 'filters', component: FiltersComponent } 16 | ]; 17 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind'); 2 | const { join } = require('path'); 3 | 4 | const { shadcnUIPlugin } = require('@radix-ng/shadcn/theme'); 5 | 6 | /** @type {import('tailwindcss').Config} */ 7 | module.exports = { 8 | content: [ 9 | join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'), 10 | ...createGlobPatternsForDependencies(__dirname), 11 | './node_modules/@radix-ng/shadcn/**/*.{mjs,js}', 12 | './node_modules/@radix-ng/shadcn/**/(button|label|checkbox)/*.{mjs,js}' 13 | ], 14 | theme: { 15 | container: { 16 | center: true, 17 | padding: '2rem', 18 | screens: { 19 | '2xl': '1400px' 20 | } 21 | }, 22 | extend: {} 23 | }, 24 | plugins: [shadcnUIPlugin()] 25 | }; 26 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'datagrid', 4 | preset: './jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | coverageDirectory: './coverage/datagrid', 7 | transform: { 8 | '^.+\\.(ts|mjs|js|html)$': [ 9 | 'jest-preset-angular', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | stringifyContentPathRegex: '\\.(html|svg)$', 13 | }, 14 | ], 15 | }, 16 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 17 | snapshotSerializers: [ 18 | 'jest-preset-angular/build/serializers/no-ng-attributes', 19 | 'jest-preset-angular/build/serializers/ng-snapshot', 20 | 'jest-preset-angular/build/serializers/html-comment', 21 | ], 22 | testMatch: [ 23 | '/src/**/__tests__/**/*.[jt]s?(x)', 24 | '/src/**/*(*.)@(spec|test).[jt]s?(x)', 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /src/app/pages/view/components/title-row.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { ShBadgeDirective } from '@radix-ng/shadcn/badge'; 4 | import { injectFlexRenderContext, type CellContext } from '@tanstack/angular-table'; 5 | 6 | import { Task } from '../types'; 7 | 8 | @Component({ 9 | standalone: true, 10 | imports: [ShBadgeDirective], 11 | template: ` 12 |
13 |
{{ label }}
14 | 15 | {{ context.getValue() }} 16 | 17 |
18 | ` 19 | }) 20 | export class ViewTitleRowComponent implements OnInit { 21 | context = injectFlexRenderContext>(); 22 | 23 | label = ''; 24 | 25 | ngOnInit() { 26 | this.label = (this.context.row.original as Task).label; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | 4 | import { 5 | ArrowUpDown, 6 | Check, 7 | ChevronDown, 8 | ChevronLeft, 9 | ChevronRight, 10 | ChevronsLeft, 11 | ChevronsRight, 12 | LucideAngularModule, 13 | Minus 14 | } from 'lucide-angular'; 15 | 16 | import { appRoutes } from './app.routes'; 17 | 18 | export const appConfig: ApplicationConfig = { 19 | providers: [ 20 | provideZoneChangeDetection({ eventCoalescing: true }), 21 | provideRouter(appRoutes), 22 | 23 | importProvidersFrom( 24 | LucideAngularModule.pick({ 25 | Check, 26 | Minus, 27 | ArrowUpDown, 28 | ChevronsLeft, 29 | ChevronLeft, 30 | ChevronDown, 31 | ChevronRight, 32 | ChevronsRight 33 | }) 34 | ) 35 | ] 36 | }; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Radix Angular 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/app/pages/view/components/debounced-input.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, inject, input, NgZone } from '@angular/core'; 2 | import { outputFromObservable, toObservable } from '@angular/core/rxjs-interop'; 3 | import { 4 | debounceTime, 5 | fromEvent, 6 | Observable, 7 | switchMap, 8 | type MonoTypeOperatorFunction 9 | } from 'rxjs'; 10 | 11 | export function runOutsideAngular(zone: NgZone): MonoTypeOperatorFunction { 12 | return (source) => 13 | new Observable((subscriber) => zone.runOutsideAngular(() => source.subscribe(subscriber))); 14 | } 15 | 16 | @Directive({ 17 | standalone: true, 18 | selector: 'input[debouncedInput]' 19 | }) 20 | export class DebouncedInputDirective { 21 | #ref = inject(ElementRef).nativeElement as HTMLInputElement; 22 | 23 | readonly debounce = input(500); 24 | readonly debounce$ = toObservable(this.debounce); 25 | 26 | readonly changeEvent = outputFromObservable( 27 | this.debounce$.pipe( 28 | switchMap((debounce) => { 29 | return fromEvent(this.#ref, 'change').pipe(debounceTime(debounce)); 30 | }) 31 | ) 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/pages/filters/components/debounced-input.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, inject, input, NgZone } from '@angular/core'; 2 | import { outputFromObservable, toObservable } from '@angular/core/rxjs-interop'; 3 | import { 4 | debounceTime, 5 | fromEvent, 6 | Observable, 7 | switchMap, 8 | type MonoTypeOperatorFunction 9 | } from 'rxjs'; 10 | 11 | export function runOutsideAngular(zone: NgZone): MonoTypeOperatorFunction { 12 | return (source) => 13 | new Observable((subscriber) => zone.runOutsideAngular(() => source.subscribe(subscriber))); 14 | } 15 | 16 | @Directive({ 17 | standalone: true, 18 | selector: 'input[debouncedInput]' 19 | }) 20 | export class DebouncedInputDirective { 21 | #ref = inject(ElementRef).nativeElement as HTMLInputElement; 22 | 23 | readonly debounce = input(500); 24 | readonly debounce$ = toObservable(this.debounce); 25 | 26 | readonly changeEvent = outputFromObservable( 27 | this.debounce$.pipe( 28 | switchMap((debounce) => { 29 | return fromEvent(this.#ref, 'change').pipe(debounceTime(debounce)); 30 | }) 31 | ) 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc.json", 3 | "singleQuote": true, 4 | "printWidth": 100, 5 | "useTabs": false, 6 | "tabWidth": 4, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "endOfLine": "lf", 10 | "htmlWhitespaceSensitivity": "ignore", 11 | "overrides": [ 12 | { 13 | "files": [".component.html", ".page.html"], 14 | "options": { 15 | "parser": "angular" 16 | } 17 | }, 18 | { 19 | "files": ["*.html"], 20 | "options": { 21 | "parser": "html", 22 | "singleQuote": false 23 | } 24 | } 25 | ], 26 | "plugins": ["@ianvs/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"], 27 | "importOrderParserPlugins": ["typescript", "decorators-legacy"], 28 | "importOrderTypeScriptVersion": "5.0.0", 29 | "importOrder": [ 30 | "", 31 | "", 32 | "^@angular/(.*)$", 33 | "^rxjs(.*)$", 34 | "", 35 | "", 36 | "", 37 | "^[./]", 38 | "" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | .firebase 3 | .vercel 4 | 5 | documentation.json 6 | dependency-graph.png 7 | 8 | # turbo 9 | .turbo 10 | packages/**/.turbo 11 | 12 | /.pnp 13 | .pnp.js 14 | 15 | # .nx 16 | .nx 17 | .angular 18 | .verdaccio 19 | /out/ 20 | 21 | # compiled output 22 | dist 23 | /build 24 | tmp 25 | /out-tsc 26 | 27 | # dependencies 28 | node_modules 29 | 30 | # IDEs and editors 31 | /.idea 32 | .project 33 | .classpath 34 | .c9/ 35 | *.launch 36 | .settings/ 37 | *.sublime-workspace 38 | 39 | # IDE - VSCode 40 | .vscode/* 41 | !.vscode/settings.json 42 | !.vscode/tasks.json 43 | !.vscode/launch.json 44 | !.vscode/extensions.json 45 | 46 | # misc 47 | /.sass-cache 48 | /connect.lock 49 | /coverage 50 | /libpeerconnection.log 51 | npm-debug.log 52 | yarn-error.log 53 | testem.log 54 | /typings 55 | .env 56 | .env.local 57 | .env.development.local 58 | .env.test.local 59 | .env.production.local 60 | .env.production 61 | 62 | # System Files 63 | .DS_Store 64 | Thumbs.db 65 | 66 | # Next.js 67 | .next 68 | 69 | # debug 70 | npm-debug.log* 71 | yarn-debug.log* 72 | yarn-error.log* 73 | .yarn-integrity 74 | .idea 75 | dist 76 | esm 77 | 78 | 79 | # NgDoc files 80 | /ng-doc 81 | -------------------------------------------------------------------------------- /src/app/pages/row-selection/components/selection-column.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | import { 4 | injectFlexRenderContext, 5 | type CellContext, 6 | type HeaderContext 7 | } from '@tanstack/angular-table'; 8 | 9 | @Component({ 10 | template: ` 11 | 17 | `, 18 | host: { 19 | class: 'px-1 block' 20 | }, 21 | standalone: true, 22 | changeDetection: ChangeDetectionStrategy.OnPush 23 | }) 24 | export class TableHeadSelectionComponent { 25 | context = injectFlexRenderContext>(); 26 | } 27 | 28 | @Component({ 29 | template: ` 30 | 35 | `, 36 | host: { 37 | class: 'px-1 block' 38 | }, 39 | standalone: true, 40 | changeDetection: ChangeDetectionStrategy.OnPush 41 | }) 42 | export class TableRowSelectionComponent { 43 | context = injectFlexRenderContext>(); 44 | } 45 | -------------------------------------------------------------------------------- /src/app/pages/filters/makeData.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | 3 | export type Person = { 4 | firstName: string; 5 | lastName: string; 6 | age: number; 7 | visits: number; 8 | progress: number; 9 | status: 'relationship' | 'complicated' | 'single'; 10 | subRows?: Person[]; 11 | }; 12 | 13 | const range = (len: number) => { 14 | const arr: number[] = []; 15 | for (let i = 0; i < len; i++) { 16 | arr.push(i); 17 | } 18 | return arr; 19 | }; 20 | 21 | const newPerson = (): Person => { 22 | return { 23 | firstName: faker.person.firstName(), 24 | lastName: faker.person.lastName(), 25 | age: faker.number.int(40), 26 | visits: faker.number.int(1000), 27 | progress: faker.number.int(100), 28 | status: faker.helpers.shuffle([ 29 | 'relationship', 30 | 'complicated', 31 | 'single' 32 | ])[0]! 33 | }; 34 | }; 35 | 36 | export function makeData(...lens: number[]) { 37 | const makeDataLevel = (depth = 0): Person[] => { 38 | const len = lens[depth]!; 39 | return range(len).map((d): Person => { 40 | return { 41 | ...newPerson(), 42 | subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined 43 | }; 44 | }); 45 | }; 46 | 47 | return makeDataLevel(); 48 | } 49 | -------------------------------------------------------------------------------- /src/app/pages/row-selection/makeData.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | 3 | export type Person = { 4 | firstName: string; 5 | lastName: string; 6 | age: number; 7 | visits: number; 8 | progress: number; 9 | status: 'relationship' | 'complicated' | 'single'; 10 | subRows?: Person[]; 11 | }; 12 | 13 | const range = (len: number) => { 14 | const arr: number[] = []; 15 | for (let i = 0; i < len; i++) { 16 | arr.push(i); 17 | } 18 | return arr; 19 | }; 20 | 21 | const newPerson = (): Person => { 22 | return { 23 | firstName: faker.person.firstName(), 24 | lastName: faker.person.lastName(), 25 | age: faker.number.int(40), 26 | visits: faker.number.int(1000), 27 | progress: faker.number.int(100), 28 | status: faker.helpers.shuffle([ 29 | 'relationship', 30 | 'complicated', 31 | 'single' 32 | ])[0]! 33 | }; 34 | }; 35 | 36 | export function makeData(...lens: number[]) { 37 | const makeDataLevel = (depth = 0): Person[] => { 38 | const len = lens[depth]!; 39 | return range(len).map((d): Person => { 40 | return { 41 | ...newPerson(), 42 | subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined 43 | }; 44 | }); 45 | }; 46 | 47 | return makeDataLevel(); 48 | } 49 | -------------------------------------------------------------------------------- /src/app/pages/column-ordering/makeData.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | 3 | export type Person = { 4 | firstName: string; 5 | lastName: string; 6 | age: number; 7 | visits: number; 8 | progress: number; 9 | status: 'relationship' | 'complicated' | 'single'; 10 | subRows?: Person[]; 11 | }; 12 | 13 | const range = (len: number) => { 14 | const arr: number[] = []; 15 | for (let i = 0; i < len; i++) { 16 | arr.push(i); 17 | } 18 | return arr; 19 | }; 20 | 21 | const newPerson = (): Person => { 22 | return { 23 | firstName: faker.person.firstName(), 24 | lastName: faker.person.lastName(), 25 | age: faker.number.int(40), 26 | visits: faker.number.int(1000), 27 | progress: faker.number.int(100), 28 | status: faker.helpers.shuffle([ 29 | 'relationship', 30 | 'complicated', 31 | 'single' 32 | ])[0]! 33 | }; 34 | }; 35 | 36 | export function makeData(...lens: number[]) { 37 | const makeDataLevel = (depth = 0): Person[] => { 38 | const len = lens[depth]!; 39 | return range(len).map((d): Person => { 40 | return { 41 | ...newPerson(), 42 | subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined 43 | }; 44 | }); 45 | }; 46 | 47 | return makeDataLevel(); 48 | } 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "sourceMap": true, 5 | "declaration": false, 6 | "moduleResolution": "node", 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "importHelpers": true, 10 | "target": "es2022", 11 | "module": "esnext", 12 | "lib": [ 13 | "es2020", 14 | "dom" 15 | ], 16 | "skipLibCheck": true, 17 | "skipDefaultLibCheck": true, 18 | "baseUrl": ".", 19 | "paths": {}, 20 | "useDefineForClassFields": false, 21 | "esModuleInterop": true, 22 | "resolveJsonModule": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "strict": true, 25 | "noImplicitOverride": true, 26 | "noPropertyAccessFromIndexSignature": true, 27 | "noImplicitReturns": true, 28 | "noFallthroughCasesInSwitch": true 29 | }, 30 | "files": [], 31 | "include": [], 32 | "references": [ 33 | { 34 | "path": "./tsconfig.editor.json" 35 | }, 36 | { 37 | "path": "./tsconfig.app.json" 38 | }, 39 | { 40 | "path": "./tsconfig.spec.json" 41 | } 42 | ], 43 | "compileOnSave": false, 44 | "exclude": [ 45 | "node_modules", 46 | "tmp" 47 | ], 48 | "angularCompilerOptions": { 49 | "enableI18nLegacyMessageIdFormat": false, 50 | "strictInjectionParameters": true, 51 | "strictInputAccessModifiers": true, 52 | "strictTemplates": true 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "namedInputs": { 4 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 5 | "production": [ 6 | "default", 7 | "!{projectRoot}/.eslintrc.json", 8 | "!{projectRoot}/eslint.config.js", 9 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 10 | "!{projectRoot}/tsconfig.spec.json", 11 | "!{projectRoot}/jest.config.[jt]s", 12 | "!{projectRoot}/src/test-setup.[jt]s", 13 | "!{projectRoot}/test-setup.[jt]s" 14 | ], 15 | "sharedGlobals": [] 16 | }, 17 | "targetDefaults": { 18 | "@angular-devkit/build-angular:application": { 19 | "cache": true, 20 | "dependsOn": ["^build"], 21 | "inputs": ["production", "^production"] 22 | }, 23 | "@nx/eslint:lint": { 24 | "cache": true, 25 | "inputs": [ 26 | "default", 27 | "{workspaceRoot}/.eslintrc.json", 28 | "{workspaceRoot}/.eslintignore", 29 | "{workspaceRoot}/eslint.config.js" 30 | ] 31 | }, 32 | "@nx/jest:jest": { 33 | "cache": true, 34 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"], 35 | "options": { 36 | "passWithNoTests": true 37 | }, 38 | "configurations": { 39 | "ci": { 40 | "ci": true, 41 | "codeCoverage": true 42 | } 43 | } 44 | } 45 | }, 46 | "generators": { 47 | "@nx/angular:application": { 48 | "e2eTestRunner": "none", 49 | "linter": "eslint", 50 | "style": "css", 51 | "unitTestRunner": "jest" 52 | } 53 | }, 54 | "defaultProject": "datagrid" 55 | } 56 | -------------------------------------------------------------------------------- /src/app/pages/view/components/selection-column.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { ShCheckboxComponent } from '@radix-ng/shadcn/checkbox'; 4 | import { 5 | injectFlexRenderContext, 6 | type CellContext, 7 | type HeaderContext 8 | } from '@tanstack/angular-table'; 9 | 10 | @Component({ 11 | template: ` 12 | 20 | `, 21 | host: { 22 | class: 'px-1 block' 23 | }, 24 | standalone: true, 25 | imports: [ShCheckboxComponent] 26 | }) 27 | export class ViewTableHeadSelectionComponent { 28 | context = injectFlexRenderContext>(); 29 | 30 | onCheckedChange(checked: boolean) { 31 | this.context.table.toggleAllRowsSelected(checked); 32 | } 33 | } 34 | 35 | @Component({ 36 | template: ` 37 | 42 | `, 43 | host: { 44 | class: 'px-1 block' 45 | }, 46 | standalone: true, 47 | imports: [ShCheckboxComponent] 48 | }) 49 | export class ViewTableRowSelectionComponent { 50 | context = injectFlexRenderContext>(); 51 | 52 | onCheckedChange(checked: boolean) { 53 | this.context.row.toggleSelected(checked); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": "*.json", 8 | "parser": "jsonc-eslint-parser", 9 | "rules": {} 10 | }, 11 | { 12 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 13 | "rules": { 14 | "@angular-eslint/no-input-rename": "off", 15 | "@nx/enforce-module-boundaries": [ 16 | "error", 17 | { 18 | "enforceBuildableLibDependency": true, 19 | "allow": [], 20 | "depConstraints": [ 21 | { 22 | "sourceTag": "*", 23 | "onlyDependOnLibsWithTags": ["*"] 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.ts", "*.tsx"], 32 | "extends": ["plugin:@nx/typescript"], 33 | "rules": { 34 | "@typescript-eslint/no-extra-semi": "error", 35 | "no-extra-semi": "off" 36 | } 37 | }, 38 | { 39 | "files": ["*.js", "*.jsx"], 40 | "extends": ["plugin:@nx/javascript"], 41 | "rules": { 42 | "@typescript-eslint/no-extra-semi": "error", 43 | "no-extra-semi": "off" 44 | } 45 | }, 46 | { 47 | "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"], 48 | "env": { 49 | "jest": true 50 | }, 51 | "rules": {} 52 | }, 53 | { 54 | "files": ["./package.json", "./generators.json"], 55 | "parser": "jsonc-eslint-parser", 56 | "rules": { 57 | "@nx/nx-plugin-checks": "error" 58 | } 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /src/app/pages/base/base.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { 6 | 7 | @for (header of headerGroup.headers; track header.id) { @if 8 | (!header.isPlaceholder) { 9 | 20 | } } 21 | 22 | } 23 | 24 | 25 | @for (row of table.getRowModel().rows; track row.id) { 26 | 27 | @for (cell of row.getVisibleCells(); track cell.id) { 28 | 39 | } 40 | 41 | } 42 | 43 | 44 | @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { 45 | 46 | @for (footer of footerGroup.headers; track footer.id) { 47 | 58 | } 59 | 60 | } 61 | 62 |
10 | 17 |
18 |
19 |
29 | 36 |
37 |
38 |
48 | 55 | {{ footer }} 56 | 57 |
63 |
64 |
65 | 66 |
67 | -------------------------------------------------------------------------------- /src/app/pages/row-selection/components/filter.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, input, OnInit } from '@angular/core'; 3 | 4 | import { Column, type Table } from '@tanstack/angular-table'; 5 | 6 | @Component({ 7 | selector: 'app-table-filter', 8 | template: ` 9 | @if (columnType) { 10 | @if (columnType == 'number') { 11 |
12 | 20 | 28 |
29 | } @else { 30 | 38 | } 39 | } 40 | `, 41 | standalone: true, 42 | imports: [CommonModule] 43 | }) 44 | export class FilterComponent implements OnInit { 45 | column = input.required>(); 46 | 47 | table = input.required>(); 48 | 49 | columnType!: string; 50 | 51 | ngOnInit() { 52 | this.columnType = typeof this.table() 53 | .getPreFilteredRowModel() 54 | .flatRows[0]?.getValue(this.column().id); 55 | } 56 | 57 | getMinValue() { 58 | const minValue = this.column().getFilterValue() as any; 59 | 60 | return (minValue?.[0] ?? '') as string; 61 | } 62 | 63 | getMaxValue() { 64 | const maxValue = this.column().getFilterValue() as any; 65 | return (maxValue?.[1] ?? '') as string; 66 | } 67 | 68 | updateMinFilterValue(newValue: string): void { 69 | this.column().setFilterValue((old: any) => { 70 | return [newValue, old?.[1]]; 71 | }); 72 | } 73 | 74 | updateMaxFilterValue(newValue: string): void { 75 | this.column().setFilterValue((old: any) => [old?.[0], newValue]); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datagrid", 3 | "$schema": "node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "prefix": "app", 6 | "sourceRoot": "./src", 7 | "tags": [], 8 | "targets": { 9 | "build": { 10 | "executor": "@angular-devkit/build-angular:application", 11 | "outputs": [ 12 | "{options.outputPath}" 13 | ], 14 | "options": { 15 | "outputPath": "dist/datagrid", 16 | "index": "./src/index.html", 17 | "browser": "./src/main.ts", 18 | "polyfills": [ 19 | "zone.js" 20 | ], 21 | "tsConfig": "tsconfig.app.json", 22 | "assets": [ 23 | { 24 | "glob": "**/*", 25 | "input": "public" 26 | } 27 | ], 28 | "styles": [ 29 | "./src/styles.css", 30 | "@angular/cdk/overlay-prebuilt.css" 31 | ], 32 | "scripts": [] 33 | }, 34 | "configurations": { 35 | "production": { 36 | "outputHashing": "all" 37 | }, 38 | "development": { 39 | "optimization": false, 40 | "extractLicenses": false, 41 | "sourceMap": true 42 | } 43 | }, 44 | "defaultConfiguration": "production" 45 | }, 46 | "serve": { 47 | "executor": "@angular-devkit/build-angular:dev-server", 48 | "configurations": { 49 | "production": { 50 | "buildTarget": "datagrid:build:production" 51 | }, 52 | "development": { 53 | "buildTarget": "datagrid:build:development" 54 | } 55 | }, 56 | "defaultConfiguration": "development" 57 | }, 58 | "extract-i18n": { 59 | "executor": "@angular-devkit/build-angular:extract-i18n", 60 | "options": { 61 | "buildTarget": "datagrid:build" 62 | } 63 | }, 64 | "lint": { 65 | "executor": "@nx/eslint:lint", 66 | "options": { 67 | "lintFilePatterns": [ 68 | "./src" 69 | ] 70 | } 71 | }, 72 | "test": { 73 | "executor": "@nx/jest:jest", 74 | "outputs": [ 75 | "{workspaceRoot}/coverage/{projectName}" 76 | ], 77 | "options": { 78 | "jestConfig": "jest.config.ts" 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@datagrid/source", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "nx serve", 7 | "build": "nx build", 8 | "test": "nx test", 9 | "----Others-------": "-----------------------------------------------------------------------", 10 | "format:check": "prettier --check src/**/** --cache", 11 | "format:write": "prettier --write src/**/** --cache" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "~18.0.0", 16 | "@angular/cdk": "~18.0.0", 17 | "@angular/common": "~18.0.0", 18 | "@angular/compiler": "~18.0.0", 19 | "@angular/core": "~18.0.0", 20 | "@angular/forms": "~18.0.0", 21 | "@angular/platform-browser": "~18.0.0", 22 | "@angular/platform-browser-dynamic": "~18.0.0", 23 | "@angular/router": "~18.0.0", 24 | "@faker-js/faker": "^8.4.1", 25 | "@nx/angular": "19.4.3", 26 | "@radix-ng/shadcn": "^0.6.2", 27 | "@radix-ng/primitives": "0.9.1", 28 | "@tanstack/angular-table": "^8.19.3", 29 | "class-variance-authority": "^0.7.0", 30 | "clsx": "^2.1.1", 31 | "lucide-angular": "^0.408.0", 32 | "rxjs": "~7.8.0", 33 | "tailwind-merge": "^2.4.0", 34 | "tslib": "^2.3.0", 35 | "zone.js": "~0.14.3" 36 | }, 37 | "devDependencies": { 38 | "@angular-devkit/build-angular": "~18.0.0", 39 | "@angular-devkit/core": "~18.0.0", 40 | "@angular-devkit/schematics": "~18.0.0", 41 | "@angular-eslint/eslint-plugin": "^18.0.1", 42 | "@angular-eslint/eslint-plugin-template": "^18.0.1", 43 | "@angular-eslint/template-parser": "^18.0.1", 44 | "@angular/cli": "~18.0.0", 45 | "@angular/compiler-cli": "~18.0.0", 46 | "@angular/language-service": "~18.0.0", 47 | "@ianvs/prettier-plugin-sort-imports": "^4.3.1", 48 | "@nx/eslint": "19.4.3", 49 | "@nx/eslint-plugin": "19.4.3", 50 | "@nx/jest": "19.4.3", 51 | "@nx/js": "19.4.3", 52 | "@nx/workspace": "19.4.3", 53 | "@schematics/angular": "~18.0.0", 54 | "@swc-node/register": "~1.9.1", 55 | "@swc/core": "~1.5.7", 56 | "@swc/helpers": "~0.5.11", 57 | "@tailwindcss/typography": "^0.5.13", 58 | "@types/jest": "^29.4.0", 59 | "@types/node": "18.16.9", 60 | "@typescript-eslint/eslint-plugin": "^7.3.0", 61 | "@typescript-eslint/parser": "^7.3.0", 62 | "@typescript-eslint/utils": "^8.0.0-alpha.28", 63 | "autoprefixer": "^10.4.0", 64 | "eslint": "~8.57.0", 65 | "eslint-config-prettier": "^9.0.0", 66 | "jest": "^29.4.1", 67 | "jest-environment-jsdom": "^29.4.1", 68 | "jest-preset-angular": "~14.1.0", 69 | "nx": "19.4.3", 70 | "postcss": "^8.4.5", 71 | "prettier": "^3.3.3", 72 | "prettier-plugin-tailwindcss": "^0.6.5", 73 | "tailwindcss": "^3.0.2", 74 | "ts-jest": "^29.1.0", 75 | "ts-node": "10.9.1", 76 | "typescript": "~5.4.2" 77 | }, 78 | "nx": { 79 | "includedScripts": [] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/app/pages/base/base.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, signal } from '@angular/core'; 2 | 3 | import { ShButtonDirective } from '@radix-ng/shadcn/button'; 4 | import { 5 | ShTableModule, 6 | TableBodyDirective, 7 | TableCellDirective, 8 | TableDirective, 9 | TableFooterDirective, 10 | TableHeadDirective, 11 | TableHeaderDirective, 12 | TableRowDirective 13 | } from '@radix-ng/shadcn/table'; 14 | import { 15 | ColumnDef, 16 | createAngularTable, 17 | FlexRenderDirective, 18 | getCoreRowModel 19 | } from '@tanstack/angular-table'; 20 | 21 | type Person = { 22 | firstName: string; 23 | lastName: string; 24 | age: number; 25 | visits: number; 26 | status: string; 27 | progress: number; 28 | }; 29 | 30 | const defaultData: Person[] = [ 31 | { 32 | firstName: 'tanner', 33 | lastName: 'linsley', 34 | age: 24, 35 | visits: 100, 36 | status: 'In Relationship', 37 | progress: 50 38 | }, 39 | { 40 | firstName: 'tandy', 41 | lastName: 'miller', 42 | age: 40, 43 | visits: 40, 44 | status: 'Single', 45 | progress: 80 46 | }, 47 | { 48 | firstName: 'joe', 49 | lastName: 'dirte', 50 | age: 45, 51 | visits: 20, 52 | status: 'Complicated', 53 | progress: 10 54 | } 55 | ]; 56 | 57 | const defaultColumns: ColumnDef[] = [ 58 | { 59 | accessorKey: 'firstName', 60 | cell: (info) => info.getValue(), 61 | footer: (info) => info.column.id 62 | }, 63 | { 64 | accessorFn: (row) => row.lastName, 65 | id: 'lastName', 66 | cell: (info) => `${info.getValue()}`, 67 | header: () => `Last Name`, 68 | footer: (info) => info.column.id 69 | }, 70 | { 71 | accessorKey: 'age', 72 | header: () => 'Age', 73 | footer: (info) => info.column.id 74 | }, 75 | { 76 | accessorKey: 'visits', 77 | header: () => `Visits`, 78 | footer: (info) => info.column.id 79 | }, 80 | { 81 | accessorKey: 'status', 82 | header: 'Status', 83 | footer: (info) => info.column.id 84 | }, 85 | { 86 | accessorKey: 'progress', 87 | header: 'Profile Progress', 88 | footer: (info) => info.column.id 89 | } 90 | ]; 91 | 92 | @Component({ 93 | selector: 'app-base', 94 | standalone: true, 95 | imports: [ 96 | FlexRenderDirective, 97 | TableDirective, 98 | TableHeaderDirective, 99 | ShButtonDirective, 100 | TableBodyDirective, 101 | TableFooterDirective, 102 | TableRowDirective, 103 | TableCellDirective, 104 | TableHeadDirective 105 | ], 106 | templateUrl: './base.component.html' 107 | }) 108 | export class BaseComponent { 109 | data = signal(defaultData); 110 | 111 | table = createAngularTable(() => ({ 112 | data: this.data(), 113 | columns: defaultColumns, 114 | columnResizeMode: 'onChange', 115 | columnResizeDirection: 'ltr', 116 | getCoreRowModel: getCoreRowModel(), 117 | debugTable: true 118 | })); 119 | 120 | rerender() { 121 | this.data.set([...defaultData.sort(() => -1)]); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/app/pages/column-ordering/column-ordering.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 12 |
13 | 14 | @for (column of table.getAllLeafColumns(); track column.id) { 15 |
16 | 24 |
25 | } 26 |
27 | 28 |
29 |
30 | 31 | 32 |
33 | 34 |
35 | 36 | 37 | @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { 38 | 39 | @for (header of headerGroup.headers; track header.id) { 40 | 53 | } 54 | 55 | } 56 | 57 | 58 | @for (row of table.getRowModel().rows; track row.id) { 59 | 60 | @for (cell of row.getVisibleCells(); track cell.id) { 61 | 72 | } 73 | 74 | } 75 | 76 | 77 | @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { 78 | 79 | @for (header of footerGroup.headers; track header.id) { 80 | 93 | } 94 | 95 | } 96 | 97 |
41 | @if (!header.isPlaceholder) { 42 | 49 | {{ header }} 50 | 51 | } 52 |
62 | 69 | {{ cell }} 70 | 71 |
81 | @if (!header.isPlaceholder) { 82 | 89 | {{ header }} 90 | 91 | } 92 |
98 |
99 | 100 |
101 |
{{ stringifiedColumnOrdering() }}
102 |
103 | -------------------------------------------------------------------------------- /src/app/pages/column-ordering/column-ordering.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core'; 2 | 3 | import { faker } from '@faker-js/faker'; 4 | import { 5 | TableBodyDirective, 6 | TableCellDirective, 7 | TableDirective, 8 | TableFooterDirective, 9 | TableHeadDirective, 10 | TableHeaderDirective, 11 | TableRowDirective 12 | } from '@radix-ng/shadcn/table'; 13 | import { 14 | ColumnDef, 15 | createAngularTable, 16 | FlexRenderDirective, 17 | getCoreRowModel, 18 | type ColumnOrderState, 19 | type VisibilityState 20 | } from '@tanstack/angular-table'; 21 | 22 | import { makeData, type Person } from './makeData'; 23 | 24 | const defaultColumns: ColumnDef[] = [ 25 | { 26 | header: 'Name', 27 | footer: (props) => props.column.id, 28 | columns: [ 29 | { 30 | accessorKey: 'firstName', 31 | cell: (info) => info.getValue(), 32 | footer: (props) => props.column.id 33 | }, 34 | { 35 | accessorFn: (row) => row.lastName, 36 | id: 'lastName', 37 | cell: (info) => info.getValue(), 38 | header: () => 'Last Name', 39 | footer: (props) => props.column.id 40 | } 41 | ] 42 | }, 43 | { 44 | header: 'Info', 45 | footer: (props) => props.column.id, 46 | columns: [ 47 | { 48 | accessorKey: 'age', 49 | header: () => 'Age', 50 | footer: (props) => props.column.id 51 | }, 52 | { 53 | header: 'More Info', 54 | columns: [ 55 | { 56 | accessorKey: 'visits', 57 | header: () => 'Visits', 58 | footer: (props) => props.column.id 59 | }, 60 | { 61 | accessorKey: 'status', 62 | header: 'Status', 63 | footer: (props) => props.column.id 64 | }, 65 | { 66 | accessorKey: 'progress', 67 | header: 'Profile Progress', 68 | footer: (props) => props.column.id 69 | } 70 | ] 71 | } 72 | ] 73 | } 74 | ]; 75 | 76 | @Component({ 77 | selector: 'app-column-ordering', 78 | standalone: true, 79 | imports: [ 80 | FlexRenderDirective, 81 | TableDirective, 82 | TableHeaderDirective, 83 | TableRowDirective, 84 | TableHeadDirective, 85 | TableBodyDirective, 86 | TableCellDirective, 87 | TableFooterDirective 88 | ], 89 | templateUrl: './column-ordering.component.html', 90 | changeDetection: ChangeDetectionStrategy.OnPush 91 | }) 92 | export class ColumnOrderingComponent { 93 | readonly data = signal(makeData(20)); 94 | readonly columnVisibility = signal({}); 95 | readonly columnOrder = signal([]); 96 | 97 | readonly table = createAngularTable(() => ({ 98 | data: this.data(), 99 | columns: defaultColumns, 100 | state: { 101 | columnOrder: this.columnOrder(), 102 | columnVisibility: this.columnVisibility() 103 | }, 104 | getCoreRowModel: getCoreRowModel(), 105 | onColumnVisibilityChange: (updaterOrValue) => { 106 | typeof updaterOrValue === 'function' 107 | ? this.columnVisibility.update(updaterOrValue) 108 | : this.columnVisibility.set(updaterOrValue); 109 | }, 110 | onColumnOrderChange: (updaterOrValue) => { 111 | typeof updaterOrValue === 'function' 112 | ? this.columnOrder.update(updaterOrValue) 113 | : this.columnOrder.set(updaterOrValue); 114 | }, 115 | debugTable: true, 116 | debugHeaders: true, 117 | debugColumns: true 118 | })); 119 | 120 | readonly stringifiedColumnOrdering = computed(() => { 121 | return JSON.stringify(this.table.getState().columnOrder); 122 | }); 123 | 124 | randomizeColumns() { 125 | this.table.setColumnOrder( 126 | faker.helpers.shuffle(this.table.getAllLeafColumns().map((d) => d.id)) 127 | ); 128 | } 129 | 130 | rerender() { 131 | this.data.set([...makeData(20)]); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/app/pages/filters/filters.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { ShButtonDirective } from '@radix-ng/shadcn/button'; 6 | import { ShInputDirective } from '@radix-ng/shadcn/input'; 7 | import { 8 | TableBodyDirective, 9 | TableCellDirective, 10 | TableDirective, 11 | TableFooterDirective, 12 | TableHeadDirective, 13 | TableHeaderDirective, 14 | TableRowDirective 15 | } from '@radix-ng/shadcn/table'; 16 | import { 17 | ColumnDef, 18 | createAngularTable, 19 | FlexRenderDirective, 20 | getCoreRowModel, 21 | getFacetedMinMaxValues, 22 | getFacetedRowModel, 23 | getFacetedUniqueValues, 24 | getFilteredRowModel, 25 | getPaginationRowModel, 26 | getSortedRowModel, 27 | type ColumnFiltersState 28 | } from '@tanstack/angular-table'; 29 | import { LucideAngularModule } from 'lucide-angular'; 30 | 31 | import { FilterComponent } from './components/table-filter.component'; 32 | import { makeData, type Person } from './makeData'; 33 | 34 | @Component({ 35 | selector: 'app-filters', 36 | standalone: true, 37 | imports: [ 38 | FilterComponent, 39 | FlexRenderDirective, 40 | FormsModule, 41 | NgClass, 42 | TableDirective, 43 | TableHeaderDirective, 44 | ShButtonDirective, 45 | TableBodyDirective, 46 | TableFooterDirective, 47 | TableRowDirective, 48 | TableCellDirective, 49 | TableHeadDirective, 50 | ShInputDirective, 51 | LucideAngularModule 52 | ], 53 | templateUrl: './filters.component.html', 54 | changeDetection: ChangeDetectionStrategy.OnPush 55 | }) 56 | export class FiltersComponent { 57 | readonly columnFilters = signal([]); 58 | readonly data = signal(makeData(5000)); 59 | 60 | readonly columns: ColumnDef[] = [ 61 | { 62 | accessorKey: 'firstName', 63 | cell: (info) => info.getValue() 64 | }, 65 | { 66 | accessorFn: (row) => row.lastName, 67 | id: 'lastName', 68 | cell: (info) => info.getValue(), 69 | header: () => 'Last Name' 70 | }, 71 | { 72 | accessorKey: 'age', 73 | header: () => 'Age', 74 | meta: { 75 | filterVariant: 'range' 76 | } 77 | }, 78 | { 79 | accessorKey: 'visits', 80 | header: () => 'Visits', 81 | meta: { 82 | filterVariant: 'range' 83 | } 84 | }, 85 | { 86 | accessorKey: 'status', 87 | header: 'Status', 88 | meta: { 89 | filterVariant: 'select' 90 | } 91 | }, 92 | { 93 | accessorKey: 'progress', 94 | header: 'Profile Progress', 95 | meta: { 96 | filterVariant: 'range' 97 | } 98 | } 99 | ]; 100 | 101 | table = createAngularTable(() => ({ 102 | columns: this.columns, 103 | data: this.data(), 104 | state: { 105 | columnFilters: this.columnFilters() 106 | }, 107 | onColumnFiltersChange: (updater) => { 108 | updater instanceof Function 109 | ? this.columnFilters.update(updater) 110 | : this.columnFilters.set(updater); 111 | }, 112 | getCoreRowModel: getCoreRowModel(), 113 | getFilteredRowModel: getFilteredRowModel(), //client-side filtering 114 | getSortedRowModel: getSortedRowModel(), 115 | getPaginationRowModel: getPaginationRowModel(), 116 | getFacetedRowModel: getFacetedRowModel(), // client-side faceting 117 | getFacetedUniqueValues: getFacetedUniqueValues(), // generate unique values for select filter/autocomplete 118 | getFacetedMinMaxValues: getFacetedMinMaxValues(), // generate min/max values for range filter 119 | debugTable: true, 120 | debugHeaders: true, 121 | debugColumns: false 122 | })); 123 | 124 | readonly stringifiedFilters = computed(() => JSON.stringify(this.columnFilters(), null, 2)); 125 | 126 | onPageInputChange(event: Event): void { 127 | const inputElement = event.target as HTMLInputElement; 128 | const page = inputElement.value ? Number(inputElement.value) - 1 : 0; 129 | this.table.setPageIndex(page); 130 | } 131 | 132 | onPageSizeChange(event: any): void { 133 | this.table.setPageSize(Number(event.target.value)); 134 | } 135 | 136 | refreshData(): void { 137 | this.data.set(makeData(100_000)); // stress test 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/app/pages/row-selection/row-selection.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 | 7 | @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { 8 | 9 | @for (header of headerGroup.headers; track header.id) { 10 | 28 | } 29 | 30 | } 31 | 32 | 33 | @for (row of table.getRowModel().rows; track row.id) { 34 | 35 | @for (cell of row.getVisibleCells(); track cell.id) { 36 | 47 | } 48 | 49 | } 50 | 51 | 52 | 53 | 61 | 64 | 65 | 66 |
11 | @if (!header.isPlaceholder) { 12 | 19 | {{ headerCell }} 20 | 21 | 22 | @if (header.column.getCanFilter()) { 23 |
24 | 25 |
26 | } } 27 |
37 | 44 | {{ renderCell }} 45 | 46 |
54 | 60 | 62 | Page Rows ({{ table.getRowModel().rows.length }}) 63 |
67 |
68 | 69 |
70 |
71 | 78 | 85 | 92 | 99 | 100 |
Page
101 | 102 | {{ table.getState().pagination.pageIndex + 1 }} of {{ table.getPageCount() }} 103 | 104 |
105 | 106 | | Go to page: 107 | 113 | 114 | 115 | 120 |
121 |
122 |
123 | {{ rowSelectionLength() }} of {{ table.getPreFilteredRowModel().rows.length }} Total Rows 124 |
125 |
126 |
127 |
128 | 129 |
130 |
131 | 134 |
135 |
136 | 137 |
{{ stringifiedRowSelection() }}
138 |
139 |
140 | 141 | 142 | Age 🥳 143 | 144 | -------------------------------------------------------------------------------- /src/app/pages/filters/components/table-filter.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, computed, input, OnInit } from '@angular/core'; 3 | 4 | import type { Column, RowData, Table } from '@tanstack/angular-table'; 5 | 6 | import { DebouncedInputDirective } from './debounced-input.directive'; 7 | 8 | declare module '@tanstack/angular-table' { 9 | //allows us to define custom properties for our columns 10 | interface ColumnMeta { 11 | filterVariant?: 'text' | 'range' | 'select'; 12 | } 13 | } 14 | 15 | @Component({ 16 | selector: 'app-table-filter', 17 | template: ` 18 | @if (filterVariant() === 'range') { 19 |
20 |
21 | 32 | 33 | 44 |
45 |
46 |
47 | } @else if (filterVariant() === 'select') { 48 | 59 | } @else { 60 | 61 | @for (value of sortedUniqueValues(); track value) { 62 | 65 | } 66 | 67 | 77 |
78 | } 79 | `, 80 | standalone: true, 81 | imports: [CommonModule, DebouncedInputDirective] 82 | }) 83 | export class FilterComponent { 84 | column = input.required>(); 85 | 86 | table = input.required>(); 87 | 88 | readonly filterVariant = computed(() => { 89 | return (this.column().columnDef.meta ?? {}).filterVariant; 90 | }); 91 | 92 | readonly columnFilterValue = computed(() => this.column().getFilterValue()); 93 | 94 | readonly minRangePlaceholder = computed(() => { 95 | return `Min ${ 96 | this.column().getFacetedMinMaxValues()?.[0] !== undefined 97 | ? `(${this.column().getFacetedMinMaxValues()?.[0]})` 98 | : '' 99 | }`; 100 | }); 101 | 102 | readonly maxRangePlaceholder = computed(() => { 103 | return `Max ${ 104 | this.column().getFacetedMinMaxValues()?.[1] 105 | ? `(${this.column().getFacetedMinMaxValues()?.[1]})` 106 | : '' 107 | }`; 108 | }); 109 | 110 | readonly sortedUniqueValues = computed(() => { 111 | const filterVariant = this.filterVariant(); 112 | const column = this.column(); 113 | if (filterVariant === 'range') { 114 | return []; 115 | } 116 | return Array.from(column.getFacetedUniqueValues().keys()).sort().slice(0, 5000); 117 | }); 118 | 119 | readonly changeMinRangeValue = (event: Event) => { 120 | const value = (event.target as HTMLInputElement).value; 121 | this.column().setFilterValue((old: [number, number]) => { 122 | return [value, old?.[1]]; 123 | }); 124 | }; 125 | 126 | readonly changeMaxRangeValue = (event: Event) => { 127 | const value = (event.target as HTMLInputElement).value; 128 | this.column().setFilterValue((old: [number, number]) => { 129 | return [old?.[0], value]; 130 | }); 131 | }; 132 | } 133 | -------------------------------------------------------------------------------- /src/app/pages/filters/filters.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 | 7 | @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { 8 | 9 | @for (header of headerGroup.headers; track header.id) { 10 | 40 | } 41 | 42 | } 43 | 44 | 45 | @for (row of table.getRowModel().rows; track row.id) { 46 | 47 | @for (cell of row.getVisibleCells(); track cell.id) { 48 | 59 | } 60 | 61 | } 62 | 63 |
11 | @if (!header.isPlaceholder) { 12 |
17 | 24 | {{ headerCell }} 25 | 26 | 27 | @if (header.column.getIsSorted() === 'asc') { 28 | 🔼 29 | } @if (header.column.getIsSorted() === 'desc') { 30 | 🔽 31 | } 32 |
33 | 34 | @if (header.column.getCanFilter()) { 35 |
36 | 37 |
38 | } } 39 |
49 | 56 | {{ renderCell }} 57 | 58 |
64 |
65 | 66 |
67 |
68 | 77 | 87 | 96 | 105 | 106 |
Page
107 | 108 | {{ table.getState().pagination.pageIndex + 1 }} of {{ table.getPageCount() }} 109 | 110 |
111 | 112 | | Go to page: 113 | 120 | 121 | 122 | 127 |
128 |
{{ table.getPrePaginationRowModel().rows.length }} Rows
129 |
130 | 131 |
132 |
133 |
{{ stringifiedFilters() }}
134 |
135 |
136 | 137 | 138 | Age 🥳 139 | 140 | -------------------------------------------------------------------------------- /src/app/pages/view/components/table-filter.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, computed, input, OnInit } from '@angular/core'; 3 | 4 | import type { Column, RowData, Table } from '@tanstack/angular-table'; 5 | 6 | import { DebouncedInputDirective } from './debounced-input.directive'; 7 | 8 | declare module '@tanstack/angular-table' { 9 | //allows us to define custom properties for our columns 10 | interface ColumnMeta { 11 | filterVariant?: 'text' | 'range' | 'select'; 12 | } 13 | } 14 | 15 | @Component({ 16 | selector: 'app-view-table-filter', 17 | template: ` 18 | @if (filterVariant() === 'range') { 19 |
20 |
21 | 32 | 33 | 44 |
45 |
46 |
47 | } @else if (filterVariant() === 'select') { 48 | 59 | } @else { 60 | 61 | @for (value of sortedUniqueValues(); track value) { 62 | 65 | } 66 | 67 | 77 |
78 | } 79 | `, 80 | standalone: true, 81 | imports: [CommonModule, DebouncedInputDirective] 82 | }) 83 | export class FilterComponent { 84 | column = input.required>(); 85 | 86 | table = input.required>(); 87 | 88 | readonly filterVariant = computed(() => { 89 | return (this.column().columnDef.meta ?? {}).filterVariant; 90 | }); 91 | 92 | readonly columnFilterValue = computed(() => this.column().getFilterValue()); 93 | 94 | readonly minRangePlaceholder = computed(() => { 95 | return `Min ${ 96 | this.column().getFacetedMinMaxValues()?.[0] !== undefined 97 | ? `(${this.column().getFacetedMinMaxValues()?.[0]})` 98 | : '' 99 | }`; 100 | }); 101 | 102 | readonly maxRangePlaceholder = computed(() => { 103 | return `Max ${ 104 | this.column().getFacetedMinMaxValues()?.[1] 105 | ? `(${this.column().getFacetedMinMaxValues()?.[1]})` 106 | : '' 107 | }`; 108 | }); 109 | 110 | readonly sortedUniqueValues = computed(() => { 111 | const filterVariant = this.filterVariant(); 112 | const column = this.column(); 113 | if (filterVariant === 'range') { 114 | return []; 115 | } 116 | return Array.from(column.getFacetedUniqueValues().keys()).sort().slice(0, 5000); 117 | }); 118 | 119 | readonly changeMinRangeValue = (event: Event) => { 120 | const value = (event.target as HTMLInputElement).value; 121 | this.column().setFilterValue((old: [number, number]) => { 122 | return [value, old?.[1]]; 123 | }); 124 | }; 125 | 126 | readonly changeMaxRangeValue = (event: Event) => { 127 | const value = (event.target as HTMLInputElement).value; 128 | this.column().setFilterValue((old: [number, number]) => { 129 | return [old?.[0], value]; 130 | }); 131 | }; 132 | } 133 | -------------------------------------------------------------------------------- /src/app/pages/view/view.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { RdxDropdownMenuTriggerDirective } from '@radix-ng/primitives/dropdown-menu'; 6 | import { ShButtonDirective } from '@radix-ng/shadcn/button'; 7 | import { 8 | ShDropdownMenuCheckboxItemComponent, 9 | ShDropdownMenuContentComponent 10 | } from '@radix-ng/shadcn/dropdown-menu'; 11 | import { ShInputDirective } from '@radix-ng/shadcn/input'; 12 | import { 13 | TableBodyDirective, 14 | TableCellDirective, 15 | TableDirective, 16 | TableFooterDirective, 17 | TableHeadDirective, 18 | TableHeaderDirective, 19 | TableRowDirective 20 | } from '@radix-ng/shadcn/table'; 21 | import { 22 | Column, 23 | ColumnDef, 24 | createAngularTable, 25 | FlexRenderComponent, 26 | FlexRenderDirective, 27 | getCoreRowModel, 28 | getFacetedMinMaxValues, 29 | getFacetedRowModel, 30 | getFacetedUniqueValues, 31 | getFilteredRowModel, 32 | getPaginationRowModel, 33 | getSortedRowModel, 34 | SortingState, 35 | type ColumnFiltersState 36 | } from '@tanstack/angular-table'; 37 | import { LucideAngularModule } from 'lucide-angular'; 38 | 39 | import dataTasks from '../../../../public/data/tasks.json'; 40 | import { 41 | FilterComponent, 42 | ViewTableHeadSelectionComponent, 43 | ViewTableRowSelectionComponent, 44 | ViewTitleRowComponent 45 | } from './components'; 46 | import { Task } from './types'; 47 | 48 | @Component({ 49 | selector: 'app-views', 50 | standalone: true, 51 | imports: [ 52 | FilterComponent, 53 | FlexRenderDirective, 54 | FormsModule, 55 | NgClass, 56 | TableDirective, 57 | TableHeaderDirective, 58 | ShButtonDirective, 59 | ShDropdownMenuContentComponent, 60 | ShDropdownMenuCheckboxItemComponent, 61 | TableBodyDirective, 62 | TableFooterDirective, 63 | TableRowDirective, 64 | TableCellDirective, 65 | TableHeadDirective, 66 | ShInputDirective, 67 | LucideAngularModule, 68 | RdxDropdownMenuTriggerDirective 69 | ], 70 | templateUrl: './view.component.html', 71 | changeDetection: ChangeDetectionStrategy.OnPush 72 | }) 73 | export class ViewComponent { 74 | readonly columnFilters = signal([]); 75 | readonly columnSorting = signal([]); 76 | 77 | readonly data = dataTasks as Task[]; 78 | 79 | readonly columns: ColumnDef[] = [ 80 | { 81 | id: 'select', 82 | header: () => { 83 | return new FlexRenderComponent(ViewTableHeadSelectionComponent); 84 | }, 85 | cell: () => { 86 | return new FlexRenderComponent(ViewTableRowSelectionComponent); 87 | }, 88 | enableHiding: false 89 | }, 90 | { 91 | accessorKey: 'id', 92 | header: () => 'Task', 93 | cell: (info) => info.getValue(), 94 | enableHiding: false 95 | }, 96 | { 97 | accessorKey: 'title', 98 | header: () => 'Title', 99 | cell: (info) => { 100 | return new FlexRenderComponent(ViewTitleRowComponent); 101 | }, 102 | enableSorting: true 103 | }, 104 | { 105 | accessorKey: 'status', 106 | header: () => 'Status', 107 | cell: (info) => info.getValue() 108 | }, 109 | { 110 | accessorKey: 'label', 111 | cell: (info) => info.getValue() 112 | }, 113 | { 114 | accessorKey: 'priority', 115 | cell: (info) => info.getValue() 116 | } 117 | ]; 118 | 119 | table = createAngularTable(() => ({ 120 | columns: this.columns, 121 | data: this.data, 122 | state: { 123 | sorting: this.columnSorting(), 124 | columnFilters: this.columnFilters() 125 | }, 126 | onColumnFiltersChange: (updater) => { 127 | updater instanceof Function 128 | ? this.columnFilters.update(updater) 129 | : this.columnFilters.set(updater); 130 | }, 131 | onSortingChange: (updater) => { 132 | updater instanceof Function 133 | ? this.columnSorting.update(updater) 134 | : this.columnSorting.set(updater); 135 | }, 136 | getCoreRowModel: getCoreRowModel(), 137 | getFilteredRowModel: getFilteredRowModel(), //client-side filtering 138 | getSortedRowModel: getSortedRowModel(), 139 | getPaginationRowModel: getPaginationRowModel(), 140 | getFacetedRowModel: getFacetedRowModel(), // client-side faceting 141 | getFacetedUniqueValues: getFacetedUniqueValues(), // generate unique values for select filter/autocomplete 142 | getFacetedMinMaxValues: getFacetedMinMaxValues(), // generate min/max values for range filter 143 | debugTable: true, 144 | debugHeaders: true, 145 | debugColumns: false 146 | })); 147 | 148 | onPageSizeChange(event: any): void { 149 | this.table.setPageSize(Number(event.target.value)); 150 | } 151 | 152 | onFilterTasksChange(data: any): void { 153 | this.table.getColumn('title')?.setFilterValue(data); 154 | } 155 | 156 | onColumnVisibilityChange($event: any, column: any): void { 157 | column.toggleVisibility(!!$event); 158 | } 159 | 160 | filterHideColumns(): Column[] { 161 | return this.table.getAllColumns().filter((column) => column.getCanHide()); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/app/pages/row-selection/row-selection.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | computed, 5 | signal, 6 | TemplateRef, 7 | viewChild 8 | } from '@angular/core'; 9 | import { FormsModule } from '@angular/forms'; 10 | 11 | import { ShButtonDirective } from '@radix-ng/shadcn/button'; 12 | import { 13 | TableBodyDirective, 14 | TableCellDirective, 15 | TableDirective, 16 | TableFooterDirective, 17 | TableHeadDirective, 18 | TableHeaderDirective, 19 | TableRowDirective 20 | } from '@radix-ng/shadcn/table'; 21 | import { 22 | ColumnDef, 23 | createAngularTable, 24 | FlexRenderComponent, 25 | FlexRenderDirective, 26 | getCoreRowModel, 27 | getFilteredRowModel, 28 | getPaginationRowModel, 29 | RowSelectionState 30 | } from '@tanstack/angular-table'; 31 | 32 | import { FilterComponent } from './components/filter.component'; 33 | import { 34 | TableHeadSelectionComponent, 35 | TableRowSelectionComponent 36 | } from './components/selection-column.component'; 37 | import { makeData, type Person } from './makeData'; 38 | 39 | @Component({ 40 | selector: 'app-row-selection', 41 | standalone: true, 42 | imports: [ 43 | FilterComponent, 44 | FlexRenderDirective, 45 | FormsModule, 46 | TableDirective, 47 | TableHeaderDirective, 48 | ShButtonDirective, 49 | TableBodyDirective, 50 | TableFooterDirective, 51 | TableRowDirective, 52 | TableCellDirective, 53 | TableHeadDirective 54 | ], 55 | templateUrl: './row-selection.component.html', 56 | changeDetection: ChangeDetectionStrategy.OnPush 57 | }) 58 | export class RowSelectionComponent { 59 | private readonly rowSelection = signal({}); 60 | readonly globalFilter = signal(''); 61 | readonly data = signal(makeData(10_000)); 62 | 63 | readonly ageHeaderCell = viewChild.required>('ageHeaderCell'); 64 | 65 | readonly columns: ColumnDef[] = [ 66 | { 67 | id: 'select', 68 | header: () => { 69 | return new FlexRenderComponent(TableHeadSelectionComponent); 70 | }, 71 | cell: () => { 72 | return new FlexRenderComponent(TableRowSelectionComponent); 73 | } 74 | }, 75 | { 76 | header: 'Name', 77 | footer: (props) => props.column.id, 78 | columns: [ 79 | { 80 | accessorKey: 'firstName', 81 | cell: (info) => info.getValue(), 82 | footer: (props) => props.column.id, 83 | header: 'First name' 84 | }, 85 | { 86 | accessorFn: (row) => row.lastName, 87 | id: 'lastName', 88 | cell: (info) => info.getValue(), 89 | header: () => 'Last Name', 90 | footer: (props) => props.column.id 91 | } 92 | ] 93 | }, 94 | { 95 | header: 'Info', 96 | footer: (props) => props.column.id, 97 | columns: [ 98 | { 99 | accessorKey: 'age', 100 | header: () => this.ageHeaderCell(), 101 | footer: (props) => props.column.id 102 | }, 103 | { 104 | header: 'More Info', 105 | columns: [ 106 | { 107 | accessorKey: 'visits', 108 | header: () => 'Visits', 109 | footer: (props) => props.column.id 110 | }, 111 | { 112 | accessorKey: 'status', 113 | header: 'Status', 114 | footer: (props) => props.column.id 115 | } 116 | ] 117 | } 118 | ] 119 | } 120 | ]; 121 | 122 | table = createAngularTable(() => ({ 123 | data: this.data(), 124 | columns: this.columns, 125 | state: { 126 | rowSelection: this.rowSelection() 127 | }, 128 | enableRowSelection: true, // enable row selection for all rows 129 | // enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row 130 | onRowSelectionChange: (updaterOrValue) => { 131 | this.rowSelection.set( 132 | typeof updaterOrValue === 'function' 133 | ? updaterOrValue(this.rowSelection()) 134 | : updaterOrValue 135 | ); 136 | }, 137 | getCoreRowModel: getCoreRowModel(), 138 | getFilteredRowModel: getFilteredRowModel(), 139 | getPaginationRowModel: getPaginationRowModel(), 140 | debugTable: true 141 | })); 142 | 143 | readonly stringifiedRowSelection = computed(() => JSON.stringify(this.rowSelection(), null, 2)); 144 | 145 | readonly rowSelectionLength = computed(() => Object.keys(this.rowSelection()).length); 146 | 147 | onPageInputChange(event: Event): void { 148 | const inputElement = event.target as HTMLInputElement; 149 | const page = inputElement.value ? Number(inputElement.value) - 1 : 0; 150 | this.table.setPageIndex(page); 151 | } 152 | 153 | onPageSizeChange(event: any): void { 154 | this.table.setPageSize(Number(event.target.value)); 155 | } 156 | 157 | logSelectedFlatRows(): void { 158 | console.info( 159 | 'table.getSelectedRowModel().flatRows', 160 | this.table.getSelectedRowModel().flatRows 161 | ); 162 | } 163 | 164 | refreshData(): void { 165 | this.data.set(makeData(10_000)); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/app/components/site-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core'; 2 | import { Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; 3 | 4 | import { ShButtonDirective } from '@radix-ng/shadcn/button'; 5 | 6 | @Component({ 7 | selector: 'app-site-header', 8 | standalone: true, 9 | imports: [RouterLink, RouterLinkActive, RouterOutlet, ShButtonDirective], 10 | template: ` 11 |
14 |
15 | 16 | 22 | 23 | 30 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 70 | 88 |
89 |
90 | ` 91 | }) 92 | export class SiteHeaderComponent { 93 | private readonly router = inject(Router); 94 | 95 | protected readonly routes: { 96 | path: string; 97 | exact?: boolean; 98 | external?: boolean; 99 | label: string; 100 | }[] = [ 101 | { path: '/', exact: true, label: 'Base' }, 102 | { path: '/column-ordering', exact: true, label: 'Column Ordering' }, 103 | { path: '/filters', exact: true, label: 'Filters' }, 104 | { path: '/row-selection', exact: true, label: 'Row Selection' } 105 | ]; 106 | 107 | isActive(route: { path: string; exact?: boolean; label: string; external?: boolean }): boolean { 108 | if (route.external) { 109 | return false; 110 | } 111 | return this.router.url === route.path; 112 | } 113 | 114 | getRouteClasses(route: any): string { 115 | const baseClasses = this.isActive(route) ? 'text-foreground' : 'text-foreground/60'; 116 | const additionalClasses = 117 | route.path === '/docs/getting-started/installation' ? ' hidden md:block' : ''; 118 | return baseClasses + additionalClasses; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/app/pages/view/view.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | 14 | 15 | 19 | 20 | 21 | 22 | @for (column of filterHideColumns(); track column.id) { 23 | 24 | {{ column.id }} 25 | 26 | } 27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 | 35 | 36 | @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { 37 | 38 | @for (header of headerGroup.headers; track header.id) { 39 | 58 | } 59 | 60 | } 61 | 62 | 63 | @for (row of table.getRowModel().rows; track row.id) { 64 | 65 | @for (cell of row.getVisibleCells(); track cell.id) { 66 | 76 | } 77 | 78 | } 79 | 80 |
40 | @if (!header.isPlaceholder) { 41 |
42 | 48 | 54 | 55 |
56 | } 57 |
67 | 73 | {{ renderCell }} 74 | 75 |
81 |
82 | 83 |
84 | 85 |
87 |
88 | {{ table.getFilteredSelectedRowModel().rows.length }} 89 | of {{ table.getFilteredRowModel().rows.length }} row(s) selected. 90 |
91 |
92 |
93 |

Rows per page

94 | 100 |
101 |
102 | Page {{ table.getState().pagination.pageIndex + 1 }} of {{ table.getPageCount() }} 103 |
104 |
105 | 114 | 124 | 133 | 142 |
143 |
144 |
145 |
146 | -------------------------------------------------------------------------------- /public/data/tasks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "TASK-8782", 4 | "title": "You can't compress the program without quantifying the open-source SSD pixel!", 5 | "status": "in progress", 6 | "label": "documentation", 7 | "priority": "medium" 8 | }, 9 | { 10 | "id": "TASK-7878", 11 | "title": "Try to calculate the EXE feed, maybe it will index the multi-byte pixel!", 12 | "status": "backlog", 13 | "label": "documentation", 14 | "priority": "medium" 15 | }, 16 | { 17 | "id": "TASK-7839", 18 | "title": "We need to bypass the neural TCP card!", 19 | "status": "todo", 20 | "label": "bug", 21 | "priority": "high" 22 | }, 23 | { 24 | "id": "TASK-5562", 25 | "title": "The SAS interface is down, bypass the open-source pixel so we can back up the PNG bandwidth!", 26 | "status": "backlog", 27 | "label": "feature", 28 | "priority": "medium" 29 | }, 30 | { 31 | "id": "TASK-8686", 32 | "title": "I'll parse the wireless SSL protocol, that should driver the API panel!", 33 | "status": "canceled", 34 | "label": "feature", 35 | "priority": "medium" 36 | }, 37 | { 38 | "id": "TASK-1280", 39 | "title": "Use the digital TLS panel, then you can transmit the haptic system!", 40 | "status": "done", 41 | "label": "bug", 42 | "priority": "high" 43 | }, 44 | { 45 | "id": "TASK-7262", 46 | "title": "The UTF8 application is down, parse the neural bandwidth so we can back up the PNG firewall!", 47 | "status": "done", 48 | "label": "feature", 49 | "priority": "high" 50 | }, 51 | { 52 | "id": "TASK-1138", 53 | "title": "Generating the driver won't do anything, we need to quantify the 1080p SMTP bandwidth!", 54 | "status": "in progress", 55 | "label": "feature", 56 | "priority": "medium" 57 | }, 58 | { 59 | "id": "TASK-7184", 60 | "title": "We need to program the back-end THX pixel!", 61 | "status": "todo", 62 | "label": "feature", 63 | "priority": "low" 64 | }, 65 | { 66 | "id": "TASK-5160", 67 | "title": "Calculating the bus won't do anything, we need to navigate the back-end JSON protocol!", 68 | "status": "in progress", 69 | "label": "documentation", 70 | "priority": "high" 71 | }, 72 | { 73 | "id": "TASK-5618", 74 | "title": "Generating the driver won't do anything, we need to index the online SSL application!", 75 | "status": "done", 76 | "label": "documentation", 77 | "priority": "medium" 78 | }, 79 | { 80 | "id": "TASK-6699", 81 | "title": "I'll transmit the wireless JBOD capacitor, that should hard drive the SSD feed!", 82 | "status": "backlog", 83 | "label": "documentation", 84 | "priority": "medium" 85 | }, 86 | { 87 | "id": "TASK-2858", 88 | "title": "We need to override the online UDP bus!", 89 | "status": "backlog", 90 | "label": "bug", 91 | "priority": "medium" 92 | }, 93 | { 94 | "id": "TASK-9864", 95 | "title": "I'll reboot the 1080p FTP panel, that should matrix the HEX hard drive!", 96 | "status": "done", 97 | "label": "bug", 98 | "priority": "high" 99 | }, 100 | { 101 | "id": "TASK-8404", 102 | "title": "We need to generate the virtual HEX alarm!", 103 | "status": "in progress", 104 | "label": "bug", 105 | "priority": "low" 106 | }, 107 | { 108 | "id": "TASK-5365", 109 | "title": "Backing up the pixel won't do anything, we need to transmit the primary IB array!", 110 | "status": "in progress", 111 | "label": "documentation", 112 | "priority": "low" 113 | }, 114 | { 115 | "id": "TASK-1780", 116 | "title": "The CSS feed is down, index the bluetooth transmitter so we can compress the CLI protocol!", 117 | "status": "todo", 118 | "label": "documentation", 119 | "priority": "high" 120 | }, 121 | { 122 | "id": "TASK-6938", 123 | "title": "Use the redundant SCSI application, then you can hack the optical alarm!", 124 | "status": "todo", 125 | "label": "documentation", 126 | "priority": "high" 127 | }, 128 | { 129 | "id": "TASK-9885", 130 | "title": "We need to compress the auxiliary VGA driver!", 131 | "status": "backlog", 132 | "label": "bug", 133 | "priority": "high" 134 | }, 135 | { 136 | "id": "TASK-3216", 137 | "title": "Transmitting the transmitter won't do anything, we need to compress the virtual HDD sensor!", 138 | "status": "backlog", 139 | "label": "documentation", 140 | "priority": "medium" 141 | }, 142 | { 143 | "id": "TASK-9285", 144 | "title": "The IP monitor is down, copy the haptic alarm so we can generate the HTTP transmitter!", 145 | "status": "todo", 146 | "label": "bug", 147 | "priority": "high" 148 | }, 149 | { 150 | "id": "TASK-1024", 151 | "title": "Overriding the microchip won't do anything, we need to transmit the digital OCR transmitter!", 152 | "status": "in progress", 153 | "label": "documentation", 154 | "priority": "low" 155 | }, 156 | { 157 | "id": "TASK-7068", 158 | "title": "You can't generate the capacitor without indexing the wireless HEX pixel!", 159 | "status": "canceled", 160 | "label": "bug", 161 | "priority": "low" 162 | }, 163 | { 164 | "id": "TASK-6502", 165 | "title": "Navigating the microchip won't do anything, we need to bypass the back-end SQL bus!", 166 | "status": "todo", 167 | "label": "bug", 168 | "priority": "high" 169 | }, 170 | { 171 | "id": "TASK-5326", 172 | "title": "We need to hack the redundant UTF8 transmitter!", 173 | "status": "todo", 174 | "label": "bug", 175 | "priority": "low" 176 | }, 177 | { 178 | "id": "TASK-6274", 179 | "title": "Use the virtual PCI circuit, then you can parse the bluetooth alarm!", 180 | "status": "canceled", 181 | "label": "documentation", 182 | "priority": "low" 183 | }, 184 | { 185 | "id": "TASK-1571", 186 | "title": "I'll input the neural DRAM circuit, that should protocol the SMTP interface!", 187 | "status": "in progress", 188 | "label": "feature", 189 | "priority": "medium" 190 | }, 191 | { 192 | "id": "TASK-9518", 193 | "title": "Compressing the interface won't do anything, we need to compress the online SDD matrix!", 194 | "status": "canceled", 195 | "label": "documentation", 196 | "priority": "medium" 197 | }, 198 | { 199 | "id": "TASK-5581", 200 | "title": "I'll synthesize the digital COM pixel, that should transmitter the UTF8 protocol!", 201 | "status": "backlog", 202 | "label": "documentation", 203 | "priority": "high" 204 | }, 205 | { 206 | "id": "TASK-2197", 207 | "title": "Parsing the feed won't do anything, we need to copy the bluetooth DRAM bus!", 208 | "status": "todo", 209 | "label": "documentation", 210 | "priority": "low" 211 | }, 212 | { 213 | "id": "TASK-8484", 214 | "title": "We need to parse the solid state UDP firewall!", 215 | "status": "in progress", 216 | "label": "bug", 217 | "priority": "low" 218 | }, 219 | { 220 | "id": "TASK-9892", 221 | "title": "If we back up the application, we can get to the UDP application through the multi-byte THX capacitor!", 222 | "status": "done", 223 | "label": "documentation", 224 | "priority": "high" 225 | }, 226 | { 227 | "id": "TASK-9616", 228 | "title": "We need to synthesize the cross-platform ASCII pixel!", 229 | "status": "in progress", 230 | "label": "feature", 231 | "priority": "medium" 232 | }, 233 | { 234 | "id": "TASK-9744", 235 | "title": "Use the back-end IP card, then you can input the solid state hard drive!", 236 | "status": "done", 237 | "label": "documentation", 238 | "priority": "low" 239 | }, 240 | { 241 | "id": "TASK-1376", 242 | "title": "Generating the alarm won't do anything, we need to generate the mobile IP capacitor!", 243 | "status": "backlog", 244 | "label": "documentation", 245 | "priority": "low" 246 | }, 247 | { 248 | "id": "TASK-7382", 249 | "title": "If we back up the firewall, we can get to the RAM alarm through the primary UTF8 pixel!", 250 | "status": "todo", 251 | "label": "feature", 252 | "priority": "low" 253 | }, 254 | { 255 | "id": "TASK-2290", 256 | "title": "I'll compress the virtual JSON panel, that should application the UTF8 bus!", 257 | "status": "canceled", 258 | "label": "documentation", 259 | "priority": "high" 260 | }, 261 | { 262 | "id": "TASK-1533", 263 | "title": "You can't input the firewall without overriding the wireless TCP firewall!", 264 | "status": "done", 265 | "label": "bug", 266 | "priority": "high" 267 | }, 268 | { 269 | "id": "TASK-4920", 270 | "title": "Bypassing the hard drive won't do anything, we need to input the bluetooth JSON program!", 271 | "status": "in progress", 272 | "label": "bug", 273 | "priority": "high" 274 | }, 275 | { 276 | "id": "TASK-5168", 277 | "title": "If we synthesize the bus, we can get to the IP panel through the virtual TLS array!", 278 | "status": "in progress", 279 | "label": "feature", 280 | "priority": "low" 281 | }, 282 | { 283 | "id": "TASK-7103", 284 | "title": "We need to parse the multi-byte EXE bandwidth!", 285 | "status": "canceled", 286 | "label": "feature", 287 | "priority": "low" 288 | }, 289 | { 290 | "id": "TASK-4314", 291 | "title": "If we compress the program, we can get to the XML alarm through the multi-byte COM matrix!", 292 | "status": "in progress", 293 | "label": "bug", 294 | "priority": "high" 295 | }, 296 | { 297 | "id": "TASK-3415", 298 | "title": "Use the cross-platform XML application, then you can quantify the solid state feed!", 299 | "status": "todo", 300 | "label": "feature", 301 | "priority": "high" 302 | }, 303 | { 304 | "id": "TASK-8339", 305 | "title": "Try to calculate the DNS interface, maybe it will input the bluetooth capacitor!", 306 | "status": "in progress", 307 | "label": "feature", 308 | "priority": "low" 309 | }, 310 | { 311 | "id": "TASK-6995", 312 | "title": "Try to hack the XSS bandwidth, maybe it will override the bluetooth matrix!", 313 | "status": "todo", 314 | "label": "feature", 315 | "priority": "high" 316 | }, 317 | { 318 | "id": "TASK-8053", 319 | "title": "If we connect the program, we can get to the UTF8 matrix through the digital UDP protocol!", 320 | "status": "todo", 321 | "label": "feature", 322 | "priority": "medium" 323 | }, 324 | { 325 | "id": "TASK-4336", 326 | "title": "If we synthesize the microchip, we can get to the SAS sensor through the optical UDP program!", 327 | "status": "todo", 328 | "label": "documentation", 329 | "priority": "low" 330 | }, 331 | { 332 | "id": "TASK-8790", 333 | "title": "I'll back up the optical COM alarm, that should alarm the RSS capacitor!", 334 | "status": "done", 335 | "label": "bug", 336 | "priority": "medium" 337 | }, 338 | { 339 | "id": "TASK-8980", 340 | "title": "Try to navigate the SQL transmitter, maybe it will back up the virtual firewall!", 341 | "status": "canceled", 342 | "label": "bug", 343 | "priority": "low" 344 | }, 345 | { 346 | "id": "TASK-7342", 347 | "title": "Use the neural CLI card, then you can parse the online port!", 348 | "status": "backlog", 349 | "label": "documentation", 350 | "priority": "low" 351 | }, 352 | { 353 | "id": "TASK-5608", 354 | "title": "I'll hack the haptic SSL program, that should bus the UDP transmitter!", 355 | "status": "canceled", 356 | "label": "documentation", 357 | "priority": "low" 358 | }, 359 | { 360 | "id": "TASK-1606", 361 | "title": "I'll generate the bluetooth PNG firewall, that should pixel the SSL driver!", 362 | "status": "done", 363 | "label": "feature", 364 | "priority": "medium" 365 | }, 366 | { 367 | "id": "TASK-7872", 368 | "title": "Transmitting the circuit won't do anything, we need to reboot the 1080p RSS monitor!", 369 | "status": "canceled", 370 | "label": "feature", 371 | "priority": "medium" 372 | }, 373 | { 374 | "id": "TASK-4167", 375 | "title": "Use the cross-platform SMS circuit, then you can synthesize the optical feed!", 376 | "status": "canceled", 377 | "label": "bug", 378 | "priority": "medium" 379 | }, 380 | { 381 | "id": "TASK-9581", 382 | "title": "You can't index the port without hacking the cross-platform XSS monitor!", 383 | "status": "backlog", 384 | "label": "documentation", 385 | "priority": "low" 386 | }, 387 | { 388 | "id": "TASK-8806", 389 | "title": "We need to bypass the back-end SSL panel!", 390 | "status": "done", 391 | "label": "bug", 392 | "priority": "medium" 393 | }, 394 | { 395 | "id": "TASK-6542", 396 | "title": "Try to quantify the RSS firewall, maybe it will quantify the open-source system!", 397 | "status": "done", 398 | "label": "feature", 399 | "priority": "low" 400 | }, 401 | { 402 | "id": "TASK-6806", 403 | "title": "The VGA protocol is down, reboot the back-end matrix so we can parse the CSS panel!", 404 | "status": "canceled", 405 | "label": "documentation", 406 | "priority": "low" 407 | }, 408 | { 409 | "id": "TASK-9549", 410 | "title": "You can't bypass the bus without connecting the neural JBOD bus!", 411 | "status": "todo", 412 | "label": "feature", 413 | "priority": "high" 414 | }, 415 | { 416 | "id": "TASK-1075", 417 | "title": "Backing up the driver won't do anything, we need to parse the redundant RAM pixel!", 418 | "status": "done", 419 | "label": "feature", 420 | "priority": "medium" 421 | }, 422 | { 423 | "id": "TASK-1427", 424 | "title": "Use the auxiliary PCI circuit, then you can calculate the cross-platform interface!", 425 | "status": "done", 426 | "label": "documentation", 427 | "priority": "high" 428 | }, 429 | { 430 | "id": "TASK-1907", 431 | "title": "Hacking the circuit won't do anything, we need to back up the online DRAM system!", 432 | "status": "todo", 433 | "label": "documentation", 434 | "priority": "high" 435 | }, 436 | { 437 | "id": "TASK-4309", 438 | "title": "If we generate the system, we can get to the TCP sensor through the optical GB pixel!", 439 | "status": "backlog", 440 | "label": "bug", 441 | "priority": "medium" 442 | }, 443 | { 444 | "id": "TASK-3973", 445 | "title": "I'll parse the back-end ADP array, that should bandwidth the RSS bandwidth!", 446 | "status": "todo", 447 | "label": "feature", 448 | "priority": "medium" 449 | }, 450 | { 451 | "id": "TASK-7962", 452 | "title": "Use the wireless RAM program, then you can hack the cross-platform feed!", 453 | "status": "canceled", 454 | "label": "bug", 455 | "priority": "low" 456 | }, 457 | { 458 | "id": "TASK-3360", 459 | "title": "You can't quantify the program without synthesizing the neural OCR interface!", 460 | "status": "done", 461 | "label": "feature", 462 | "priority": "medium" 463 | }, 464 | { 465 | "id": "TASK-9887", 466 | "title": "Use the auxiliary ASCII sensor, then you can connect the solid state port!", 467 | "status": "backlog", 468 | "label": "bug", 469 | "priority": "medium" 470 | }, 471 | { 472 | "id": "TASK-3649", 473 | "title": "I'll input the virtual USB system, that should circuit the DNS monitor!", 474 | "status": "in progress", 475 | "label": "feature", 476 | "priority": "medium" 477 | }, 478 | { 479 | "id": "TASK-3586", 480 | "title": "If we quantify the circuit, we can get to the CLI feed through the mobile SMS hard drive!", 481 | "status": "in progress", 482 | "label": "bug", 483 | "priority": "low" 484 | }, 485 | { 486 | "id": "TASK-5150", 487 | "title": "I'll hack the wireless XSS port, that should transmitter the IP interface!", 488 | "status": "canceled", 489 | "label": "feature", 490 | "priority": "medium" 491 | }, 492 | { 493 | "id": "TASK-3652", 494 | "title": "The SQL interface is down, override the optical bus so we can program the ASCII interface!", 495 | "status": "backlog", 496 | "label": "feature", 497 | "priority": "low" 498 | }, 499 | { 500 | "id": "TASK-6884", 501 | "title": "Use the digital PCI circuit, then you can synthesize the multi-byte microchip!", 502 | "status": "canceled", 503 | "label": "feature", 504 | "priority": "high" 505 | }, 506 | { 507 | "id": "TASK-1591", 508 | "title": "We need to connect the mobile XSS driver!", 509 | "status": "in progress", 510 | "label": "feature", 511 | "priority": "high" 512 | }, 513 | { 514 | "id": "TASK-3802", 515 | "title": "Try to override the ASCII protocol, maybe it will parse the virtual matrix!", 516 | "status": "in progress", 517 | "label": "feature", 518 | "priority": "low" 519 | }, 520 | { 521 | "id": "TASK-7253", 522 | "title": "Programming the capacitor won't do anything, we need to bypass the neural IB hard drive!", 523 | "status": "backlog", 524 | "label": "bug", 525 | "priority": "high" 526 | }, 527 | { 528 | "id": "TASK-9739", 529 | "title": "We need to hack the multi-byte HDD bus!", 530 | "status": "done", 531 | "label": "documentation", 532 | "priority": "medium" 533 | }, 534 | { 535 | "id": "TASK-4424", 536 | "title": "Try to hack the HEX alarm, maybe it will connect the optical pixel!", 537 | "status": "in progress", 538 | "label": "documentation", 539 | "priority": "medium" 540 | }, 541 | { 542 | "id": "TASK-3922", 543 | "title": "You can't back up the capacitor without generating the wireless PCI program!", 544 | "status": "backlog", 545 | "label": "bug", 546 | "priority": "low" 547 | }, 548 | { 549 | "id": "TASK-4921", 550 | "title": "I'll index the open-source IP feed, that should system the GB application!", 551 | "status": "canceled", 552 | "label": "bug", 553 | "priority": "low" 554 | }, 555 | { 556 | "id": "TASK-5814", 557 | "title": "We need to calculate the 1080p AGP feed!", 558 | "status": "backlog", 559 | "label": "bug", 560 | "priority": "high" 561 | }, 562 | { 563 | "id": "TASK-2645", 564 | "title": "Synthesizing the system won't do anything, we need to navigate the multi-byte HDD firewall!", 565 | "status": "todo", 566 | "label": "documentation", 567 | "priority": "medium" 568 | }, 569 | { 570 | "id": "TASK-4535", 571 | "title": "Try to copy the JSON circuit, maybe it will connect the wireless feed!", 572 | "status": "in progress", 573 | "label": "feature", 574 | "priority": "low" 575 | }, 576 | { 577 | "id": "TASK-4463", 578 | "title": "We need to copy the solid state AGP monitor!", 579 | "status": "done", 580 | "label": "documentation", 581 | "priority": "low" 582 | }, 583 | { 584 | "id": "TASK-9745", 585 | "title": "If we connect the protocol, we can get to the GB system through the bluetooth PCI microchip!", 586 | "status": "canceled", 587 | "label": "feature", 588 | "priority": "high" 589 | }, 590 | { 591 | "id": "TASK-2080", 592 | "title": "If we input the bus, we can get to the RAM matrix through the auxiliary RAM card!", 593 | "status": "todo", 594 | "label": "bug", 595 | "priority": "medium" 596 | }, 597 | { 598 | "id": "TASK-3838", 599 | "title": "I'll bypass the online TCP application, that should panel the AGP system!", 600 | "status": "backlog", 601 | "label": "bug", 602 | "priority": "high" 603 | }, 604 | { 605 | "id": "TASK-1340", 606 | "title": "We need to navigate the virtual PNG circuit!", 607 | "status": "todo", 608 | "label": "bug", 609 | "priority": "medium" 610 | }, 611 | { 612 | "id": "TASK-6665", 613 | "title": "If we parse the monitor, we can get to the SSD hard drive through the cross-platform AGP alarm!", 614 | "status": "canceled", 615 | "label": "feature", 616 | "priority": "low" 617 | }, 618 | { 619 | "id": "TASK-7585", 620 | "title": "If we calculate the hard drive, we can get to the SSL program through the multi-byte CSS microchip!", 621 | "status": "backlog", 622 | "label": "feature", 623 | "priority": "low" 624 | }, 625 | { 626 | "id": "TASK-6319", 627 | "title": "We need to copy the multi-byte SCSI program!", 628 | "status": "backlog", 629 | "label": "bug", 630 | "priority": "high" 631 | }, 632 | { 633 | "id": "TASK-4369", 634 | "title": "Try to input the SCSI bus, maybe it will generate the 1080p pixel!", 635 | "status": "backlog", 636 | "label": "bug", 637 | "priority": "high" 638 | }, 639 | { 640 | "id": "TASK-9035", 641 | "title": "We need to override the solid state PNG array!", 642 | "status": "canceled", 643 | "label": "documentation", 644 | "priority": "low" 645 | }, 646 | { 647 | "id": "TASK-3970", 648 | "title": "You can't index the transmitter without quantifying the haptic ASCII card!", 649 | "status": "todo", 650 | "label": "documentation", 651 | "priority": "medium" 652 | }, 653 | { 654 | "id": "TASK-4473", 655 | "title": "You can't bypass the protocol without overriding the neural RSS program!", 656 | "status": "todo", 657 | "label": "documentation", 658 | "priority": "low" 659 | }, 660 | { 661 | "id": "TASK-4136", 662 | "title": "You can't hack the hard drive without hacking the primary JSON program!", 663 | "status": "canceled", 664 | "label": "bug", 665 | "priority": "medium" 666 | }, 667 | { 668 | "id": "TASK-3939", 669 | "title": "Use the back-end SQL firewall, then you can connect the neural hard drive!", 670 | "status": "done", 671 | "label": "feature", 672 | "priority": "low" 673 | }, 674 | { 675 | "id": "TASK-2007", 676 | "title": "I'll input the back-end USB protocol, that should bandwidth the PCI system!", 677 | "status": "backlog", 678 | "label": "bug", 679 | "priority": "high" 680 | }, 681 | { 682 | "id": "TASK-7516", 683 | "title": "Use the primary SQL program, then you can generate the auxiliary transmitter!", 684 | "status": "done", 685 | "label": "documentation", 686 | "priority": "medium" 687 | }, 688 | { 689 | "id": "TASK-6906", 690 | "title": "Try to back up the DRAM system, maybe it will reboot the online transmitter!", 691 | "status": "done", 692 | "label": "feature", 693 | "priority": "high" 694 | }, 695 | { 696 | "id": "TASK-5207", 697 | "title": "The SMS interface is down, copy the bluetooth bus so we can quantify the VGA card!", 698 | "status": "in progress", 699 | "label": "bug", 700 | "priority": "low" 701 | } 702 | ] 703 | --------------------------------------------------------------------------------