├── .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 |
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 |
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 | |
10 |
17 |
18 |
19 | |
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 | |
29 |
36 |
37 |
38 | |
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 | |
48 |
55 | {{ footer }}
56 |
57 | |
58 | }
59 |
60 | }
61 |
62 |
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 |
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 | |
41 | @if (!header.isPlaceholder) {
42 |
49 | {{ header }}
50 |
51 | }
52 | |
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 | |
62 |
69 | {{ cell }}
70 |
71 | |
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 | |
81 | @if (!header.isPlaceholder) {
82 |
89 | {{ header }}
90 |
91 | }
92 | |
93 | }
94 |
95 | }
96 |
97 |
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 |
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 |
47 | } @else if (filterVariant() === 'select') {
48 |
59 | } @else {
60 |
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 | |
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 |
38 | } }
39 | |
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 | |
49 |
56 | {{ renderCell }}
57 |
58 | |
59 | }
60 |
61 | }
62 |
63 |
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 |
47 | } @else if (filterVariant() === 'select') {
48 |
59 | } @else {
60 |
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 |
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 | |
40 | @if (!header.isPlaceholder) {
41 |
42 |
48 |
54 |
55 |
56 | }
57 | |
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 | |
67 |
73 | {{ renderCell }}
74 |
75 | |
76 | }
77 |
78 | }
79 |
80 |
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 |
--------------------------------------------------------------------------------