diff --git a/apps/code-examples/src/app/app.component.scss b/apps/code-examples/src/app/app.component.scss index 6e01270223..05fb5cdabf 100644 --- a/apps/code-examples/src/app/app.component.scss +++ b/apps/code-examples/src/app/app.component.scss @@ -1,3 +1,10 @@ +:host { + display: block; + width: calc(100vw - var(--sky-viewport-left) - var(--sky-viewport-right)); + height: calc(100vh - var(--sky-viewport-top) - var(--sky-viewport-bottom)); + position: relative; +} + #home-btn { display: block; margin: 5px; @@ -6,6 +13,9 @@ #content { // Value set in the app component. margin-top: var(--sky-viewport-top); + overflow-y: auto; + position: absolute; + inset: 0; } #controls { diff --git a/apps/code-examples/src/styles.scss b/apps/code-examples/src/styles.scss index 55d6e36fc5..6785b97ab8 100644 --- a/apps/code-examples/src/styles.scss +++ b/apps/code-examples/src/styles.scss @@ -1,6 +1,5 @@ html, body { - min-height: 100%; scroll-behavior: smooth; } diff --git a/apps/e2e/data-grid-storybook-e2e/cypress.config.ts b/apps/e2e/data-grid-storybook-e2e/cypress.config.ts new file mode 100644 index 0000000000..79623eb561 --- /dev/null +++ b/apps/e2e/data-grid-storybook-e2e/cypress.config.ts @@ -0,0 +1,14 @@ +import { sendCypressScreenshotsToPercy } from '@skyux-sdk/e2e-schematics'; +import { skyE2ePreset } from '@skyux-sdk/e2e-schematics/cypress-preset'; + +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + ...skyE2ePreset(__dirname, { + setupNodeEvents: (on, config) => { + sendCypressScreenshotsToPercy(on, config); + }, + }), + }, +}); diff --git a/apps/e2e/data-grid-storybook-e2e/eslint.config.js b/apps/e2e/data-grid-storybook-e2e/eslint.config.js new file mode 100644 index 0000000000..f5e63bf056 --- /dev/null +++ b/apps/e2e/data-grid-storybook-e2e/eslint.config.js @@ -0,0 +1,3 @@ +const config = require('../../../eslint-e2e.config'); + +module.exports = config; diff --git a/apps/e2e/data-grid-storybook-e2e/project.json b/apps/e2e/data-grid-storybook-e2e/project.json new file mode 100644 index 0000000000..4e7d1a525c --- /dev/null +++ b/apps/e2e/data-grid-storybook-e2e/project.json @@ -0,0 +1,37 @@ +{ + "name": "data-grid-storybook-e2e", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/e2e/data-grid-storybook-e2e/src", + "projectType": "application", + "targets": { + "e2e": { + "executor": "@nx/cypress:cypress", + "options": { + "cypressConfig": "apps/e2e/data-grid-storybook-e2e/cypress.config.ts", + "devServerTarget": "data-grid-storybook:storybook", + "baseUrl": "http://localhost:4400", + "browser": "chrome", + "testingType": "e2e" + }, + "configurations": { + "ci": { + "baseUrl": "http://localhost:4200", + "browser": "chrome", + "devServerTarget": "data-grid-storybook:static-storybook:ci" + }, + "prebuilt": { + "devServerTarget": "data-grid-storybook:static-storybook:prebuilt", + "baseUrl": "" + } + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "options": { + "lintFilePatterns": ["{projectRoot}/**/*.{js,ts}"] + } + } + }, + "tags": [], + "implicitDependencies": ["data-grid-storybook"] +} diff --git a/apps/e2e/data-grid-storybook-e2e/src/e2e/.gitkeep b/apps/e2e/data-grid-storybook-e2e/src/e2e/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/e2e/data-grid-storybook-e2e/src/e2e/data-grid/data-grid.component.cy.ts b/apps/e2e/data-grid-storybook-e2e/src/e2e/data-grid/data-grid.component.cy.ts new file mode 100644 index 0000000000..09570b10b6 --- /dev/null +++ b/apps/e2e/data-grid-storybook-e2e/src/e2e/data-grid/data-grid.component.cy.ts @@ -0,0 +1,29 @@ +import { E2eVariations } from '@skyux-sdk/e2e-schematics'; + +describe('data-grid', () => { + E2eVariations.forEachTheme((theme) => { + describe(`in ${theme} theme`, () => { + beforeEach(() => + cy.visit( + `/iframe.html?globals=theme:${theme}&id=data-gridcomponent--data-grid`, + ), + ); + + it('should render the component', () => { + cy.skyReady(); + cy.get('app-data-grid').should('exist').should('be.visible'); + cy.get( + '[row-index="6"] [col-id="jobTitle"] sky-ag-grid-cell-renderer-template', + ) + .should('exist') + .should('be.visible') + .should('have.text', 'UX Designer'); + cy.get('[row-index="6"] [col-id="context"] button.sky-dropdown-button') + .should('exist') + .should('be.visible') + .should('be.enabled'); + cy.skyVisualTest(`data-grid-${theme}`); + }); + }); + }); +}); diff --git a/apps/e2e/data-grid-storybook-e2e/src/support/commands.ts b/apps/e2e/data-grid-storybook-e2e/src/support/commands.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/e2e/data-grid-storybook-e2e/src/support/e2e.ts b/apps/e2e/data-grid-storybook-e2e/src/support/e2e.ts new file mode 100644 index 0000000000..939a57c1a2 --- /dev/null +++ b/apps/e2e/data-grid-storybook-e2e/src/support/e2e.ts @@ -0,0 +1,19 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** +// Import commands.ts using ES2015 syntax: +import '@percy/cypress'; +import '@skyux-sdk/cypress-commands'; + +import './commands'; diff --git a/apps/e2e/data-grid-storybook-e2e/tsconfig.json b/apps/e2e/data-grid-storybook-e2e/tsconfig.json new file mode 100644 index 0000000000..479152932e --- /dev/null +++ b/apps/e2e/data-grid-storybook-e2e/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "sourceMap": false, + "outDir": "../../../dist/out-tsc", + "allowJs": true, + "types": ["cypress", "node"] + }, + "include": ["src/**/*.ts", "src/**/*.js", "cypress.config.ts"] +} diff --git a/apps/e2e/data-grid-storybook/.storybook/main.ts b/apps/e2e/data-grid-storybook/.storybook/main.ts new file mode 100644 index 0000000000..24ca0ad7a2 --- /dev/null +++ b/apps/e2e/data-grid-storybook/.storybook/main.ts @@ -0,0 +1,10 @@ +import type { StorybookConfig } from 'storybook/internal/types'; + +// eslint-disable-next-line @nx/enforce-module-boundaries +import { rootMain } from '../../../../.storybook/main'; + +const config: StorybookConfig = { + ...rootMain, + stories: ['../src/app/**/*.stories.@(js|ts)'], +}; +export default config; diff --git a/apps/e2e/data-grid-storybook/.storybook/manager.ts b/apps/e2e/data-grid-storybook/.storybook/manager.ts new file mode 100644 index 0000000000..7db9e198db --- /dev/null +++ b/apps/e2e/data-grid-storybook/.storybook/manager.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @nx/enforce-module-boundaries, no-restricted-syntax +export * from '../../../../.storybook/manager'; diff --git a/apps/e2e/data-grid-storybook/.storybook/preview.ts b/apps/e2e/data-grid-storybook/.storybook/preview.ts new file mode 100644 index 0000000000..10230a8523 --- /dev/null +++ b/apps/e2e/data-grid-storybook/.storybook/preview.ts @@ -0,0 +1,21 @@ +import { + previewWrapperDecorators, + previewWrapperGlobalTypes, + previewWrapperParameters, +} from '@skyux/storybook'; +import { moduleMetadata } from '@storybook/angular'; + +export const parameters = { + ...previewWrapperParameters, +}; + +export const globalTypes = { + ...previewWrapperGlobalTypes, +}; + +export const decorators = [ + ...previewWrapperDecorators, + moduleMetadata({ + imports: [], + }), +]; diff --git a/apps/e2e/data-grid-storybook/.storybook/tsconfig.json b/apps/e2e/data-grid-storybook/.storybook/tsconfig.json new file mode 100644 index 0000000000..f64fa3a2a2 --- /dev/null +++ b/apps/e2e/data-grid-storybook/.storybook/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "resolveJsonModule": true + }, + "exclude": ["../**/*.spec.ts"], + "include": [ + "../src/**/*.stories.ts", + "../src/**/*.stories.js", + "../src/**/*.stories.jsx", + "../src/**/*.stories.tsx", + + "*.ts", + "*.js", + "./*" + ] +} diff --git a/apps/e2e/data-grid-storybook/eslint.config.js b/apps/e2e/data-grid-storybook/eslint.config.js new file mode 100644 index 0000000000..c9958e222d --- /dev/null +++ b/apps/e2e/data-grid-storybook/eslint.config.js @@ -0,0 +1,3 @@ +const config = require('../../../eslint-storybook.config'); + +module.exports = config; diff --git a/apps/e2e/data-grid-storybook/project.json b/apps/e2e/data-grid-storybook/project.json new file mode 100644 index 0000000000..5b3a5c1238 --- /dev/null +++ b/apps/e2e/data-grid-storybook/project.json @@ -0,0 +1,146 @@ +{ + "name": "data-grid-storybook", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "sourceRoot": "apps/e2e/data-grid-storybook/src", + "prefix": "app", + "tags": ["component-e2e"], + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/apps/e2e/data-grid-storybook" + }, + "index": "apps/e2e/data-grid-storybook/src/index.html", + "browser": "apps/e2e/data-grid-storybook/src/main.ts", + "polyfills": ["zone.js", "libs/components/packages/src/polyfills.js"], + "tsConfig": "apps/e2e/data-grid-storybook/tsconfig.app.json", + "inlineStyleLanguage": "scss", + "stylePreprocessorOptions": { + "includePaths": ["{workspaceRoot}"] + }, + "styles": [ + "apps/e2e/data-grid-storybook/src/styles.scss", + "libs/components/theme/src/lib/styles/sky.scss", + "libs/components/theme/src/lib/styles/themes/modern/styles.scss", + "libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb", + "maximumError": "10kb" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "data-grid-storybook:build:production" + }, + "development": { + "buildTarget": "data-grid-storybook:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "executor": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "data-grid-storybook:build" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "options": { + "lintFilePatterns": ["{projectRoot}/**/*.ts", "{projectRoot}/**/*.html"] + } + }, + "storybook": { + "executor": "@storybook/angular:start-storybook", + "options": { + "port": 4400, + "configDir": "apps/e2e/data-grid-storybook/.storybook", + "browserTarget": "data-grid-storybook:build", + "compodoc": false, + "styles": [ + "apps/e2e/data-grid-storybook/src/styles.scss", + "libs/components/theme/src/lib/styles/sky.scss", + "libs/components/theme/src/lib/styles/themes/modern/styles.scss", + "libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss" + ] + }, + "configurations": { + "ci": { + "quiet": true, + "ci": true + } + } + }, + "build-storybook": { + "executor": "@storybook/angular:build-storybook", + "outputs": ["{options.outputDir}"], + "options": { + "outputDir": "dist/storybook/data-grid-storybook", + "configDir": "apps/e2e/data-grid-storybook/.storybook", + "browserTarget": "data-grid-storybook:build", + "compodoc": false, + "styles": [ + "apps/e2e/data-grid-storybook/src/styles.scss", + "libs/components/theme/src/lib/styles/sky.scss", + "libs/components/theme/src/lib/styles/themes/modern/styles.scss", + "libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss" + ] + }, + "configurations": { + "ci": { + "quiet": true + } + } + }, + "static-storybook": { + "executor": "@nx/web:file-server", + "dependsOn": ["build-storybook"], + "options": { + "buildTarget": "data-grid-storybook:build-storybook", + "staticFilePath": "dist/storybook/data-grid-storybook", + "spa": true + }, + "configurations": { + "ci": { + "buildTarget": "data-grid-storybook:build-storybook:ci" + }, + "prebuilt": { + "buildTarget": "data-grid-storybook:noop" + } + } + }, + "noop": { + "executor": "nx:noop" + } + } +} diff --git a/apps/e2e/data-grid-storybook/src/app/app.component.ts b/apps/e2e/data-grid-storybook/src/app/app.component.ts new file mode 100644 index 0000000000..c4687cab76 --- /dev/null +++ b/apps/e2e/data-grid-storybook/src/app/app.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + template: ``, + imports: [RouterOutlet], +}) +export class AppComponent {} diff --git a/apps/e2e/data-grid-storybook/src/app/app.routes.ts b/apps/e2e/data-grid-storybook/src/app/app.routes.ts new file mode 100644 index 0000000000..b71e35cb43 --- /dev/null +++ b/apps/e2e/data-grid-storybook/src/app/app.routes.ts @@ -0,0 +1,13 @@ +import { Route } from '@angular/router'; + +export const routes: Route[] = [ + { + path: '', + pathMatch: 'full', + redirectTo: 'data-grid', + }, + { + path: 'data-grid', + loadComponent: () => import('./data-grid/data-grid.component'), + }, +]; diff --git a/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.html b/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.html new file mode 100644 index 0000000000..67e54343e1 --- /dev/null +++ b/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.html @@ -0,0 +1,72 @@ +
+ + + + + + + + {{ row.department?.name }} + + + {{ row.jobTitle?.name }} + + +
+ + + + + + + + + + + + + + + + diff --git a/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.stories.ts b/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.stories.ts new file mode 100644 index 0000000000..e2e586ecc1 --- /dev/null +++ b/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.stories.ts @@ -0,0 +1,12 @@ +import type { Meta, StoryObj } from '@storybook/angular'; + +import { DataGridComponent } from './data-grid.component'; + +export default { + id: 'data-gridcomponent', + title: 'Components/Data Grid', + component: DataGridComponent, +} as Meta; +type Story = StoryObj; +export const DataGrid: Story = {}; +DataGrid.args = {}; diff --git a/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.ts b/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.ts new file mode 100644 index 0000000000..d1b09b9bb2 --- /dev/null +++ b/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyDataGridModule } from '@skyux/data-grid'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { DATA_GRID_DEMO_DATA, DataGridDemoRow } from './data'; + +/** + * @title Basic data grid + */ +@Component({ + selector: 'app-data-grid', + templateUrl: './data-grid.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDataGridModule, SkyDropdownModule], +}) +export class DataGridComponent { + protected gridData: DataGridDemoRow[] = DATA_GRID_DEMO_DATA; + + public actionClicked(row: DataGridDemoRow, action: string): void { + alert(`${action} clicked for ${row.name}`); + } +} +export default DataGridComponent; diff --git a/apps/e2e/data-grid-storybook/src/app/data-grid/data.ts b/apps/e2e/data-grid-storybook/src/app/data-grid/data.ts new file mode 100644 index 0000000000..25ebd17408 --- /dev/null +++ b/apps/e2e/data-grid-storybook/src/app/data-grid/data.ts @@ -0,0 +1,157 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface DataGridDemoRow { + id: string; + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const DATA_GRID_DEMO_DATA = [ + { + id: '1', + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + id: '2', + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + id: '3', + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + }, + { + id: '4', + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + id: '5', + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + id: '6', + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + id: '7', + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/apps/e2e/data-grid-storybook/src/index.html b/apps/e2e/data-grid-storybook/src/index.html new file mode 100644 index 0000000000..35c642541d --- /dev/null +++ b/apps/e2e/data-grid-storybook/src/index.html @@ -0,0 +1,12 @@ + + + + + data-grid-storybook + + + + + + + diff --git a/apps/e2e/data-grid-storybook/src/main.ts b/apps/e2e/data-grid-storybook/src/main.ts new file mode 100644 index 0000000000..3a19f1ac8d --- /dev/null +++ b/apps/e2e/data-grid-storybook/src/main.ts @@ -0,0 +1,18 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { + provideRouter, + withEnabledBlockingInitialNavigation, +} from '@angular/router'; +import { provideInitialTheme } from '@skyux/theme'; + +import { AppComponent } from './app/app.component'; +import { routes } from './app/app.routes'; + +bootstrapApplication(AppComponent, { + providers: [ + provideInitialTheme('modern'), + provideAnimationsAsync(), + provideRouter(routes, withEnabledBlockingInitialNavigation()), + ], +}).catch((err) => console.error(err)); diff --git a/apps/e2e/data-grid-storybook/src/styles.scss b/apps/e2e/data-grid-storybook/src/styles.scss new file mode 100644 index 0000000000..90d4ee0072 --- /dev/null +++ b/apps/e2e/data-grid-storybook/src/styles.scss @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/apps/e2e/data-grid-storybook/tsconfig.app.json b/apps/e2e/data-grid-storybook/tsconfig.app.json new file mode 100644 index 0000000000..ea3e05c0c5 --- /dev/null +++ b/apps/e2e/data-grid-storybook/tsconfig.app.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [], + "esModuleInterop": true, + "resolveJsonModule": true + }, + "include": ["src/**/*.ts"], + "exclude": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.stories.ts", + "**/*.stories.js" + ] +} diff --git a/apps/e2e/data-grid-storybook/tsconfig.json b/apps/e2e/data-grid-storybook/tsconfig.json new file mode 100644 index 0000000000..9b0b81e919 --- /dev/null +++ b/apps/e2e/data-grid-storybook/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "resolveJsonModule": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./.storybook/tsconfig.json" + } + ] +} diff --git a/apps/playground/src/app/components/components.module.ts b/apps/playground/src/app/components/components.module.ts index 0e542c804f..7171592020 100644 --- a/apps/playground/src/app/components/components.module.ts +++ b/apps/playground/src/app/components/components.module.ts @@ -75,6 +75,16 @@ export const componentRoutes: Routes = [ loadChildren: () => import('./forms/forms.module').then((m) => m.FormsModule), }, + { + path: 'grids', + loadChildren: () => + import('./grids/grids.module').then((m) => m.GridsModule), + }, + { + path: 'data-grid', + loadChildren: () => + import('./data-grid/data-grid.module').then((m) => m.DataGridModule), + }, { path: 'help-inline', loadChildren: () => @@ -92,6 +102,13 @@ export const componentRoutes: Routes = [ loadChildren: () => import('./layout/layout.module').then((m) => m.LayoutModule), }, + { + path: 'list-builder', + loadChildren: () => + import('./list-builder/list-builder.module').then( + (m) => m.ListBuilderModule, + ), + }, { path: 'lists', loadChildren: () => diff --git a/apps/playground/src/app/components/data-grid/basic/grid.component.html b/apps/playground/src/app/components/data-grid/basic/grid.component.html new file mode 100644 index 0000000000..23a7dd2519 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/basic/grid.component.html @@ -0,0 +1,191 @@ +

Grid

+ + +
+ + + + @if (!hideCol3()) { + + } + +
+ +

Grid with multiselect enabled

+ + + + + + + +
+ + + + + @if (!hideCol3()) { + + } + + +

Selected rows: {{ selectedRowIds | json }}

+
+ +

Grid with inline help

+ +
+ + + + @if (!hideCol3()) { + + } + + + + This is a numeric column. Click here to learn more. + + + + This is a string column. Click here to learn more. + +
+ +

Grid with scroll bars

+ +
+ + + + @if (!hideCol3()) { + + } + +
+ +

Grid with row delete

+ +
+ + + + + @if (row.id) { + + + + + + + + } + + + + + @if (!hideCol3()) { + + } + + + + This is a numeric column. Click here to learn more. + +
+ +

Grid w/ aligned columns

+ +
+ + + + @if (!hideCol3()) { + + } + +
+ +

Grid w/ aligned columns and inline help

+ +
+ + + + @if (!hideCol3()) { + + } + +
diff --git a/apps/playground/src/app/components/data-grid/basic/grid.component.scss b/apps/playground/src/app/components/data-grid/basic/grid.component.scss new file mode 100644 index 0000000000..cf3267ce27 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/basic/grid.component.scss @@ -0,0 +1,5 @@ +h1 { + // Margins throw off the screenshots; use padding for the same effect. + margin: 0; + padding: 15px 0; +} diff --git a/apps/playground/src/app/components/data-grid/basic/grid.component.ts b/apps/playground/src/app/components/data-grid/basic/grid.component.ts new file mode 100644 index 0000000000..d1bdf5893c --- /dev/null +++ b/apps/playground/src/app/components/data-grid/basic/grid.component.ts @@ -0,0 +1,127 @@ +import { JsonPipe } from '@angular/common'; +import { + ChangeDetectorRef, + Component, + ViewChild, + inject, + model, +} from '@angular/core'; +import { + SkyAgGridRowDeleteCancelArgs, + SkyAgGridRowDeleteConfirmArgs, +} from '@skyux/ag-grid'; +import { + SkyDataGridColumnComponent, + SkyDataGridComponent, +} from '@skyux/data-grid'; +import { SkyDropdownModule, SkyPopoverModule } from '@skyux/popovers'; + +interface RowModel { + id: string; + column1: string; + column2: string; + column3: string; + myId?: string; +} + +@Component({ + selector: 'app-data-grid', + templateUrl: './grid.component.html', + styleUrls: ['./grid.component.scss'], + imports: [ + SkyDataGridComponent, + SkyDataGridColumnComponent, + SkyPopoverModule, + SkyDropdownModule, + JsonPipe, + ], +}) +export default class GridComponent { + public asyncPopover: any; + + public dataForRowDeleteGrid: RowModel[] = [ + { id: '1', column1: '1', column2: 'Apple', column3: 'aa' }, + { id: '2', column1: '01', column2: 'Banana', column3: 'bb' }, + { id: '3', column1: '11', column2: 'Banana', column3: 'cc' }, + { id: '4', column1: '12', column2: 'Daikon', column3: 'dd' }, + { id: '5', column1: '13', column2: 'Edamame', column3: 'ee' }, + { id: '6', column1: '20', column2: 'Fig', column3: 'ff' }, + { id: '7', column1: '21', column2: 'Grape', column3: 'gg' }, + ]; + + public dataForSimpleGrid: RowModel[] = [ + { id: '1', column1: '1', column2: 'Apple', column3: 'aa' }, + { id: '2', column1: '01', column2: 'Banana', column3: 'bb' }, + { id: '3', column1: '11', column2: 'Banana', column3: 'cc' }, + { id: '4', column1: '12', column2: 'Daikon', column3: 'dd' }, + { id: '5', column1: '13', column2: 'Edamame', column3: 'ee' }, + { id: '6', column1: '20', column2: 'Fig', column3: 'ff' }, + { id: '7', column1: '21', column2: 'Grape', column3: 'gg' }, + ]; + + public dataForSimpleGridWithMultiselect: RowModel[] = [ + { id: '1', column1: '1', column2: 'Apple', column3: 'aa', myId: '101' }, + { id: '2', column1: '01', column2: 'Banana', column3: 'bb', myId: '102' }, + { id: '3', column1: '11', column2: 'Banana', column3: 'cc', myId: '103' }, + { id: '4', column1: '12', column2: 'Daikon', column3: 'dd', myId: '104' }, + { id: '5', column1: '13', column2: 'Edamame', column3: 'ee', myId: '105' }, + { id: '6', column1: '20', column2: 'Fig', column3: 'ff', myId: '106' }, + { id: '7', column1: '21', column2: 'Grape', column3: 'gg', myId: '107' }, + ]; + + public selectedRowIds: string[] = []; + + public removeRowIds: string[] = []; + + protected readonly hideCol3 = model(false); + protected toggleCol3(): void { + this.hideCol3.update((show) => !show); + } + + @ViewChild('asyncPopoverRef') + private popoverTemplate: any; + + readonly #cdr = inject(ChangeDetectorRef); + + constructor() { + setTimeout(() => { + this.asyncPopover = this.popoverTemplate; + }, 1000); + } + + public selectAll(): void { + this.selectedRowIds = this.dataForSimpleGridWithMultiselect.map( + (item) => item.myId, + ); + this.#cdr.markForCheck(); + } + + public clearAll(): void { + this.selectedRowIds = []; + this.#cdr.markForCheck(); + } + + public cancelRowDelete(cancelArgs: SkyAgGridRowDeleteCancelArgs): void { + console.log('Item with id ' + cancelArgs.id + ' has not been deleted'); + } + + public deleteItem(id: string): void { + this.removeRowIds = [id, ...this.removeRowIds]; + } + + public finishRowDelete(confirmArgs: SkyAgGridRowDeleteConfirmArgs): void { + setTimeout(() => { + console.log('Item with id ' + confirmArgs.id + ' has been deleted'); + // IF WORKED + this.dataForRowDeleteGrid = this.dataForRowDeleteGrid.filter( + (data: any) => data.id !== confirmArgs.id, + ); + this.#cdr.markForCheck(); + }, 5000); + } + + public selectRow(): void { + this.selectedRowIds = ['101', '103', '105']; + this.#cdr.markForCheck(); + } +} diff --git a/apps/playground/src/app/components/data-grid/data-grid.module.ts b/apps/playground/src/app/components/data-grid/data-grid.module.ts new file mode 100644 index 0000000000..55b09df48e --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-grid.module.ts @@ -0,0 +1,67 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { ComponentRouteInfo } from '../../shared/component-info/component-route-info'; + +const routes: ComponentRouteInfo[] = [ + { + path: 'basic', + loadComponent: () => import('./basic/grid.component'), + data: { + name: 'Data Grid', + icon: 'table', + library: 'data-grid', + }, + }, + { + path: 'data-manager', + loadComponent: () => + import('./data-manager-large/data-manager-large.component'), + data: { + name: 'Data Grid w/ data-manager', + icon: 'table', + library: 'data-grid', + }, + }, + { + path: 'data-manager-in-modal', + loadComponent: () => + import('./data-manager-large/data-manager-large-in-modal.component'), + data: { + name: 'Data Grid w/ data-manager, in a modal', + icon: 'table', + library: 'data-grid', + }, + }, + { + path: 'paging', + loadComponent: () => import('./paging/grid-paging.component'), + data: { + name: 'Data Grid Paging', + icon: 'table', + library: 'data-grid', + }, + }, + { + path: 'filtered', + loadComponent: () => import('./filtered/grid.component'), + data: { + name: 'Data Grid Filtered', + icon: 'filter', + library: 'data-grid', + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +class DataGridRoutingModule {} + +@NgModule({ + imports: [DataGridRoutingModule], +}) +export class DataGridModule { + public static routes = routes; +} diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/custom-link/custom-link.component.html b/apps/playground/src/app/components/data-grid/data-manager-large/custom-link/custom-link.component.html new file mode 100644 index 0000000000..35a49a8efc --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-manager-large/custom-link/custom-link.component.html @@ -0,0 +1,3 @@ +@if (link) { + {{ link() }} +} diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/custom-link/custom-link.component.ts b/apps/playground/src/app/components/data-grid/data-manager-large/custom-link/custom-link.component.ts new file mode 100644 index 0000000000..9d129cfd5b --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-manager-large/custom-link/custom-link.component.ts @@ -0,0 +1,21 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +@Component({ + selector: 'app-custom-link', + templateUrl: './custom-link.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [ + ` + :host { + display: block; + text-overflow: ellipsis; + word-break: break-all; + overflow: hidden; + white-space: nowrap; + } + `, + ], +}) +export class CustomLinkComponent { + public readonly link = input(); +} diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-in-modal.component.html b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-in-modal.component.html new file mode 100644 index 0000000000..c94895bb57 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-in-modal.component.html @@ -0,0 +1,31 @@ + + +

Modal

+
+ + + + + + + + +
diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-in-modal.component.ts b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-in-modal.component.ts new file mode 100644 index 0000000000..255c172566 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-in-modal.component.ts @@ -0,0 +1,67 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { + SkyModalInstance, + SkyModalModule, + SkyModalService, +} from '@skyux/modals'; + +import { DataManagerLargeComponent } from './data-manager-large.component'; + +@Component({ + selector: 'app-data-manager-large-in-modal-modal', + templateUrl: './data-manager-large-in-modal.component.html', + imports: [SkyModalModule], +}) +export class DataManagerLargeInModalModalComponent { + protected readonly modal = inject(SkyModalInstance); + readonly #modalService = inject(SkyModalService); + + public openGridModal(): void { + this.#modalService.open(DataManagerLargeInModalModalGridComponent, { + size: 'large', + }); + } + + public openNotGridModal(): void { + this.#modalService.open(DataManagerLargeInModalModalNotGridComponent, { + size: 'large', + }); + } +} + +@Component({ + selector: 'app-data-manager-large-in-modal-modal-grid', + template: ` + + + + `, + imports: [DataManagerLargeComponent, DataManagerLargeInModalModalComponent], +}) +export class DataManagerLargeInModalModalGridComponent {} + +@Component({ + selector: 'app-data-manager-large-in-modal-modal-not-grid', + template: ` + +

Not a grid.

+
+ `, + imports: [DataManagerLargeInModalModalComponent], +}) +export class DataManagerLargeInModalModalNotGridComponent {} + +@Component({ + selector: 'app-data-manager-large-in-modal', + template: '', +}) +export class DataManagerLargeInModalComponent implements OnInit { + readonly #modalService = inject(SkyModalService); + + public ngOnInit(): void { + this.#modalService.open(DataManagerLargeInModalModalGridComponent, { + size: 'large', + }); + } +} +export default DataManagerLargeInModalComponent; diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-routes.ts b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-routes.ts new file mode 100644 index 0000000000..ed78baccfd --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-routes.ts @@ -0,0 +1,28 @@ +import { ComponentRouteInfo } from '../../../shared/component-info/component-route-info'; + +import { DataManagerLargeInModalComponent } from './data-manager-large-in-modal.component'; +import { DataManagerLargeComponent } from './data-manager-large.component'; + +const routes: ComponentRouteInfo[] = [ + { + path: '', + pathMatch: 'full', + component: DataManagerLargeComponent, + data: { + name: 'Data Grid (data manager large)', + library: 'data-grid', + icon: 'table', + }, + }, + { + path: 'in-modal', + component: DataManagerLargeInModalComponent, + data: { + name: 'Data Grid (data manager large, in modal)', + library: 'data-grid', + icon: 'table', + }, + }, +]; + +export default routes; diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.html b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.html new file mode 100644 index 0000000000..561a7f7202 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.html @@ -0,0 +1,273 @@ + + + +
+ + + + + + + + + + +
+
+ + + + @if (ready()) { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } +
+ + + + + + + + + diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.scss b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.scss new file mode 100644 index 0000000000..f259ee1110 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.scss @@ -0,0 +1,27 @@ +app-data-manager-large.use-normal-dom-layout { + position: relative; + top: 0; + left: 0; + width: 100%; + height: calc(100vh - 80px); + display: block; + + sky-data-manager, + sky-data-view { + display: block; + height: 100%; + width: 100%; + } + + sky-data-manager > .sky-data-manager { + display: flex; + flex-direction: column; + flex-basis: fit-content; + height: 100%; + width: 100%; + } +} + +form > * { + display: inline-block; +} diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.ts b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.ts new file mode 100644 index 0000000000..92c19bd9fa --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.ts @@ -0,0 +1,298 @@ +import { + Component, + Signal, + ViewEncapsulation, + computed, + effect, + inject, + linkedSignal, + model, + signal, + untracked, +} from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyAgGridModule, SkyAgGridRowDeleteConfirmArgs } from '@skyux/ag-grid'; +import { SkyLogService, SkyUIConfigService } from '@skyux/core'; +import { SkyDataGridFilterValue, SkyDataGridModule } from '@skyux/data-grid'; +import { + SkyDataManagerConfig, + SkyDataManagerModule, + SkyDataManagerService, + SkyDataManagerState, +} from '@skyux/data-manager'; +import { + SkyFilterBarFilterItem, + SkyFilterItemLookupSearchAsyncArgs, +} from '@skyux/filter-bar'; +import { SkyFilterBarModule } from '@skyux/filter-bar'; +import { SkyCheckboxModule, SkyRadioModule } from '@skyux/forms'; +import { SkyHelpInlineModule } from '@skyux/help-inline'; +import { SkyIconModule } from '@skyux/icon'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { BehaviorSubject, of } from 'rxjs'; +import { take } from 'rxjs/operators'; + +import { CustomLinkComponent } from './custom-link/custom-link.component'; +import { data } from './data-set-large'; +import { LocalStorageConfigService } from './local-storage-config.service'; + +interface GridSettingsType { + enableTopScroll: FormControl; + domLayout: FormControl<'normal' | 'autoHeight' | 'print'>; + compact: FormControl; + showSelect: FormControl; + showDelete: FormControl; + wrapText: FormControl; +} + +@Component({ + selector: 'app-data-manager-large', + templateUrl: './data-manager-large.component.html', + styleUrls: ['./data-manager-large.component.scss'], + encapsulation: ViewEncapsulation.None, + imports: [ + SkyAgGridModule, + SkyDataGridModule, + SkyDataManagerModule, + SkyDropdownModule, + SkyCheckboxModule, + SkyIconModule, + SkyRadioModule, + ReactiveFormsModule, + SkyHelpInlineModule, + CustomLinkComponent, + SkyFilterBarModule, + ], + providers: [ + SkyDataManagerService, + { + provide: SkyUIConfigService, + useClass: LocalStorageConfigService, + }, + ], +}) +export class DataManagerLargeComponent { + public dataManagerConfig: SkyDataManagerConfig = {}; + + public defaultDataState = new SkyDataManagerState({ + additionalData: { + compact: false, + domLayout: 'autoHeight', + enableTopScroll: true, + showDelete: true, + showSelect: true, + version: 2, + }, + filterData: { + filtersApplied: false, + filters: {}, + }, + views: [ + { + viewId: 'gridView', + displayedColumnIds: [ + 'credit_line', + 'object_date', + 'title', + 'culture', + 'artist_display_name', + /* spell-checker:disable-next-line */ + 'artist_display_bio', + 'accessionyear', + 'object_wikidata_url', + 'link_resource', + 'country', + 'department', + 'dimensions', + 'gallery_number', + 'geography_type', + 'medium', + 'menu', + 'object_name', + 'object_begin_date', + 'object_date_1', + 'object_end_date', + 'object_number', + ], + }, + ], + }); + + public readonly viewId = 'gridView'; + + public dataState: SkyDataManagerState | undefined; + public readonly items = signal( + data.map((item) => ({ + id: item.object_id, + ...item, + })), + ); + public readonly settingsKey = 'data-grid-large-test'; + public readonly isActive$ = new BehaviorSubject(true); + public readonly gridSettings: FormGroup; + public readonly gridSettingsChanges: Signal; + public readonly enableTopScroll: Signal; + public readonly dataManagerStateUpdates: Signal; + public readonly dataManagerStateGridSettings: Signal< + typeof this.gridSettings.value + >; + public readonly dataManagerStateShowDelete: Signal; + + protected readonly appliedFilters = + model[]>(); + protected readonly height = computed(() => + this.gridSettingsChanges()?.domLayout === 'normal' ? 500 : undefined, + ); + protected readonly ready = signal(true); + protected readonly rowDeleteIds = model([]); + + readonly #dataManagerService = inject(SkyDataManagerService); + readonly #logger = inject(SkyLogService); + + constructor() { + const formBuilder = inject(FormBuilder); + this.gridSettings = formBuilder.group({ + enableTopScroll: formBuilder.nonNullable.control(true), + showSelect: formBuilder.nonNullable.control(true), + showDelete: formBuilder.nonNullable.control(true), + domLayout: formBuilder.nonNullable.control('autoHeight'), + compact: formBuilder.nonNullable.control(false), + wrapText: formBuilder.nonNullable.control(false), + }); + this.gridSettingsChanges = toSignal(this.gridSettings.valueChanges, { + initialValue: this.gridSettings.value, + }); + this.enableTopScroll = computed( + () => this.gridSettingsChanges().enableTopScroll, + ); + effect(() => { + this.enableTopScroll(); + untracked(() => { + this.ready.set(false); + setTimeout(() => this.ready.set(true), 10); + }); + }); + + this.#dataManagerService.initDataManager({ + activeViewId: 'gridView', + dataManagerConfig: this.dataManagerConfig, + defaultDataState: this.defaultDataState, + settingsKey: this.settingsKey, + }); + + this.#dataManagerService.initDataView({ + id: this.viewId, + name: 'Grid View', + iconName: 'table', + searchEnabled: true, + columnPickerEnabled: true, + }); + + this.dataManagerStateUpdates = toSignal( + this.#dataManagerService.getDataStateUpdates('gridSettings'), + ); + this.dataManagerStateGridSettings = linkedSignal< + SkyDataManagerState, + typeof this.gridSettings.value + >({ + source: () => this.dataManagerStateUpdates(), + computation: (state) => + state.additionalData ?? { + compact: false, + domLayout: 'autoHeight', + enableTopScroll: true, + showDelete: true, + showSelect: true, + wrapText: false, + }, + equal: (a, b) => JSON.stringify(a) === JSON.stringify(b), + }); + this.dataManagerStateShowDelete = computed(() => + this.dataManagerStateUpdates().views[0].displayedColumnIds.includes( + 'menu', + ), + ); + effect(() => { + const gridSettings = this.gridSettingsChanges(); + const state = new SkyDataManagerState( + untracked(this.dataManagerStateUpdates), + ); + if (gridSettings.showDelete) { + state.views[0].displayedColumnIds = [ + ...new Set(['menu', ...state.views[0].displayedColumnIds]), + ]; + } else { + state.views[0].displayedColumnIds = + state.views[0].displayedColumnIds.filter((col) => col !== 'menu'); + } + state.additionalData = { + ...state.additionalData, + ...gridSettings, + }; + this.#dataManagerService.updateDataState(state, 'gridSettings'); + }); + effect(() => { + const stateGridSettings = this.dataManagerStateGridSettings(); + this.gridSettings.setValue({ + compact: stateGridSettings.compact, + domLayout: stateGridSettings.domLayout, + enableTopScroll: stateGridSettings.enableTopScroll, + showDelete: stateGridSettings.showDelete, + showSelect: stateGridSettings.showSelect, + wrapText: stateGridSettings.wrapText, + }); + }); + effect(() => { + const showDelete = this.dataManagerStateShowDelete(); + this.gridSettings.controls.showDelete.setValue(showDelete); + }); + + this.#dataManagerService + .getDataStateUpdates('version-update') + .pipe(take(1)) + .subscribe((state) => { + if (state.additionalData?.version !== 2) { + const update = new SkyDataManagerState(state); + update.additionalData ??= {}; + update.additionalData.version = + this.defaultDataState.additionalData.version; + const gridViewDefault = this.defaultDataState.views[0]; + const gridView = state.views[0]; + gridView.displayedColumnIds = gridViewDefault.displayedColumnIds; + this.#dataManagerService.updateDataState(update, 'version-update'); + } + }); + } + + public markForDelete(rowId: string): void { + this.rowDeleteIds.update((rowIds) => { + return [...new Set([...rowIds, rowId])]; + }); + } + + protected deleteConfirm($event: SkyAgGridRowDeleteConfirmArgs): void { + this.items.update((items) => items.filter((item) => item.id !== $event.id)); + } + + protected searchCulture(search: SkyFilterItemLookupSearchAsyncArgs): void { + const options = [...new Set(this.items().map((item) => item.culture))] + .filter(Boolean) + .sort((a, b) => a.localeCompare(b)); + search.result = of({ + items: options.map((culture) => Object.assign(`${culture}`, { culture })), + totalCount: options.length, + }); + } + + protected selectionChange($event: string[]): void { + this.#logger.info(`selectionChange:`, [$event]); + } +} +export default DataManagerLargeComponent; diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-set-large.ts b/apps/playground/src/app/components/data-grid/data-manager-large/data-set-large.ts new file mode 100644 index 0000000000..9926dd9542 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-set-large.ts @@ -0,0 +1,5913 @@ +// Source: https://github.com/metmuseum/openaccess +import { SkyCellType } from '@skyux/ag-grid'; + +import { ColDef } from 'ag-grid-community'; + +/* spell-checker:disable */ +export const columnDefinitions: ColDef[] = [ + { + field: 'object_number', + headerName: 'Object Number', + type: SkyCellType.Text, + sortable: false, + wrapText: true, + autoHeight: true, + }, + { + field: 'is_highlight', + headerName: 'Is Highlight', + type: [], + sortable: true, + }, + { + field: 'is_timeline_work', + headerName: 'Is Timeline Work', + type: [], + sortable: true, + }, + { + field: 'is_public_domain', + headerName: 'Is Public Domain', + type: [], + sortable: true, + }, + { + field: 'object_id', + headerName: 'Object ID', + type: [], + sortable: true, + }, + { + field: 'gallery_number', + headerName: 'Gallery Number', + type: [], + sortable: true, + }, + { + field: 'department', + headerName: 'Department', + type: [], + sortable: true, + }, + { + field: 'accessionyear', + headerName: 'AccessionYear', + type: [], + sortable: false, + }, + { + field: 'object_name', + headerName: 'Object Name', + type: [], + sortable: true, + }, + { + field: 'title', + headerName: 'Title', + type: [], + sortable: false, + }, + { + field: 'culture', + headerName: 'Culture', + type: [], + sortable: true, + }, + { + field: 'period', + headerName: 'Period', + type: [], + sortable: true, + }, + { + field: 'dynasty', + headerName: 'Dynasty', + type: [], + sortable: true, + }, + { + field: 'reign', + headerName: 'Reign', + type: [], + sortable: true, + }, + { + field: 'portfolio', + headerName: 'Portfolio', + type: [], + sortable: true, + }, + { + field: 'constituent_id', + headerName: 'Constituent ID', + type: [], + sortable: true, + }, + { + field: 'artist_role', + headerName: 'Artist Role', + type: [], + sortable: true, + }, + { + field: 'artist_prefix', + headerName: 'Artist Prefix', + type: [], + sortable: true, + }, + { + field: 'artist_display_name', + headerName: 'Artist Display Name', + type: [], + sortable: true, + }, + { + field: 'artist_display_bio', + headerName: 'Artist Display Bio', + type: [], + sortable: true, + }, + { + field: 'artist_suffix', + headerName: 'Artist Suffix', + type: [], + sortable: true, + }, + { + field: 'artist_alpha_sort', + headerName: 'Artist Alpha Sort', + type: [], + sortable: true, + }, + { + field: 'artist_nationality', + headerName: 'Artist Nationality', + type: [], + sortable: true, + }, + { + field: 'artist_begin_date', + headerName: 'Artist Begin Date', + type: [], + sortable: true, + }, + { + field: 'artist_end_date', + headerName: 'Artist End Date', + type: [], + sortable: true, + }, + { + field: 'artist_gender', + headerName: 'Artist Gender', + type: [], + sortable: true, + }, + { + field: 'artist_ulan_url', + headerName: 'Artist ULAN URL', + type: ['custom_link'], + sortable: true, + }, + { + field: 'artist_wikidata_url', + headerName: 'Artist Wikidata URL', + type: ['custom_link'], + sortable: true, + }, + { + field: 'object_date', + headerName: 'Object Date', + type: [], + sortable: true, + }, + { + field: 'object_begin_date', + headerName: 'Object Begin Date', + type: [], + sortable: true, + }, + { + field: 'object_end_date', + headerName: 'Object End Date', + type: [], + sortable: true, + }, + { + field: 'medium', + headerName: 'Medium', + type: [], + sortable: true, + }, + { + field: 'dimensions', + headerName: 'Dimensions', + type: [], + sortable: true, + }, + { + field: 'credit_line', + headerName: 'Credit Line', + type: SkyCellType.Text, + sortable: true, + }, + { + field: 'geography_type', + headerName: 'Geography Type', + type: [], + sortable: true, + }, + { + field: 'city', + headerName: 'City', + type: [], + sortable: true, + }, + { + field: 'state', + headerName: 'State', + type: [], + sortable: true, + }, + { + field: 'county', + headerName: 'County', + type: [], + sortable: true, + }, + { + field: 'country', + headerName: 'Country', + type: [], + sortable: true, + }, + { + field: 'region', + headerName: 'Region', + type: [], + sortable: true, + }, + { + field: 'subregion', + headerName: 'Subregion', + type: [], + sortable: true, + }, + { + field: 'locale', + headerName: 'Locale', + type: [], + sortable: true, + }, + { + field: 'locus', + headerName: 'Locus', + type: [], + sortable: true, + }, + { + field: 'excavation', + headerName: 'Excavation', + type: [], + sortable: true, + }, + { + field: 'river', + headerName: 'River', + type: [], + sortable: true, + }, + { + field: 'classification', + headerName: 'Classification', + type: [], + sortable: true, + }, + { + field: 'rights_and_reproduction', + headerName: 'Rights and Reproduction', + type: [], + sortable: true, + }, + { + field: 'link_resource', + headerName: 'Link Resource', + type: ['custom_link'], + sortable: true, + }, + { + field: 'object_wikidata_url', + headerName: 'Object Wikidata URL', + type: ['custom_link'], + sortable: true, + }, + { + field: 'metadata_date', + headerName: 'Metadata Date', + type: [], + sortable: true, + }, + { + field: 'repository', + headerName: 'Repository', + type: [], + sortable: true, + }, + { + field: 'tags', + headerName: 'Tags', + type: [], + sortable: true, + }, + { + field: 'tags_aat_url', + headerName: 'Tags AAT URL', + type: ['custom_link'], + sortable: true, + }, + { + field: 'tags_wikidata_url', + headerName: 'Tags Wikidata URL', + type: ['custom_link'], + sortable: true, + }, +]; + +export const data = [ + { + object_number: '24.109.38a\u2013c', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1153', + gallery_number: '704', + department: 'The American Wing', + accessionyear: '1924', + object_name: 'Candlestick', + title: 'Candle Holder', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '130', + artist_role: 'Maker', + artist_prefix: ' ', + artist_display_name: 'Joseph Lownes', + artist_display_bio: '1758\u20131820', + artist_suffix: ' ', + artist_alpha_sort: 'Lownes, Joseph', + artist_nationality: ' ', + artist_begin_date: '1758 ', + artist_end_date: '1820 ', + artist_gender: '', + artist_ulan_url: 'http://vocab.getty.edu/page/ulan/500330248', + artist_wikidata_url: '', + object_date: '1790\u20131810', + object_begin_date: '1790', + object_end_date: '1810', + medium: 'Silver, steel', + dimensions: + 'Overall: 5 1/16 x 9 5/8 x 4 3/4 in. (12.9 x 24.4 x 12.1 cm); 17 oz. 17 dwt. (556.5 g)\r\n44.12.13,False,False,True,1170,,The American Wing,1944,Candlestick,Standing candlestick,,,,,,,,,,,,,,,,,,,1730\u201350,1730,1750,Brass and iron,H. 62 in. (157.5 cm),Bequest of Sarah Williams', + credit_line: ' 1944"', + geography_type: 'Possibly made in', + city: 'Boston', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1170', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '11.87.78', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1183', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1911', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'Mexican', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1800', + object_begin_date: '1797', + object_end_date: '1800', + medium: 'Tin-glazed earthenware', + dimensions: 'H. 15 1/2 in. (39.4 cm)', + credit_line: 'Gift of Mrs. Robert W. de Forest, 1911', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'Mexico', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1183', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Dogs', + tags_aat_url: 'http://vocab.getty.edu/page/aat/300265714', + tags_wikidata_url: 'https://www.wikidata.org/wiki/Q144', + }, + { + object_number: '18.95.4', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1184', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1918', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1800\u20131830', + object_begin_date: '1800', + object_end_date: '1830', + medium: 'Earthenware', + dimensions: 'H. 4 in. (10.2 cm); Diam. 5 in. (12.7 cm)', + credit_line: 'Rogers Fund, 1918', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1184', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '20.14.7', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1185', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1920', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1700\u20131800', + object_begin_date: '1700', + object_end_date: '1800', + medium: 'Free-blown lead aquamarine glass', + dimensions: 'H. 10 1/4 in. (26 cm)', + credit_line: 'Rogers Fund, 1920', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1185', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '35.43.1', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1186', + gallery_number: '', + department: 'The American Wing', + accessionyear: '1935', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1650', + object_begin_date: '1647', + object_end_date: '1650', + medium: 'Brass', + dimensions: 'H. 8 1/4 in. (21 cm)', + credit_line: 'Rogers Fund, 1935', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1186', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '35.43.2', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1187', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1935', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1650', + object_begin_date: '1647', + object_end_date: '1650', + medium: 'Brass', + dimensions: 'H. 7 in. (17.8 cm)', + credit_line: 'Rogers Fund, 1935', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1187', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '35.43.3', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1188', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1935', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1650', + object_begin_date: '1647', + object_end_date: '1650', + medium: 'Brass', + dimensions: 'H. 5 3/4 in. (14.6 cm)', + credit_line: 'Rogers Fund, 1935', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1188', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '35.43.4', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1189', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1935', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1650', + object_begin_date: '1647', + object_end_date: '1650', + medium: 'Brass', + dimensions: 'H. 5 1/4 in. (13.3 cm)', + credit_line: 'Rogers Fund, 1935', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1189', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '36.164', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1190', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1936', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1700\u20131800', + object_begin_date: '1700', + object_end_date: '1800', + medium: 'Steel, brass', + dimensions: '10 x 5 x 5 in. (25.4 x 12.7 x 12.7 cm)', + credit_line: 'Rogers Fund, 1936', + geography_type: '', + city: '', + state: '', + county: '', + country: '', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1190', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '37.134.8', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1191', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1937', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1800\u20131900', + object_begin_date: '1800', + object_end_date: '1900', + medium: 'Earthenware', + dimensions: 'H. 1 3/4 in. (4.4 cm); Diam. 2 5/8 in. (6.7 cm)', + credit_line: 'Gift of Mrs. J. Insley Blair, 1937', + geography_type: 'Made in', + city: 'Strasburg', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1191', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '41.160.389', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1192', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1941', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1770\u20131800', + object_begin_date: '1770', + object_end_date: '1800', + medium: 'Brass', + dimensions: 'H. 6 1/2 in. (16.5 cm)', + credit_line: 'Bequest of W. Gedney Beatty, 1941', + geography_type: '', + city: '', + state: '', + county: '', + country: '', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1192', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '41.160.397', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1193', + gallery_number: '712', + department: 'The American Wing', + accessionyear: '1941', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1660\u201380', + object_begin_date: '1660', + object_end_date: '1680', + medium: 'Brass', + dimensions: 'H. 10 in. (25.4 cm)', + credit_line: 'Bequest of W. Gedney Beatty, 1941', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1193', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '41.160.398', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1194', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1941', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1600\u20131700', + object_begin_date: '1600', + object_end_date: '1700', + medium: 'Brass', + dimensions: 'H. 8 in. (20.3 cm)', + credit_line: 'Bequest of W. Gedney Beatty, 1941', + geography_type: 'Possibly made in|Possibly made in', + city: '', + state: '', + county: '', + country: 'England|Netherlands', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1194', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '41.160.399', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1195', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1941', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1600\u20131700', + object_begin_date: '1600', + object_end_date: '1700', + medium: 'Brass', + dimensions: 'H. 8 in. (20.3 cm)', + credit_line: 'Bequest of W. Gedney Beatty, 1941', + geography_type: 'Possibly made in|Possibly made in', + city: '', + state: '', + county: '', + country: 'England|Netherlands', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1195', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '41.160.400', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1196', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1941', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1600\u20131700', + object_begin_date: '1600', + object_end_date: '1700', + medium: 'Brass', + dimensions: 'H. 7 3/4 in. (19.7 cm)', + credit_line: 'Bequest of W. Gedney Beatty, 1941', + geography_type: '', + city: '', + state: '', + county: '', + country: '', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1196', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '41.160.401', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1197', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1941', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1600\u20131700', + object_begin_date: '1600', + object_end_date: '1700', + medium: 'Brass', + dimensions: 'H. 8 3/8 in. (21.3 cm)', + credit_line: 'Bequest of W. Gedney Beatty, 1941', + geography_type: 'Possibly made in|Possibly made in', + city: '', + state: '', + county: '', + country: 'England|Netherlands', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1197', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '41.160.403', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1198', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1941', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American or British', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1700\u20131800', + object_begin_date: '1700', + object_end_date: '1800', + medium: 'Brass', + dimensions: 'H. 7 in. (17.8 cm)', + credit_line: 'Bequest of W. Gedney Beatty, 1941', + geography_type: 'Possibly made in|Possibly made in', + city: '', + state: '', + county: '', + country: 'United States|England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1198', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '41.160.404', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1199', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1941', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American or British', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1700\u20131800', + object_begin_date: '1700', + object_end_date: '1800', + medium: 'Brass', + dimensions: 'H. 7 in. (17.8 cm)', + credit_line: 'Bequest of W. Gedney Beatty, 1941', + geography_type: 'Possibly made in|Possibly made in', + city: '', + state: '', + county: '', + country: 'United States|England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1199', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.54', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1200', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1825\u201350', + object_begin_date: '1825', + object_end_date: '1850', + medium: 'Lacy pressed glass', + dimensions: 'H. 8 1/2 in. (21.6 cm)', + credit_line: 'Rogers Fund, 1946', + geography_type: 'Made in', + city: 'Pittsburgh', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1200', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.140.297', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1201', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1860\u201370', + object_begin_date: '1860', + object_end_date: '1870', + medium: 'Pressed amethyst glass', + dimensions: 'H. 7 5/8 in. (19.4 cm)', + credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1201', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.140.312', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1202', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1875\u20131900', + object_begin_date: '1875', + object_end_date: '1900', + medium: 'Pressed yellow glass', + dimensions: 'H. 6 5/8 in. (16.8 cm)', + credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1202', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Dolphins', + tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159', + tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369', + }, + { + object_number: '46.140.358', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1203', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1840\u201350', + object_begin_date: '1840', + object_end_date: '1850', + medium: 'Lacy pressed glass, pewter', + dimensions: 'H. 7 3/8 in. (18.7 cm)', + credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946', + geography_type: 'Made in', + city: 'Pittsburgh', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1203', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.140.359', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1204', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1850\u201370', + object_begin_date: '1850', + object_end_date: '1870', + medium: 'Pressed gray glass', + dimensions: 'H. 9 13/16 in. (24.9 cm)', + credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1204', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.140.780', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1206', + gallery_number: '706', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1850\u201360', + object_begin_date: '1850', + object_end_date: '1860', + medium: 'Lacy pressed glass', + dimensions: + '11 3/8 x 4 x 4 in. (28.9 x 10.2 x 10.2 cm)\r\n48.135.61,False,False,True,1207,774,The American Wing,1948,Candlestick,Candlestick,,,,,,,,,,,,,,,,,,,ca. 1740,1737,1740,Brass,H. 7 3/8 in. (18.7 cm),Bequest of Adeline R. Brown', + credit_line: ' 1947"', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1207', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.62', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1208', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1740\u201370', + object_begin_date: '1740', + object_end_date: '1770', + medium: 'Brass', + dimensions: 'H. 7 3/4 in. (19.7 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1208', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.73', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1209', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1910', + object_begin_date: '1907', + object_end_date: '1910', + medium: 'Pewter', + dimensions: 'H. 6 in. (15.2 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1209', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.74a\u2013c', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1210', + gallery_number: '', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'British or Irish', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1790', + object_begin_date: '1787', + object_end_date: '1790', + medium: 'Blown glass', + dimensions: 'H. 10 7/8 in. (27.6 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Possibly made in|Possibly made in', + city: '', + state: '', + county: '', + country: 'England|Ireland', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1210', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.75a\u2013c', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1211', + gallery_number: '', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'British or Irish', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1790', + object_begin_date: '1787', + object_end_date: '1790', + medium: 'Blown glass', + dimensions: 'H. 10 7/8 in. (27.6 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Possibly made in|Possibly made in', + city: '', + state: '', + county: '', + country: 'England|Ireland', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1211', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.76a\u2013c', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1212', + gallery_number: '', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'British', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1790', + object_begin_date: '1787', + object_end_date: '1790', + medium: 'Cut glass', + dimensions: 'H. 10 3/8 in. (26.4 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1212', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '51.171.24', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1213', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1951', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1830\u201335', + object_begin_date: '1830', + object_end_date: '1835', + medium: 'Lacy pressed glass', + dimensions: 'H. 6 15/16 in. (17.6 cm)', + credit_line: + 'Gift of Mrs. Charles W. Green, in memory of Dr. Charles W. Green, 1951', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: 'New England ', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1213', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '51.171.32', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1214', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1951', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1830\u201335', + object_begin_date: '1830', + object_end_date: '1835', + medium: 'Lacy pressed glass', + dimensions: 'H. 8 15/16 in. (22.7 cm)', + credit_line: + 'Gift of Mrs. Charles W. Green, in memory of Dr. Charles W. Green, 1951', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: 'New England ', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1214', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '51.171.94', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1215', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1951', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1830\u20131900', + object_begin_date: '1830', + object_end_date: '1900', + medium: 'Pressed blue glass', + dimensions: 'H. 7 1/16 in. (17.9 cm)', + credit_line: + 'Gift of Mrs. Charles W. Green, in memory of Dr. Charles W. Green, 1951', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1215', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '66.10.33', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1216', + gallery_number: '734', + department: 'The American Wing', + accessionyear: '1966', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American, Shaker', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '8737', + artist_role: 'Maker', + artist_prefix: ' ', + artist_display_name: + 'United Society of Believers in Christ\u2019s Second Appearing (\u201cShakers\u201d)', + artist_display_bio: 'American, active ca. 1750\u2013present', + artist_suffix: ' ', + artist_alpha_sort: + 'United Society of Believers in Christ\u2019s Second Appearing', + artist_nationality: ' ', + artist_begin_date: '1750 ', + artist_end_date: '9999 ', + artist_gender: '', + artist_ulan_url: '(not assigned)', + artist_wikidata_url: 'https://www.wikidata.org/wiki/Q1370167', + object_date: '1820\u201360', + object_begin_date: '1820', + object_end_date: '1860', + medium: 'Tin', + dimensions: 'H. 6 in. (15.2 cm); Diam. 3 5/8 in. (9.2 cm)', + credit_line: 'Friends of the American Wing Fund, 1966', + geography_type: 'Made in', + city: 'New Lebanon', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1216', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '1971.180.119', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1217', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1971', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'Chinese', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1800\u20131900', + object_begin_date: '1800', + object_end_date: '1900', + medium: 'Porcelain', + dimensions: '5 3/16 x 6 1/16 in. (13.2 x 15.4 cm)', + credit_line: 'Bequest of Flora E. Whiting, 1971', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'China', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1217', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Elephants|Flowers', + tags_aat_url: + 'http://vocab.getty.edu/page/aat/300250160|http://vocab.getty.edu/page/aat/300132399', + tags_wikidata_url: + 'https://www.wikidata.org/wiki/Q7378|https://www.wikidata.org/wiki/Q506', + }, + { + object_number: '1984.133', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1218', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1984', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1845\u201360', + object_begin_date: '1845', + object_end_date: '1860', + medium: 'Porcelain', + dimensions: 'H. 9 7/8 in. (25.1 cm); Diam. 3 15/16 in. (10 cm)', + credit_line: 'Friends of the American Wing Fund, 1984', + geography_type: 'Made in', + city: 'Trenton', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1218', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.140.296', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1219', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '113', + artist_role: 'Maker', + artist_prefix: ' ', + artist_display_name: 'Boston & Sandwich Glass Company', + artist_display_bio: 'American, 1825\u20131888, Sandwich, Massachusetts', + artist_suffix: ' ', + artist_alpha_sort: 'Boston and Sandwich Glass Company ', + artist_nationality: ' ', + artist_begin_date: '1825 ', + artist_end_date: '1888 ', + artist_gender: '', + artist_ulan_url: 'http://vocab.getty.edu/page/ulan/500334773', + artist_wikidata_url: 'https://www.wikidata.org/wiki/Q4948224', + object_date: '1835\u201340', + object_begin_date: '1835', + object_end_date: '1840', + medium: 'Pressed amethyst glass', + dimensions: 'H. 6 15/16 in. (17.6 cm)', + credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946', + geography_type: 'Made in', + city: 'Sandwich', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1219', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.140.360', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1220', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '113', + artist_role: 'Maker', + artist_prefix: ' ', + artist_display_name: 'Boston & Sandwich Glass Company', + artist_display_bio: 'American, 1825\u20131888, Sandwich, Massachusetts', + artist_suffix: ' ', + artist_alpha_sort: 'Boston and Sandwich Glass Company ', + artist_nationality: ' ', + artist_begin_date: '1825 ', + artist_end_date: '1888 ', + artist_gender: '', + artist_ulan_url: 'http://vocab.getty.edu/page/ulan/500334773', + artist_wikidata_url: 'https://www.wikidata.org/wiki/Q4948224', + object_date: 'ca. 1840', + object_begin_date: '1837', + object_end_date: '1840', + medium: 'Pressed glass', + dimensions: 'H. 8 1/2 in. (21.6 cm)', + credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946', + geography_type: 'Made in', + city: 'Sandwich', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1220', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Dolphins', + tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159', + tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369', + }, + { + object_number: '41.99.4', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1221', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1941', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '130', + artist_role: 'Maker', + artist_prefix: ' ', + artist_display_name: 'E. Durnall', + artist_display_bio: ' ', + artist_suffix: ' ', + artist_alpha_sort: 'Durnall, E.', + artist_nationality: ' ', + artist_begin_date: ' ', + artist_end_date: '9999 ', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1750\u201360', + object_begin_date: '1750', + object_end_date: '1760', + medium: 'Brass', + dimensions: 'H. 7 5/8 in. (19.4 cm)', + credit_line: + 'Gift of James De Lancey Verplanck and John Bayard Rodgers Verplanck, 1941', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1221', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '41.160.36', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1222', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1941', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'Mexican', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '131', + artist_role: 'Maker', + artist_prefix: ' ', + artist_display_name: 'Diego Gonzales de la Cueva', + artist_display_bio: ' ', + artist_suffix: ' ', + artist_alpha_sort: 'Gonzales, de la Cueva Diego', + artist_nationality: ' ', + artist_begin_date: '1733 ', + artist_end_date: '1778 ', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1700\u20131800', + object_begin_date: '1700', + object_end_date: '1800', + medium: 'Silver', + dimensions: + 'Overall: 7 3/16 in. (18.3 cm); 19 oz. 7 dwt. (601.9 g)\r\n57.153a', + credit_line: ' b"', + geography_type: 'False', + city: 'False', + state: 'True', + county: '1223', + country: '750', + region: 'The American Wing', + subregion: '1957', + locale: 'Candlestick', + locus: 'Candlestick', + excavation: 'American', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: '', + object_wikidata_url: '124', + metadata_date: 'Maker', + repository: ' ', + tags: 'Cornelius Kierstede', + tags_aat_url: '1674\u2013ca. 1757', + tags_wikidata_url: ' ', + }, + { + object_number: '46.140.320', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1230', + gallery_number: '706', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '1314108', + artist_role: 'Designer|Manufacturer', + artist_prefix: 'Designed by|Manufactured by', + artist_display_name: 'Henry Whitney|New England Glass Company', + artist_display_bio: + ' |American, East Cambridge, Massachusetts, 1818\u20131888', + artist_suffix: ' | ', + artist_alpha_sort: 'Whitney, Henry|New England Glass Company ', + artist_nationality: ' | ', + artist_begin_date: ' |1818 ', + artist_end_date: '9999 |1888 ', + artist_gender: '|', + artist_ulan_url: '|http://vocab.getty.edu/page/ulan/500356007', + artist_wikidata_url: '|https://www.wikidata.org/wiki/Q17083157', + object_date: '1870\u201375', + object_begin_date: '1870', + object_end_date: '1875', + medium: 'Pressed glass', + dimensions: 'H. 9 5/8 in. (24.4 cm)', + credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946', + geography_type: 'Made in', + city: 'East Cambridge', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1230', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Caryatids', + tags_aat_url: 'http://vocab.getty.edu/page/aat/300001583', + tags_wikidata_url: 'https://www.wikidata.org/wiki/Q208120', + }, + { + object_number: '61.231.1', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1231', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1961', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '130', + artist_role: 'Maker', + artist_prefix: ' ', + artist_display_name: 'Cornelius and Company', + artist_display_bio: '1838\u20131851', + artist_suffix: ' ', + artist_alpha_sort: 'Cornelius and Company ', + artist_nationality: ' ', + artist_begin_date: '1838 ', + artist_end_date: '1851 ', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1840\u201350', + object_begin_date: '1840', + object_end_date: '1850', + medium: 'Gilt metal, marble, crystal', + dimensions: '16 7/8 x 4 3/4 x 3 3/8 in. (42.9 x 12.1 x 8.6 cm)', + credit_line: 'Gift of Mary E. Steers, 1961', + geography_type: 'Made in', + city: 'Philadelphia', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1231', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '61.231.2', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1232', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1961', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '130', + artist_role: 'Maker', + artist_prefix: ' ', + artist_display_name: 'Cornelius and Company', + artist_display_bio: '1838\u20131851', + artist_suffix: ' ', + artist_alpha_sort: 'Cornelius and Company ', + artist_nationality: ' ', + artist_begin_date: '1838 ', + artist_end_date: '1851 ', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1840\u201350', + object_begin_date: '1840', + object_end_date: '1850', + medium: 'Gilt metal, marble, crystal', + dimensions: '16 7/8 x 4 3/4 x 3 3/8 in. (42.9 x 12.1 x 8.6 cm)', + credit_line: 'Gift of Mary E. Steers, 1961', + geography_type: 'Made in', + city: 'Philadelphia', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1232', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '24.116.6', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1235', + gallery_number: '', + department: 'The American Wing', + accessionyear: '1924', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'British', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1800\u20131830', + object_begin_date: '1800', + object_end_date: '1830', + medium: 'Metal, earthenware, glass', + dimensions: 'H. 12 in. (30.5 cm); Diam. 4 7/16 in. (11.3 cm)', + credit_line: 'Bequest of Miss Emilie Ogden, 1924', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1235', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '24.116.7', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1236', + gallery_number: '', + department: 'The American Wing', + accessionyear: '1924', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'British', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1800\u20131830', + object_begin_date: '1800', + object_end_date: '1830', + medium: 'Metal, earthenware, glass', + dimensions: 'H. 12 in. (30.5 cm); Diam. 4 7/16 in. (11.3 cm)', + credit_line: 'Bequest of Miss Emilie Ogden, 1924', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1236', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Human Figures', + tags_aat_url: 'http://vocab.getty.edu/page/aat/300404114', + tags_wikidata_url: 'https://www.wikidata.org/wiki/Q5937779', + }, + { + object_number: '35.124.1', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1241', + gallery_number: '704', + department: 'The American Wing', + accessionyear: '1935', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1830\u201355', + object_begin_date: '1830', + object_end_date: '1855', + medium: 'Blown glass', + dimensions: 'H. 9 1/8 in. (23.2 cm)', + credit_line: 'Rogers Fund, 1935', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1241', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '35.124.2', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1242', + gallery_number: '704', + department: 'The American Wing', + accessionyear: '1935', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1830\u201355', + object_begin_date: '1830', + object_end_date: '1855', + medium: 'Blown glass', + dimensions: 'H. 9 1/8 in. (23.2 cm)', + credit_line: 'Rogers Fund, 1935', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1242', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '36.148.1', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1243', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1936', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1840\u201360', + object_begin_date: '1840', + object_end_date: '1860', + medium: 'Yellow glass', + dimensions: 'H. 10 3/8 in. (26.4 cm)', + credit_line: 'Rogers Fund, 1936', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1243', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Dolphins', + tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159', + tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369', + }, + { + object_number: '36.148.2', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1244', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1936', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1840\u201360', + object_begin_date: '1840', + object_end_date: '1860', + medium: 'Yellow glass', + dimensions: 'H. 10 3/8 in. (26.4 cm)', + credit_line: 'Rogers Fund, 1936', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1244', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Dolphins', + tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159', + tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369', + }, + { + object_number: '38.138.1', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1245', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1938', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1720', + object_begin_date: '1717', + object_end_date: '1720', + medium: 'Brass', + dimensions: 'H. 6 in. (15.2 cm)', + credit_line: 'Gift of Allan B. A. Bradley, 1938', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1245', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '38.138.2', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1246', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1938', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1720', + object_begin_date: '1717', + object_end_date: '1720', + medium: 'Brass', + dimensions: 'H. 6 in. (15.2 cm)', + credit_line: 'Gift of Allan B. A. Bradley, 1938', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1246', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '41.160.387', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1247', + gallery_number: '729', + department: 'The American Wing', + accessionyear: '1941', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1700\u20131800', + object_begin_date: '1700', + object_end_date: '1800', + medium: 'Brass', + dimensions: 'H. 6 1/2 in. (16.5 cm)', + credit_line: 'Bequest of W. Gedney Beatty, 1941', + geography_type: '', + city: '', + state: '', + county: '', + country: '', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1247', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '41.160.388', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1248', + gallery_number: '729', + department: 'The American Wing', + accessionyear: '1941', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1700\u20131800', + object_begin_date: '1700', + object_end_date: '1800', + medium: 'Brass', + dimensions: 'H. 6 1/2 in. (16.5 cm)', + credit_line: 'Bequest of W. Gedney Beatty, 1941', + geography_type: '', + city: '', + state: '', + county: '', + country: '', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1248', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.67.3', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1249', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'British', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1800\u20131830', + object_begin_date: '1800', + object_end_date: '1830', + medium: 'Earthenware, lusterware', + dimensions: 'H. 6 1/4 in. (15.9 cm)', + credit_line: + 'Gift of the Members of the Committee of the Bertha King Benkard Memorial Fund, 1946', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1249', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.67.4', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1250', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'British', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1800\u20131830', + object_begin_date: '1800', + object_end_date: '1830', + medium: 'Earthenware, lusterware', + dimensions: 'H. 6 1/4 in. (15.9 cm)', + credit_line: + 'Gift of the Members of the Committee of the Bertha King Benkard Memorial Fund, 1946', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1250', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.67.61a, b', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1251', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1800\u20131900', + object_begin_date: '1800', + object_end_date: '1900', + medium: 'Brass', + dimensions: 'H. 9 5/8 in. (24.4 cm)', + credit_line: + 'Gift of the Members of the Committee of the Bertha King Benkard Memorial Fund, 1946', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'France', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1251', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.67.62a, b', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1252', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1800\u20131900', + object_begin_date: '1800', + object_end_date: '1900', + medium: 'Brass', + dimensions: 'H. 9 5/8 in. (24.4 cm)', + credit_line: + 'Gift of the Members of the Committee of the Bertha King Benkard Memorial Fund, 1946', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'France', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1252', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.67.63a, b', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1253', + gallery_number: '', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1770\u20131800', + object_begin_date: '1770', + object_end_date: '1800', + medium: 'Brass', + dimensions: 'H. 11 in. (27.9 cm)', + credit_line: + 'Gift of the Members of the Committee of the Bertha King Benkard Memorial Fund, 1946', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1253', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.67.64a, b', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1254', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1770\u20131800', + object_begin_date: '1770', + object_end_date: '1800', + medium: 'Brass', + dimensions: 'H. 11 in. (27.9 cm)', + credit_line: + 'Gift of the Members of the Committee of the Bertha King Benkard Memorial Fund, 1946', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1254', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.140.331', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1255', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1840\u201360', + object_begin_date: '1840', + object_end_date: '1860', + medium: 'Pressed glass', + dimensions: 'H. 5 1/16 in. (12.9 cm)', + credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'United States', + region: 'New England ', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1255', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.140.332', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1256', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1840\u201360', + object_begin_date: '1840', + object_end_date: '1860', + medium: 'Pressed glass', + dimensions: 'H. 5 1/8 in. (13 cm)', + credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'United States', + region: 'New England ', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1256', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.140.734', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1257', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1880\u201390', + object_begin_date: '1880', + object_end_date: '1890', + medium: 'Pressed purple marble glass', + dimensions: 'Dimensions unavailable', + credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1257', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.140.735', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1258', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1880\u201390', + object_begin_date: '1880', + object_end_date: '1890', + medium: 'Pressed purple marble glass', + dimensions: 'Dimensions unavailable', + credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1258', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.140.736', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1259', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1880\u201390', + object_begin_date: '1880', + object_end_date: '1890', + medium: 'Pressed purple marble glass', + dimensions: 'Dimensions unavailable', + credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1259', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '46.140.737', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1260', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1946', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1880\u201390', + object_begin_date: '1880', + object_end_date: '1890', + medium: 'Pressed purple marble glass', + dimensions: 'Dimensions unavailable', + credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1260', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.37', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1261', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1710\u201320', + object_begin_date: '1710', + object_end_date: '1720', + medium: 'Brass', + dimensions: 'H. 6 1/4 in. (15.9 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1261', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.38', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1262', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1710\u201320', + object_begin_date: '1710', + object_end_date: '1720', + medium: 'Brass', + dimensions: 'H. 6 1/4 in. (15.9 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1262', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.45', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1263', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1760', + object_begin_date: '1757', + object_end_date: '1760', + medium: 'Brass', + dimensions: 'H. 5 1/4 in. (13.3 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1263', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.46', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1264', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1760', + object_begin_date: '1757', + object_end_date: '1760', + medium: 'Brass', + dimensions: 'H. 5 1/4 in. (13.3 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1264', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.47', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1265', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1750', + object_begin_date: '1747', + object_end_date: '1750', + medium: 'Brass', + dimensions: 'H. 8 3/8 in. (21.3 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1265', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.48', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1266', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1750', + object_begin_date: '1747', + object_end_date: '1750', + medium: 'Brass', + dimensions: 'H. 8 3/8 in. (21.3 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1266', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.49', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1267', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1725\u201340', + object_begin_date: '1725', + object_end_date: '1740', + medium: 'Brass', + dimensions: 'H. 8 3/8 in. (21.3 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1267', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.50', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1268', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1725\u201340', + object_begin_date: '1725', + object_end_date: '1740', + medium: 'Brass', + dimensions: 'H. 8 3/8 in. (21.3 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1268', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.51', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1269', + gallery_number: '', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American or British', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1750', + object_begin_date: '1747', + object_end_date: '1750', + medium: 'Brass', + dimensions: 'H. 8 5/8 in. (21.9 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Possibly made in|Possibly made in', + city: '', + state: '', + county: '', + country: 'United States|England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1269', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.53', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1270', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1750\u201360', + object_begin_date: '1750', + object_end_date: '1760', + medium: 'Brass', + dimensions: 'H. 8 3/4 in. (22.2 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1270', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.54', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1271', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1750\u201360', + object_begin_date: '1750', + object_end_date: '1760', + medium: 'Brass', + dimensions: 'H. 8 3/4 in. (22.2 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1271', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.55', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1272', + gallery_number: '', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1750\u201370', + object_begin_date: '1750', + object_end_date: '1770', + medium: 'Brass', + dimensions: 'H. 7 3/4 in. (19.7 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1272', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.59', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1273', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1750\u201360', + object_begin_date: '1750', + object_end_date: '1760', + medium: 'Brass', + dimensions: 'H. 7 7/8 in. (20 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1273', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.60', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1274', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1750\u201360', + object_begin_date: '1750', + object_end_date: '1760', + medium: 'Brass', + dimensions: 'H. 7 7/8 in. (20 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1274', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.63', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1275', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1760', + object_begin_date: '1757', + object_end_date: '1760', + medium: 'Brass', + dimensions: 'H. 10 in. (25.4 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1275', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.64', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1276', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1760', + object_begin_date: '1757', + object_end_date: '1760', + medium: 'Brass', + dimensions: 'H. 10 in. (25.4 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1276', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.71', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1277', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1820', + object_begin_date: '1817', + object_end_date: '1820', + medium: 'Pewter', + dimensions: 'H. 7 3/4 in. (19.7 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: '', + city: '', + state: '', + county: '', + country: '', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1277', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.135.72', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1278', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1820', + object_begin_date: '1817', + object_end_date: '1820', + medium: 'Pewter', + dimensions: 'H. 7 3/4 in. (19.7 cm)', + credit_line: 'Bequest of Adeline R. Brown, 1947', + geography_type: '', + city: '', + state: '', + county: '', + country: '', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1278', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.145.1', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1279', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1790', + object_begin_date: '1787', + object_end_date: '1790', + medium: 'Sheffield silver plate', + dimensions: 'H. 11 1/2 in. (29.2 cm)', + credit_line: 'Bequest of Fannie Randolph Gaunt, 1948', + geography_type: 'Probably made in', + city: 'Sheffield', + state: 'South Yorkshire', + county: 'South Yorkshire', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1279', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '48.145.2', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1280', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1948', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1790', + object_begin_date: '1787', + object_end_date: '1790', + medium: 'Silver plate on copper', + dimensions: 'H. 11 1/2 in. (29.2 cm)', + credit_line: 'Bequest of Fannie Randolph Gaunt, 1948', + geography_type: 'Probably made in', + city: 'Sheffield', + state: 'South Yorkshire', + county: 'South Yorkshire', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1280', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '57.131.1', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1281', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1957', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1840\u201350', + object_begin_date: '1840', + object_end_date: '1850', + medium: 'Pressed glass', + dimensions: 'H. 10 3/8 in. (26.4 cm)', + credit_line: + 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: 'New England ', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1281', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Dolphins', + tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159', + tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369', + }, + { + object_number: '57.131.2', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1282', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1957', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1840\u201350', + object_begin_date: '1840', + object_end_date: '1850', + medium: 'Pressed glass', + dimensions: 'H. 10 1/4 in. (26 cm)', + credit_line: + 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: 'New England ', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1282', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Dolphins', + tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159', + tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369', + }, + { + object_number: '57.131.5', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1283', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1957', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1920s', + object_begin_date: '1917', + object_end_date: '1920', + medium: 'Pressed opaque green and white glass', + dimensions: 'H. 10 3/4 in. (27.3 cm)', + credit_line: + 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'Czech Republic', + region: 'Bohemia', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1283', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Dolphins', + tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159', + tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369', + }, + { + object_number: '57.131.6', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1284', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1957', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1920s', + object_begin_date: '1917', + object_end_date: '1920', + medium: 'Pressed opaque white and green glass', + dimensions: 'H. 10 3/4 in. (27.3 cm)', + credit_line: + 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'Czech Republic', + region: 'Bohemia', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1284', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Dolphins', + tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159', + tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369', + }, + { + object_number: '57.131.7', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1285', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1957', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1845\u201370', + object_begin_date: '1845', + object_end_date: '1870', + medium: 'Pressed opaque blue glass', + dimensions: 'H. 9 3/4 in. (24.8 cm)', + credit_line: + 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1285', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Dolphins', + tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159', + tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369', + }, + { + object_number: '57.131.8', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1286', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1957', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1845\u201370', + object_begin_date: '1845', + object_end_date: '1870', + medium: 'Pressed opaque blue glass', + dimensions: 'H. 9 3/4 in. (24.8 cm)', + credit_line: + 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1286', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Dolphins', + tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159', + tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369', + }, + { + object_number: '57.131.9', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1287', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1957', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1840\u201370', + object_begin_date: '1840', + object_end_date: '1870', + medium: 'Pressed opaque blue and white glass', + dimensions: 'H. 10 7/8 in. (27.6 cm)', + credit_line: + 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1287', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '57.131.10', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1288', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1957', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1840\u201370', + object_begin_date: '1840', + object_end_date: '1870', + medium: 'Pressed opaque blue and white glass', + dimensions: 'H. 10 15/16 in. (27.8 cm)', + credit_line: + 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1288', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '66.10.30', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1289', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1966', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American, Shaker', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '8737', + artist_role: 'Maker', + artist_prefix: ' ', + artist_display_name: + 'United Society of Believers in Christ\u2019s Second Appearing (\u201cShakers\u201d)', + artist_display_bio: 'American, active ca. 1750\u2013present', + artist_suffix: ' ', + artist_alpha_sort: + 'United Society of Believers in Christ\u2019s Second Appearing', + artist_nationality: ' ', + artist_begin_date: '1750 ', + artist_end_date: '9999 ', + artist_gender: '', + artist_ulan_url: '(not assigned)', + artist_wikidata_url: 'https://www.wikidata.org/wiki/Q1370167', + object_date: '1820\u201360', + object_begin_date: '1820', + object_end_date: '1860', + medium: 'Tin', + dimensions: 'H. 6 in. (15.2 cm); Diam. 3 5/8 in. (9.2 cm)', + credit_line: 'Friends of the American Wing Fund, 1966', + geography_type: 'Made in', + city: 'New Lebanon', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1289', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '66.10.31', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1290', + gallery_number: '', + department: 'The American Wing', + accessionyear: '1966', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American, Shaker', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '8737', + artist_role: 'Maker', + artist_prefix: ' ', + artist_display_name: + 'United Society of Believers in Christ\u2019s Second Appearing (\u201cShakers\u201d)', + artist_display_bio: 'American, active ca. 1750\u2013present', + artist_suffix: ' ', + artist_alpha_sort: + 'United Society of Believers in Christ\u2019s Second Appearing', + artist_nationality: ' ', + artist_begin_date: '1750 ', + artist_end_date: '9999 ', + artist_gender: '', + artist_ulan_url: '(not assigned)', + artist_wikidata_url: 'https://www.wikidata.org/wiki/Q1370167', + object_date: '1820\u201360', + object_begin_date: '1820', + object_end_date: '1860', + medium: 'Tin', + dimensions: 'H. 6 in. (15.2 cm); Diam. 3 5/8 in. (9.2 cm)', + credit_line: 'Friends of the American Wing Fund, 1966', + geography_type: 'Made in', + city: 'New Lebanon', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1290', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '1970.126.6', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1291', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1970', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1810', + object_begin_date: '1807', + object_end_date: '1810', + medium: 'Silver plate on copper', + dimensions: '12 1/16 x 6 1/8 x 5 in. (30.6 x 15.6 x 12.7 cm)', + credit_line: + 'Gift of Mrs. B. Langdon Tyler and Mrs. William Floyd Nichols, 1970', + geography_type: 'Made in', + city: 'Sheffield', + state: 'South Yorkshire', + county: 'South Yorkshire', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1291', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '1970.126.7', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1292', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1970', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1810', + object_begin_date: '1807', + object_end_date: '1810', + medium: 'Silver plate on copper', + dimensions: '12 1/16 x 6 1/8 x 5 in. (30.6 x 15.6 x 12.7 cm)', + credit_line: + 'Gift of Mrs. B. Langdon Tyler and Mrs. William Floyd Nichols, 1970', + geography_type: 'Made in', + city: 'Sheffield', + state: 'South Yorkshire', + county: 'South Yorkshire', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1292', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '1971.180.69', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1293', + gallery_number: '710', + department: 'The American Wing', + accessionyear: '1971', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1670\u20131700', + object_begin_date: '1670', + object_end_date: '1700', + medium: 'Brass', + dimensions: 'H. 8 1/2 in. (21.6 cm); Diam. 6 1/8 in. (15.6 cm)', + credit_line: 'Bequest of Flora E. Whiting, 1971', + geography_type: '', + city: '', + state: '', + county: '', + country: '', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1293', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '1971.180.70', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1294', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1971', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1670\u20131700', + object_begin_date: '1670', + object_end_date: '1700', + medium: 'Brass', + dimensions: 'H. 8 1/2 in. (21.6 cm); Diam. 6 1/8 in. (15.6 cm)', + credit_line: 'Bequest of Flora E. Whiting, 1971', + geography_type: '', + city: '', + state: '', + county: '', + country: '', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1294', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '1971.180.78a, b', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1295', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1971', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1700\u20131800', + object_begin_date: '1700', + object_end_date: '1800', + medium: 'Copper, enamel, brass', + dimensions: '8 5/8 x 4 13/16 in. (21.9 x 12.2 cm)', + credit_line: 'Bequest of Flora E. Whiting, 1971', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1295', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, +]; diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-set-small.ts b/apps/playground/src/app/components/data-grid/data-manager-large/data-set-small.ts new file mode 100644 index 0000000000..57e94f38c9 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-set-small.ts @@ -0,0 +1,789 @@ +// Source: https://github.com/metmuseum/openaccess +import { SkyCellType } from '@skyux/ag-grid'; + +import { ColDef } from 'ag-grid-community'; + +/* spell-checker:disable */ +export const columnDefinitions: ColDef[] = [ + { + field: 'select', + headerName: ' ', + type: [SkyCellType.RowSelector], + }, + { + field: 'object_number', + headerName: 'Object Number', + type: [], + sortable: true, + }, + { + field: 'is_highlight', + headerName: 'Is Highlight', + type: [], + sortable: true, + }, + { + field: 'is_timeline_work', + headerName: 'Is Timeline Work', + type: [], + sortable: true, + }, + { + field: 'is_public_domain', + headerName: 'Is Public Domain', + type: [], + sortable: true, + }, + { + field: 'object_id', + headerName: 'Object ID', + type: [], + sortable: true, + }, + { + field: 'gallery_number', + headerName: 'Gallery Number', + type: [], + sortable: true, + }, + { + field: 'department', + headerName: 'Department', + type: [], + sortable: true, + }, + { + field: 'accessionyear', + headerName: 'AccessionYear', + type: [], + sortable: true, + }, + { + field: 'object_name', + headerName: 'Object Name', + type: [], + sortable: true, + }, + { + field: 'title', + headerName: 'Title', + type: [], + sortable: true, + }, + { + field: 'culture', + headerName: 'Culture', + type: [], + sortable: true, + }, + { + field: 'period', + headerName: 'Period', + type: [], + sortable: true, + }, + { + field: 'dynasty', + headerName: 'Dynasty', + type: [], + sortable: true, + }, + { + field: 'reign', + headerName: 'Reign', + type: [], + sortable: true, + }, + { + field: 'portfolio', + headerName: 'Portfolio', + type: [], + sortable: true, + }, + { + field: 'constituent_id', + headerName: 'Constituent ID', + type: [], + sortable: true, + }, + { + field: 'artist_role', + headerName: 'Artist Role', + type: [], + sortable: true, + }, + { + field: 'artist_prefix', + headerName: 'Artist Prefix', + type: [], + sortable: true, + }, + { + field: 'artist_display_name', + headerName: 'Artist Display Name', + type: [], + sortable: true, + }, + { + field: 'artist_display_bio', + headerName: 'Artist Display Bio', + type: [], + sortable: true, + }, + { + field: 'artist_suffix', + headerName: 'Artist Suffix', + type: [], + sortable: true, + }, + { + field: 'artist_alpha_sort', + headerName: 'Artist Alpha Sort', + type: [], + sortable: true, + }, + { + field: 'artist_nationality', + headerName: 'Artist Nationality', + type: [], + sortable: true, + }, + { + field: 'artist_begin_date', + headerName: 'Artist Begin Date', + type: [], + sortable: true, + }, + { + field: 'artist_end_date', + headerName: 'Artist End Date', + type: [], + sortable: true, + }, + { + field: 'artist_gender', + headerName: 'Artist Gender', + type: [], + sortable: true, + }, + { + field: 'artist_ulan_url', + headerName: 'Artist ULAN URL', + type: ['custom_link'], + sortable: true, + }, + { + field: 'artist_wikidata_url', + headerName: 'Artist Wikidata URL', + type: ['custom_link'], + sortable: true, + }, + { + field: 'object_date', + headerName: 'Object Date', + type: [], + sortable: true, + }, + { + field: 'object_begin_date', + headerName: 'Object Begin Date', + type: [], + sortable: true, + }, + { + field: 'object_end_date', + headerName: 'Object End Date', + type: [], + sortable: true, + }, + { + field: 'medium', + headerName: 'Medium', + type: [], + sortable: true, + }, + { + field: 'dimensions', + headerName: 'Dimensions', + type: [], + sortable: true, + }, + { + field: 'credit_line', + headerName: 'Credit Line', + type: [], + sortable: true, + }, + { + field: 'geography_type', + headerName: 'Geography Type', + type: [], + sortable: true, + }, + { + field: 'city', + headerName: 'City', + type: [], + sortable: true, + }, + { + field: 'state', + headerName: 'State', + type: [], + sortable: true, + }, + { + field: 'county', + headerName: 'County', + type: [], + sortable: true, + }, + { + field: 'country', + headerName: 'Country', + type: [], + sortable: true, + }, + { + field: 'region', + headerName: 'Region', + type: [], + sortable: true, + }, + { + field: 'subregion', + headerName: 'Subregion', + type: [], + sortable: true, + }, + { + field: 'locale', + headerName: 'Locale', + type: [], + sortable: true, + }, + { + field: 'locus', + headerName: 'Locus', + type: [], + sortable: true, + }, + { + field: 'excavation', + headerName: 'Excavation', + type: [], + sortable: true, + }, + { + field: 'river', + headerName: 'River', + type: [], + sortable: true, + }, + { + field: 'classification', + headerName: 'Classification', + type: [], + sortable: true, + }, + { + field: 'rights_and_reproduction', + headerName: 'Rights and Reproduction', + type: [], + sortable: true, + }, + { + field: 'link_resource', + headerName: 'Link Resource', + type: ['custom_link'], + sortable: true, + }, + { + field: 'object_wikidata_url', + headerName: 'Object Wikidata URL', + type: ['custom_link'], + sortable: true, + }, + { + field: 'metadata_date', + headerName: 'Metadata Date', + type: [], + sortable: true, + }, + { + field: 'repository', + headerName: 'Repository', + type: [], + sortable: true, + }, + { + field: 'tags', + headerName: 'Tags', + type: [], + sortable: true, + }, + { + field: 'tags_aat_url', + headerName: 'Tags AAT URL', + type: ['custom_link'], + sortable: true, + }, + { + field: 'tags_wikidata_url', + headerName: 'Tags Wikidata URL', + type: ['custom_link'], + sortable: true, + }, +]; + +export const data = [ + { + object_number: '24.109.38a\u2013c', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1153', + gallery_number: '704', + department: 'The American Wing', + accessionyear: '1924', + object_name: 'Candlestick', + title: 'Candle Holder', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '130', + artist_role: 'Maker', + artist_prefix: ' ', + artist_display_name: 'Joseph Lownes', + artist_display_bio: '1758\u20131820', + artist_suffix: ' ', + artist_alpha_sort: 'Lownes, Joseph', + artist_nationality: ' ', + artist_begin_date: '1758 ', + artist_end_date: '1820 ', + artist_gender: '', + artist_ulan_url: 'http://vocab.getty.edu/page/ulan/500330248', + artist_wikidata_url: '', + object_date: '1790\u20131810', + object_begin_date: '1790', + object_end_date: '1810', + medium: 'Silver, steel', + dimensions: + 'Overall: 5 1/16 x 9 5/8 x 4 3/4 in. (12.9 x 24.4 x 12.1 cm); 17 oz. 17 dwt. (556.5 g)\r\n44.12.13,False,False,True,1170,,The American Wing,1944,Candlestick,Standing candlestick,,,,,,,,,,,,,,,,,,,1730\u201350,1730,1750,Brass and iron,H. 62 in. (157.5 cm),Bequest of Sarah Williams', + credit_line: ' 1944"', + geography_type: 'Possibly made in', + city: 'Boston', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1170', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '11.87.78', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1183', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1911', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'Mexican', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1800', + object_begin_date: '1797', + object_end_date: '1800', + medium: 'Tin-glazed earthenware', + dimensions: 'H. 15 1/2 in. (39.4 cm)', + credit_line: 'Gift of Mrs. Robert W. de Forest, 1911', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'Mexico', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1183', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: 'Dogs', + tags_aat_url: 'http://vocab.getty.edu/page/aat/300265714', + tags_wikidata_url: 'https://www.wikidata.org/wiki/Q144', + }, + { + object_number: '18.95.4', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1184', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1918', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1800\u20131830', + object_begin_date: '1800', + object_end_date: '1830', + medium: 'Earthenware', + dimensions: 'H. 4 in. (10.2 cm); Diam. 5 in. (12.7 cm)', + credit_line: 'Rogers Fund, 1918', + geography_type: 'Probably made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1184', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '20.14.7', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'True', + object_id: '1185', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1920', + object_name: 'Candlestick', + title: 'Candlestick', + culture: 'American', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: '1700\u20131800', + object_begin_date: '1700', + object_end_date: '1800', + medium: 'Free-blown lead aquamarine glass', + dimensions: 'H. 10 1/4 in. (26 cm)', + credit_line: 'Rogers Fund, 1920', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'United States', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1185', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '35.43.1', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1186', + gallery_number: '', + department: 'The American Wing', + accessionyear: '1935', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1650', + object_begin_date: '1647', + object_end_date: '1650', + medium: 'Brass', + dimensions: 'H. 8 1/4 in. (21 cm)', + credit_line: 'Rogers Fund, 1935', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1186', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '35.43.2', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1187', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1935', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1650', + object_begin_date: '1647', + object_end_date: '1650', + medium: 'Brass', + dimensions: 'H. 7 in. (17.8 cm)', + credit_line: 'Rogers Fund, 1935', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1187', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '35.43.3', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1188', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1935', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1650', + object_begin_date: '1647', + object_end_date: '1650', + medium: 'Brass', + dimensions: 'H. 5 3/4 in. (14.6 cm)', + credit_line: 'Rogers Fund, 1935', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1188', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, + { + object_number: '35.43.4', + is_highlight: 'False', + is_timeline_work: 'False', + is_public_domain: 'False', + object_id: '1189', + gallery_number: '774', + department: 'The American Wing', + accessionyear: '1935', + object_name: 'Candlestick', + title: 'Candlestick', + culture: '', + period: '', + dynasty: '', + reign: '', + portfolio: '', + constituent_id: '', + artist_role: '', + artist_prefix: '', + artist_display_name: '', + artist_display_bio: '', + artist_suffix: '', + artist_alpha_sort: '', + artist_nationality: '', + artist_begin_date: '', + artist_end_date: '', + artist_gender: '', + artist_ulan_url: '', + artist_wikidata_url: '', + object_date: 'ca. 1650', + object_begin_date: '1647', + object_end_date: '1650', + medium: 'Brass', + dimensions: 'H. 5 1/4 in. (13.3 cm)', + credit_line: 'Rogers Fund, 1935', + geography_type: 'Made in', + city: '', + state: '', + county: '', + country: 'England', + region: '', + subregion: '', + locale: '', + locus: '', + excavation: '', + river: '', + classification: '', + rights_and_reproduction: '', + link_resource: 'http://www.metmuseum.org/art/collection/search/1189', + object_wikidata_url: '', + metadata_date: '', + repository: 'Metropolitan Museum of Art, New York, NY', + tags: '', + tags_aat_url: '', + tags_wikidata_url: '', + }, +]; diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/delete-button/delete-button.component.html b/apps/playground/src/app/components/data-grid/data-manager-large/delete-button/delete-button.component.html new file mode 100644 index 0000000000..3f1e5cbb39 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-manager-large/delete-button/delete-button.component.html @@ -0,0 +1,7 @@ + diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/delete-button/delete-button.component.ts b/apps/playground/src/app/components/data-grid/data-manager-large/delete-button/delete-button.component.ts new file mode 100644 index 0000000000..a5a8e27e9a --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-manager-large/delete-button/delete-button.component.ts @@ -0,0 +1,28 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + input, +} from '@angular/core'; +import { SkyIconModule } from '@skyux/icon'; + +import { DataManagerLargeComponent } from '../data-manager-large.component'; + +@Component({ + selector: 'app-delete-button', + imports: [SkyIconModule], + templateUrl: './delete-button.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DeleteButtonComponent { + public readonly rowId = input(); + + readonly #component = inject(DataManagerLargeComponent); + + protected delete(): void { + const id = this.rowId(); + if (id) { + this.#component.markForDelete(id); + } + } +} diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/local-storage-config.service.ts b/apps/playground/src/app/components/data-grid/data-manager-large/local-storage-config.service.ts new file mode 100644 index 0000000000..30484ef330 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-manager-large/local-storage-config.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { SkyUIConfigService } from '@skyux/core'; + +import { Observable, of } from 'rxjs'; + +const SETTINGS_KEY_PREFIX = 'data-grid-test-'; + +@Injectable() +export class LocalStorageConfigService extends SkyUIConfigService { + public override getConfig(key: string, defaultConfig?: any): Observable { + const settingsJSON = localStorage.getItem(`${SETTINGS_KEY_PREFIX}${key}`); + if (settingsJSON) { + return of(JSON.parse(settingsJSON)); + } + return of(defaultConfig); + } + + public override setConfig(key: string, value: any): Observable { + localStorage.setItem(`${SETTINGS_KEY_PREFIX}${key}`, JSON.stringify(value)); + + return of(); + } +} diff --git a/apps/playground/src/app/components/data-grid/filtered/grid.component.html b/apps/playground/src/app/components/data-grid/filtered/grid.component.html new file mode 100644 index 0000000000..709d824270 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/filtered/grid.component.html @@ -0,0 +1,98 @@ +

Filtered Data Grid

+ +

+ This example demonstrates integrating sky-filter-bar with + sky-ag-grid using typed filters. All filters (text, number, date, + and boolean) are applied using AG Grid's external filtering mechanism via + isExternalFilterPresent and doesExternalFilterPass. +

+ +

Filter Bar + AG Grid Integration

+ + + + + + + + +
+ + + + + + + +
+ +

Current Filter State

+ +
+

Applied Filters:

+
{{ appliedFilters() | json }}
+ +

Typed Filter Access:

+
    +
  • + Name filter (string): + {{ activeNameFilter() ?? 'not set' }} +
  • +
  • + Salary filter (range): + {{ activeSalaryFilter() | json }} +
  • +
  • + Start date filter (date range): + {{ activeStartDateFilter() | json }} +
  • +
+
diff --git a/apps/playground/src/app/components/data-grid/filtered/grid.component.scss b/apps/playground/src/app/components/data-grid/filtered/grid.component.scss new file mode 100644 index 0000000000..e994b038c3 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/filtered/grid.component.scss @@ -0,0 +1,20 @@ +.filter-debug { + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; + padding: 16px; + margin-top: 16px; + + pre { + background-color: #fff; + border: 1px solid #ddd; + padding: 8px; + overflow-x: auto; + } + + code { + background-color: #e8e8e8; + padding: 2px 6px; + border-radius: 3px; + } +} diff --git a/apps/playground/src/app/components/data-grid/filtered/grid.component.ts b/apps/playground/src/app/components/data-grid/filtered/grid.component.ts new file mode 100644 index 0000000000..0f88d9f5fb --- /dev/null +++ b/apps/playground/src/app/components/data-grid/filtered/grid.component.ts @@ -0,0 +1,152 @@ +import { JsonPipe } from '@angular/common'; +import { Component, computed, signal } from '@angular/core'; +import { + SkyDataGridColumnComponent, + SkyDataGridComponent, + SkyDataGridFilterValue, +} from '@skyux/data-grid'; +import { SkyFilterBarModule } from '@skyux/filter-bar'; +import { SkyFilterStateFilterItem } from '@skyux/lists'; + +import { HideInactiveFilterModalComponent } from './hide-inactive-filter-modal.component'; +import { NameFilterModalComponent } from './name-filter-modal.component'; +import { SalaryFilterModalComponent } from './salary-filter-modal.component'; +import { StartDateFilterModalComponent } from './start-date-filter-modal.component'; + +interface Employee { + id: string; + name: string; + department: string; + salary: number; + startDate: string; + active: boolean; +} + +@Component({ + selector: 'app-filtered-grid', + imports: [ + JsonPipe, + SkyDataGridComponent, + SkyDataGridColumnComponent, + SkyFilterBarModule, + ], + templateUrl: './grid.component.html', + styleUrl: './grid.component.scss', +}) +export default class FilteredGridComponent { + protected readonly nameFilterModal = NameFilterModalComponent; + protected readonly salaryFilterModal = SalaryFilterModalComponent; + protected readonly hideInactiveModal = HideInactiveFilterModalComponent; + protected readonly startDateFilterModal = StartDateFilterModalComponent; + + protected readonly appliedFilters = signal< + SkyFilterStateFilterItem[] + >([]); + + /** + * Raw employee data before filtering. + */ + protected readonly allEmployees: Employee[] = [ + { + id: '1', + name: 'Alice Johnson', + department: 'Engineering', + salary: 95000, + startDate: '2020-03-15', + active: true, + }, + { + id: '2', + name: 'Bob Smith', + department: 'Sales', + salary: 72000, + startDate: '2019-07-22', + active: true, + }, + { + id: '3', + name: 'Carol Williams', + department: 'Engineering', + salary: 110000, + startDate: '2018-01-10', + active: true, + }, + { + id: '4', + name: 'David Brown', + department: 'Marketing', + salary: 65000, + startDate: '2021-09-01', + active: false, + }, + { + id: '5', + name: 'Eva Martinez', + department: 'Engineering', + salary: 88000, + startDate: '2020-11-30', + active: true, + }, + { + id: '6', + name: 'Frank Garcia', + department: 'Sales', + salary: 78000, + startDate: '2017-04-18', + active: true, + }, + { + id: '7', + name: 'Grace Lee', + department: 'Marketing', + salary: 71000, + startDate: '2022-02-14', + active: true, + }, + { + id: '8', + name: 'Henry Wilson', + department: 'Engineering', + salary: 125000, + startDate: '2016-08-05', + active: false, + }, + { + id: '9', + name: 'Ivy Chen', + department: 'Sales', + salary: 82000, + startDate: '2019-12-20', + active: true, + }, + { + id: '10', + name: 'Jack Taylor', + department: 'Marketing', + salary: 58000, + startDate: '2023-01-09', + active: true, + }, + ]; + + /** + * Example of typed filter value access for display purposes. + */ + protected readonly activeNameFilter = computed( + () => + this.appliedFilters()?.find((f) => f.filterId === 'name')?.filterValue + ?.value as string | undefined, + ); + + protected readonly activeSalaryFilter = computed( + () => + this.appliedFilters()?.find((f) => f.filterId === 'salary')?.filterValue + ?.displayValue as string | undefined, + ); + + protected readonly activeStartDateFilter = computed( + () => + this.appliedFilters()?.find((f) => f.filterId === 'startDate') + ?.filterValue?.displayValue as string | undefined, + ); +} diff --git a/apps/playground/src/app/components/data-grid/filtered/hide-inactive-filter-modal.component.ts b/apps/playground/src/app/components/data-grid/filtered/hide-inactive-filter-modal.component.ts new file mode 100644 index 0000000000..fc5ac9242e --- /dev/null +++ b/apps/playground/src/app/components/data-grid/filtered/hide-inactive-filter-modal.component.ts @@ -0,0 +1,50 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + SkyFilterBarFilterValue, + SkyFilterItemModal, + SkyFilterItemModalInstance, +} from '@skyux/filter-bar'; +import { SkyCheckboxModule } from '@skyux/forms'; +import { SkyModalModule } from '@skyux/modals'; + +@Component({ + selector: 'app-hide-inactive-filter-modal', + imports: [FormsModule, SkyCheckboxModule, SkyModalModule], + template: ` + + + + + + + + + + `, +}) +export class HideInactiveFilterModalComponent implements SkyFilterItemModal { + public readonly modalInstance = inject(SkyFilterItemModalInstance); + readonly #context = this.modalInstance.context; + + public filterLabelText = this.#context.filterLabelText; + public hideInactive = !!this.#context.filterValue?.value; + + public apply(): void { + const filterValue: SkyFilterBarFilterValue | undefined = this.hideInactive + ? { value: true, displayValue: 'Showing active only' } + : undefined; + this.modalInstance.save({ filterValue }); + } + + public cancel(): void { + this.modalInstance.cancel(); + } +} diff --git a/apps/playground/src/app/components/data-grid/filtered/name-filter-modal.component.ts b/apps/playground/src/app/components/data-grid/filtered/name-filter-modal.component.ts new file mode 100644 index 0000000000..57dcb25918 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/filtered/name-filter-modal.component.ts @@ -0,0 +1,52 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + SkyFilterBarFilterValue, + SkyFilterItemModal, + SkyFilterItemModalInstance, +} from '@skyux/filter-bar'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyModalModule } from '@skyux/modals'; + +@Component({ + selector: 'app-name-filter-modal', + imports: [FormsModule, SkyInputBoxModule, SkyModalModule], + template: ` + + + + + + + + + + + + `, +}) +export class NameFilterModalComponent implements SkyFilterItemModal { + public readonly modalInstance = inject(SkyFilterItemModalInstance); + readonly #context = this.modalInstance.context; + + public filterLabelText = this.#context.filterLabelText; + public nameFilter = (this.#context.filterValue?.value as string) ?? ''; + + public apply(): void { + const filterValue: SkyFilterBarFilterValue | undefined = this.nameFilter + ? { + value: this.nameFilter, + displayValue: `Name contains "${this.nameFilter}"`, + } + : undefined; + this.modalInstance.save({ filterValue }); + } + + public cancel(): void { + this.modalInstance.cancel(); + } +} diff --git a/apps/playground/src/app/components/data-grid/filtered/salary-filter-modal.component.ts b/apps/playground/src/app/components/data-grid/filtered/salary-filter-modal.component.ts new file mode 100644 index 0000000000..3675432045 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/filtered/salary-filter-modal.component.ts @@ -0,0 +1,146 @@ +import { Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { + SkyAutonumericModule, + SkyAutonumericOptions, +} from '@skyux/autonumeric'; +import { SkyNumericService } from '@skyux/core'; +import { + SkyDataGridNumberRangeFilterFormGroup, + SkyDataGridNumberRangeFilterValue, +} from '@skyux/data-grid'; +import { + SkyFilterItemModal, + SkyFilterItemModalInstance, +} from '@skyux/filter-bar'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyModalError, SkyModalModule } from '@skyux/modals'; + +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'app-salary-filter-modal', + imports: [ + ReactiveFormsModule, + SkyInputBoxModule, + SkyModalModule, + SkyAutonumericModule, + ], + template: ` +
+ + +
+ + + +
+
+ + + +
+
+ + + + + +
+
+ `, +}) +export class SalaryFilterModalComponent implements SkyFilterItemModal { + public readonly modalInstance = inject(SkyFilterItemModalInstance); + readonly #context = this.modalInstance.context; + readonly #fb = inject(FormBuilder); + readonly #existingValue = this.#context.filterValue?.value as + | SkyDataGridNumberRangeFilterValue + | undefined; + readonly #numeric = inject(SkyNumericService); + + public filterLabelText = this.#context.filterLabelText; + + protected readonly form = this.#fb.group( + { + from: this.#fb.control(this.#existingValue?.from ?? null), + to: this.#fb.control(this.#existingValue?.to ?? null), + }, + { + validators: (formGroup: SkyDataGridNumberRangeFilterFormGroup) => { + const min = formGroup.controls.from.value; + const max = formGroup.controls.to.value; + // At least one value must be provided. + if (min === null && max === null) { + return { + salaryRangeRequired: { message: 'Salary Range required' }, + }; + } + // If both values are provided, min must be less than or equal to max. + if (min !== null && max !== null && min >= max) { + return { salaryRangeInvalid: { message: 'Salary Range Invalid' } }; + } + return null; + }, + updateOn: 'change', + }, + ) as SkyDataGridNumberRangeFilterFormGroup; + + protected readonly formErrors = toSignal( + this.form.statusChanges.pipe( + map(() => Object.values(this.form.errors ?? {}) as SkyModalError[]), + ), + ); + protected autonumericOptions: SkyAutonumericOptions = 'dollarPos' as const; + + public apply(): void { + if (this.form.valid) { + const value = { + from: this.form.controls.from.value, + to: this.form.controls.to.value, + } as SkyDataGridNumberRangeFilterValue; + let displayValue = ''; + if (value.from && value.to) { + displayValue = `${this.#format(value.from)} - ${this.#format(value.to)}`; + } else if (value.from) { + displayValue = `From ${this.#format(value.from)}`; + } else if (value.to) { + displayValue = `Up to ${this.#format(value.to)}`; + } + this.modalInstance.save({ filterValue: { value, displayValue } }); + } else { + this.modalInstance.save({ filterValue: undefined }); + } + } + + public clear(): void { + this.modalInstance.save({ filterValue: undefined }); + } + + public cancel(): void { + this.modalInstance.cancel(); + } + + #format(value: number): string { + return this.#numeric.formatNumber(value, { format: 'currency' }); + } +} diff --git a/apps/playground/src/app/components/data-grid/filtered/start-date-filter-modal.component.ts b/apps/playground/src/app/components/data-grid/filtered/start-date-filter-modal.component.ts new file mode 100644 index 0000000000..156fdcbf52 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/filtered/start-date-filter-modal.component.ts @@ -0,0 +1,91 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + SkyDateRangeCalculation, + SkyDateRangeCalculatorId, + SkyDateRangePickerModule, +} from '@skyux/datetime'; +import { + SkyFilterBarFilterValue, + SkyFilterItemModal, + SkyFilterItemModalInstance, +} from '@skyux/filter-bar'; +import { SkyModalModule } from '@skyux/modals'; + +@Component({ + selector: 'app-start-date-filter-modal', + imports: [FormsModule, SkyDateRangePickerModule, SkyModalModule], + template: ` + + + + + + + + + + + `, +}) +export class StartDateFilterModalComponent implements SkyFilterItemModal { + public readonly modalInstance = inject(SkyFilterItemModalInstance); + readonly #context = this.modalInstance.context; + + public filterLabelText = this.#context.filterLabelText; + public dateRange: SkyDateRangeCalculation | undefined; + + constructor() { + const existingValue = this.#context.filterValue?.value as + | SkyDateRangeCalculation + | undefined; + if (existingValue) { + this.dateRange = existingValue; + } + } + + public apply(): void { + if ( + this.dateRange && + this.dateRange.calculatorId !== SkyDateRangeCalculatorId.AnyTime + ) { + const filterValue: SkyFilterBarFilterValue = { + value: this.dateRange, + displayValue: this.#formatDateRange(this.dateRange), + }; + this.modalInstance.save({ filterValue }); + } else { + this.modalInstance.save({ filterValue: undefined }); + } + } + + public clear(): void { + this.modalInstance.save({ filterValue: undefined }); + } + + public cancel(): void { + this.modalInstance.cancel(); + } + + #formatDateRange(range: SkyDateRangeCalculation): string { + if (range.startDate && range.endDate) { + const start = new Date(range.startDate).toLocaleDateString(); + const end = new Date(range.endDate).toLocaleDateString(); + return `${start} - ${end}`; + } else if (range.startDate) { + return `After ${new Date(range.startDate).toLocaleDateString()}`; + } else if (range.endDate) { + return `Before ${new Date(range.endDate).toLocaleDateString()}`; + } + return 'Date range applied'; + } +} diff --git a/apps/playground/src/app/components/data-grid/paging/grid-paging.component.html b/apps/playground/src/app/components/data-grid/paging/grid-paging.component.html new file mode 100644 index 0000000000..756e160132 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/paging/grid-paging.component.html @@ -0,0 +1,53 @@ +

Updates query string

+ + + + + + + {{ value.name }} + + + {{ value.name }} + + + +

No query string, just paging

+ + + + + + + {{ value.name }} + + + {{ value.name }} + + diff --git a/apps/playground/src/app/components/data-grid/paging/grid-paging.component.ts b/apps/playground/src/app/components/data-grid/paging/grid-paging.component.ts new file mode 100644 index 0000000000..a12a3cc6dc --- /dev/null +++ b/apps/playground/src/app/components/data-grid/paging/grid-paging.component.ts @@ -0,0 +1,18 @@ +import { Component, input } from '@angular/core'; +import { + SkyDataGridColumnComponent, + SkyDataGridComponent, +} from '@skyux/data-grid'; + +import { AG_GRID_DEMO_DATA } from '../../../shared/data-manager/data-manager-data'; + +@Component({ + selector: 'app-data-grid-paging', + imports: [SkyDataGridComponent, SkyDataGridColumnComponent], + templateUrl: './grid-paging.component.html', +}) +export default class GridPagingComponent { + public readonly page = input(); + + protected readonly data = AG_GRID_DEMO_DATA; +} diff --git a/apps/playground/src/app/components/grids/basic/grid.component.html b/apps/playground/src/app/components/grids/basic/grid.component.html new file mode 100644 index 0000000000..be6f4789a7 --- /dev/null +++ b/apps/playground/src/app/components/grids/basic/grid.component.html @@ -0,0 +1,242 @@ +

Grid

+ +
+ + + + + +
+ +

+ + +

+ +

Grid with multiselect enabled

+ + + + + + + +
+ + + + + + + +

Selected rows:

+

{{ selectedRowIdsDisplay }}

+
+ +

Grid with inline help

+ +
+ + + + + + + + This is a numeric column. Click here to learn more. + + + + This is a string column. Click here to learn more. + +
+ +

Grid with scroll bars

+ +
+ + + + + +
+ +

Grid with row delete

+ +
+ + + + + @if (row.id) { + + + + + + + + } + + + + + + + + + This is a numeric column. Click here to learn more. + + + + This is a string column. Click here to learn more. + +
+ +

Grid w/ aligned columns

+ +
+ + + + + +
+ +

Grid w/ aligned columns and inline help

+ +
+ + + + + +
diff --git a/apps/playground/src/app/components/grids/basic/grid.component.scss b/apps/playground/src/app/components/grids/basic/grid.component.scss new file mode 100644 index 0000000000..cf3267ce27 --- /dev/null +++ b/apps/playground/src/app/components/grids/basic/grid.component.scss @@ -0,0 +1,5 @@ +h1 { + // Margins throw off the screenshots; use padding for the same effect. + margin: 0; + padding: 15px 0; +} diff --git a/apps/playground/src/app/components/grids/basic/grid.component.ts b/apps/playground/src/app/components/grids/basic/grid.component.ts new file mode 100644 index 0000000000..96e523acc3 --- /dev/null +++ b/apps/playground/src/app/components/grids/basic/grid.component.ts @@ -0,0 +1,216 @@ +import { Component, ViewChild } from '@angular/core'; +import { + SkyGridMessage, + SkyGridMessageType, + SkyGridModule, + SkyGridRowDeleteCancelArgs, + SkyGridRowDeleteConfirmArgs, + SkyGridSelectedRowsModelChange, +} from '@skyux/grids'; +import { ListSortFieldSelectorModel } from '@skyux/list-builder-common'; +import { SkyDropdownModule, SkyPopoverModule } from '@skyux/popovers'; + +import { Subject } from 'rxjs'; + +@Component({ + selector: 'app-grid', + templateUrl: './grid.component.html', + styleUrls: ['./grid.component.scss'], + imports: [SkyGridModule, SkyPopoverModule, SkyDropdownModule], +}) +export default class GridComponent { + public asyncPopover: any; + + public dataForRowDeleteGrid: any = [ + { id: '1', column1: '1', column2: 'Apple', column3: 'aa' }, + { id: '2', column1: '01', column2: 'Banana', column3: 'bb' }, + { id: '3', column1: '11', column2: 'Banana', column3: 'cc' }, + { id: '4', column1: '12', column2: 'Daikon', column3: 'dd' }, + { id: '5', column1: '13', column2: 'Edamame', column3: 'ee' }, + { id: '6', column1: '20', column2: 'Fig', column3: 'ff' }, + { id: '7', column1: '21', column2: 'Grape', column3: 'gg' }, + ]; + + public dataForSimpleGrid = [ + { id: '1', column1: '1', column2: 'Apple', column3: 'aa' }, + { id: '2', column1: '01', column2: 'Banana', column3: 'bb' }, + { id: '3', column1: '11', column2: 'Banana', column3: 'cc' }, + { id: '4', column1: '12', column2: 'Daikon', column3: 'dd' }, + { id: '5', column1: '13', column2: 'Edamame', column3: 'ee' }, + { id: '6', column1: '20', column2: 'Fig', column3: 'ff' }, + { id: '7', column1: '21', column2: 'Grape', column3: 'gg' }, + ]; + + public dataForSimpleGridWithMultiselect = [ + { id: '1', column1: '1', column2: 'Apple', column3: 'aa', myId: '101' }, + { id: '2', column1: '01', column2: 'Banana', column3: 'bb', myId: '102' }, + { id: '3', column1: '11', column2: 'Banana', column3: 'cc', myId: '103' }, + { id: '4', column1: '12', column2: 'Daikon', column3: 'dd', myId: '104' }, + { id: '5', column1: '13', column2: 'Edamame', column3: 'ee', myId: '105' }, + { id: '6', column1: '20', column2: 'Fig', column3: 'ff', myId: '106' }, + { id: '7', column1: '21', column2: 'Grape', column3: 'gg', myId: '107' }, + ]; + + public columnsForSimpleGrid = ['column1', 'column2', 'column3']; + + public sortFieldForSimpleGrid: ListSortFieldSelectorModel = { + descending: true, + fieldSelector: 'column2', + }; + + public gridController = new Subject(); + + public gridRowDeleteController = new Subject(); + + public highlightText: string; + + public rowHighlightedId: string; + + public selectedRowIds: string[]; + + public selectedRowIdsDisplay: string[]; + + public selectedRows: string; + + @ViewChild('asyncPopoverRef') + private popoverTemplate: any; + + constructor() { + setTimeout(() => { + this.asyncPopover = this.popoverTemplate; + }, 1000); + } + + public toggleCol3(): void { + const col3Index = this.columnsForSimpleGrid.indexOf('column3'); + if (col3Index === -1) { + this.columnsForSimpleGrid.push('column3'); + } else { + this.columnsForSimpleGrid.splice(col3Index, 1); + } + this.columnsForSimpleGrid = [...this.columnsForSimpleGrid]; + } + + public sortChangedSimpleGrid(activeSort: ListSortFieldSelectorModel): void { + this.dataForSimpleGrid = this.performSort( + activeSort, + this.dataForSimpleGrid, + ); + } + + public sortChangedMultiselectGrid( + activeSort: ListSortFieldSelectorModel, + ): void { + this.dataForSimpleGridWithMultiselect = this.performSort( + activeSort, + this.dataForSimpleGridWithMultiselect, + ); + } + + public triggerTextHighlight(): void { + if (!this.highlightText) { + this.highlightText = 'e'; + } else { + this.highlightText = undefined; + } + } + + public triggerRowHighlight(): void { + if (!this.rowHighlightedId) { + this.rowHighlightedId = '2'; + } else { + this.rowHighlightedId = undefined; + } + } + + public onMultiselectSelectionChange( + value: SkyGridSelectedRowsModelChange, + ): void { + this.selectedRowIdsDisplay = value.selectedRowIds; + } + + public selectAll(): void { + this.sendMessage(SkyGridMessageType.SelectAll); + } + + public clearAll(): void { + this.sendMessage(SkyGridMessageType.ClearAll); + } + + public cancelRowDelete(cancelArgs: SkyGridRowDeleteCancelArgs): void { + console.log('Item with id ' + cancelArgs.id + ' has not been deleted'); + } + + public deleteItem(id: string): void { + this.gridRowDeleteController.next({ + type: SkyGridMessageType.PromptDeleteRow, + data: { + promptDeleteRow: { + id: id, + }, + }, + }); + } + + public finishRowDelete(confirmArgs: SkyGridRowDeleteConfirmArgs): void { + setTimeout(() => { + console.log('Item with id ' + confirmArgs.id + ' has been deleted'); + // IF WORKED + this.dataForRowDeleteGrid = this.dataForRowDeleteGrid.filter( + (data: any) => data.id !== confirmArgs.id, + ); + }, 5000); + } + + public selectRow(): void { + this.selectedRowIds = ['101', '103', '105']; + } + + public onSelectedColumnIdsChange(event: any[]): void { + console.log(event); + } + + public onColumnWidthChange(event: any): void { + console.log(event); + } + + private performSort( + activeSort: ListSortFieldSelectorModel, + data: any[], + ): Array { + const sortField = activeSort.fieldSelector; + const descending = activeSort.descending; + + return data + .sort((a: any, b: any) => { + let value1 = a[sortField]; + let value2 = b[sortField]; + + if (value1 && typeof value1 === 'string') { + value1 = value1.toLowerCase(); + } + + if (value2 && typeof value2 === 'string') { + value2 = value2.toLowerCase(); + } + + if (value1 === value2) { + return 0; + } + + let result = value1 > value2 ? 1 : -1; + + if (descending) { + result *= -1; + } + + return result; + }) + .slice(); + } + + private sendMessage(type: SkyGridMessageType): void { + const message: SkyGridMessage = { type }; + this.gridController.next(message); + } +} diff --git a/apps/playground/src/app/components/grids/grids.module.ts b/apps/playground/src/app/components/grids/grids.module.ts new file mode 100644 index 0000000000..8d59f1a9c4 --- /dev/null +++ b/apps/playground/src/app/components/grids/grids.module.ts @@ -0,0 +1,47 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { ComponentRouteInfo } from '../../shared/component-info/component-route-info'; + +const routes: ComponentRouteInfo[] = [ + { + path: 'basic', + loadComponent: () => import('./basic/grid.component'), + data: { + name: 'Grid', + icon: 'table', + library: 'grids', + }, + }, + { + path: 'paging', + loadComponent: () => import('./paging/grid-paging.component'), + data: { + name: 'Grid Paging', + icon: 'table', + library: 'grids', + }, + }, + { + path: 'search', + loadComponent: () => import('./search/grid-search.component'), + data: { + name: 'Grid Search', + icon: 'table', + library: 'grids', + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +class GridsRoutingModule {} + +@NgModule({ + imports: [GridsRoutingModule], +}) +export class GridsModule { + public static routes = routes; +} diff --git a/apps/playground/src/app/components/grids/paging/grid-paging.component.html b/apps/playground/src/app/components/grids/paging/grid-paging.component.html new file mode 100644 index 0000000000..4e85feca8e --- /dev/null +++ b/apps/playground/src/app/components/grids/paging/grid-paging.component.html @@ -0,0 +1,18 @@ + + + + + + + {{ value.name }} + + + {{ value.name }} + + diff --git a/apps/playground/src/app/components/grids/paging/grid-paging.component.ts b/apps/playground/src/app/components/grids/paging/grid-paging.component.ts new file mode 100644 index 0000000000..3ecdbc3b7d --- /dev/null +++ b/apps/playground/src/app/components/grids/paging/grid-paging.component.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { SkyGridModule } from '@skyux/grids'; + +import { AG_GRID_DEMO_DATA } from '../../../shared/data-manager/data-manager-data'; + +@Component({ + selector: 'app-grid-paging', + imports: [CommonModule, SkyGridModule], + templateUrl: './grid-paging.component.html', +}) +export default class GridPagingComponent { + protected readonly data = AG_GRID_DEMO_DATA; +} diff --git a/apps/playground/src/app/components/grids/search/grid-search.component.html b/apps/playground/src/app/components/grids/search/grid-search.component.html new file mode 100644 index 0000000000..4e85feca8e --- /dev/null +++ b/apps/playground/src/app/components/grids/search/grid-search.component.html @@ -0,0 +1,18 @@ + + + + + + + {{ value.name }} + + + {{ value.name }} + + diff --git a/apps/playground/src/app/components/grids/search/grid-search.component.ts b/apps/playground/src/app/components/grids/search/grid-search.component.ts new file mode 100644 index 0000000000..60a5f46951 --- /dev/null +++ b/apps/playground/src/app/components/grids/search/grid-search.component.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { SkyGridModule } from '@skyux/grids'; + +import { AG_GRID_DEMO_DATA } from '../../../shared/data-manager/data-manager-data'; + +@Component({ + selector: 'app-grid-search', + imports: [CommonModule, SkyGridModule], + templateUrl: './grid-search.component.html', +}) +export default class GridSearchComponent { + protected readonly data = AG_GRID_DEMO_DATA; +} diff --git a/apps/playground/src/app/components/list-builder/list-builder.module.ts b/apps/playground/src/app/components/list-builder/list-builder.module.ts new file mode 100644 index 0000000000..ef4c99d089 --- /dev/null +++ b/apps/playground/src/app/components/list-builder/list-builder.module.ts @@ -0,0 +1,29 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { ComponentRouteInfo } from '../../shared/component-info/component-route-info'; + +const routes: ComponentRouteInfo[] = [ + { + path: 'grid', + loadComponent: () => import('./list-view-grid/list-view-grid.component'), + data: { + name: 'List Builder Grid', + icon: 'table', + library: 'list-builder-view-grids', + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +class ListBuilderRoutingModule {} + +@NgModule({ + imports: [ListBuilderRoutingModule], +}) +export class ListBuilderModule { + public static routes = routes; +} diff --git a/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.html b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.html new file mode 100644 index 0000000000..5424aca70b --- /dev/null +++ b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.html @@ -0,0 +1,134 @@ +
+ + + + + + + + + + + + +

+ +

+
+ +
+ + + + + + + + + + + +
+ +

List-view-grid with inline delete

+ +
+ + + + + + + + + + @if (row.id) { + + } + + + + + + + +
diff --git a/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts new file mode 100644 index 0000000000..0c24293e7c --- /dev/null +++ b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts @@ -0,0 +1,108 @@ +import { Component } from '@angular/core'; +import { SkyAlertModule } from '@skyux/indicators'; +import { SkyListModule, SkyListToolbarModule } from '@skyux/list-builder'; +import { + SkyListViewGridMessage, + SkyListViewGridMessageType, + SkyListViewGridModule, + SkyListViewGridRowDeleteCancelArgs, + SkyListViewGridRowDeleteConfirmArgs, +} from '@skyux/list-builder-view-grids'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { BehaviorSubject, Subject } from 'rxjs'; + +@Component({ + selector: 'app-list-view-grid', + standalone: true, + templateUrl: './list-view-grid.component.html', + imports: [ + SkyAlertModule, + SkyDropdownModule, + SkyListModule, + SkyListViewGridModule, + SkyListToolbarModule, + ], +}) +export default class ListViewGridTestComponent { + public gridController = new Subject(); + + public itemSubject: BehaviorSubject = new BehaviorSubject([]); + + public items = this.itemSubject.asObservable(); + + public rowHighlightedId: string; + + public selectedIds: string[]; + + private defaultItems = [ + { id: '1', column1: 101, column2: 'Apple', column3: 'Anne eats apples' }, + { id: '2', column1: 202, column2: 'Banana', column3: 'Ben eats bananas' }, + { id: '3', column1: 303, column2: 'Pear', column3: 'Patty eats pears' }, + { id: '4', column1: 404, column2: 'Grape', column3: 'George eats grapes' }, + { id: '5', column1: 505, column2: 'Banana', column3: 'Becky eats bananas' }, + { id: '6', column1: 606, column2: 'Lemon', column3: 'Larry eats lemons' }, + { + id: '7', + column1: 707, + column2: 'Strawberry', + column3: 'Sally eats strawberries', + }, + ]; + + constructor() { + this.itemSubject.next(this.defaultItems); + } + + public onToggleRowHighlightClick(): void { + this.rowHighlightedId = this.rowHighlightedId ? undefined : '2'; + } + + public onSelectedIdsChange(event: any): void { + console.log(event); + } + + public onRowDeleteCancel( + cancelArgs: SkyListViewGridRowDeleteCancelArgs, + ): void { + this.gridController.next({ + type: SkyListViewGridMessageType.AbortDeleteRow, + data: { + abortDeleteRow: { + id: cancelArgs.id, + }, + }, + }); + } + + public onRowDeleteConfirm( + confirmArgs: SkyListViewGridRowDeleteConfirmArgs, + ): void { + const removeIndex = this.defaultItems + .map((item) => { + return item.id; + }) + .indexOf(confirmArgs.id); + + if (removeIndex) { + setTimeout(() => { + this.defaultItems = this.defaultItems.filter( + (data: any) => data.id !== confirmArgs.id, + ); + this.itemSubject.next(this.defaultItems); + console.log('Item with id ' + confirmArgs.id + ' has been deleted.'); + }, 1000); + } + } + + public onDeleteItemClick(id: string): void { + this.gridController.next({ + type: SkyListViewGridMessageType.PromptDeleteRow, + data: { + promptDeleteRow: { + id: id, + }, + }, + }); + } +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.ts index 2734f19abf..f75d6a8c27 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.ts @@ -46,6 +46,8 @@ function toColumnWidthName(breakpoint: SkyBreakpoint): 'xs' | 'sm' { return breakpoint === 'xs' ? 'xs' : 'sm'; } +const RESERVED_COLUMNS = ['ag-Grid-SelectionColumn']; + /** * Connects `SkyAgGridWrapperComponent` with a `SkyDataViewComponent` to control the grid using a `SkyDataManagerService` instance. */ @@ -403,7 +405,11 @@ export class SkyAgGridDataManagerAdapterDirective implements OnDestroy { const hideColumns = agGrid.api .getColumnState() .map((col) => col.colId) - .filter((colId) => !displayedColumnIds.includes(colId)); + .filter( + (colId) => + !displayedColumnIds.includes(colId) && + !RESERVED_COLUMNS.includes(colId), + ); agGrid.api.setColumnsVisible(hideColumns, false); agGrid.api.setColumnsVisible(displayedColumnIds, true); diff --git a/libs/components/ag-grid/src/lib/styles/ag-grid-theme.ts b/libs/components/ag-grid/src/lib/styles/ag-grid-theme.ts index fdf35a95cb..863176cc27 100644 --- a/libs/components/ag-grid/src/lib/styles/ag-grid-theme.ts +++ b/libs/components/ag-grid/src/lib/styles/ag-grid-theme.ts @@ -24,6 +24,10 @@ const defaultsForAllThemes: Partial = { 'var(--sky-override-switch-checked-color, var(--sky-color-icon-inverse, var(--sky-text-color-default)))', checkboxIndeterminateBackgroundColor: 'var(--sky-override-switch-checked-color, var(--sky-color-icon-inverse, transparent))', + checkboxIndeterminateBorderColor: `var( + --sky-override-ag-grid-checkbox-unchecked-border-color, + var(--sky-color-border-switch-base) + )`, checkboxUncheckedBackgroundColor: 'var(--sky-override-ag-grid-checkbox-unchecked-background-color, var(--sky-color-background-input-base))', checkboxUncheckedBorderColor: `var( diff --git a/libs/components/code-examples/package.json b/libs/components/code-examples/package.json index fd6d28626c..25b02fe3ec 100644 --- a/libs/components/code-examples/package.json +++ b/libs/components/code-examples/package.json @@ -30,6 +30,7 @@ "@skyux/avatar": "0.0.0-PLACEHOLDER", "@skyux/colorpicker": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", + "@skyux/data-grid": "0.0.0-PLACEHOLDER", "@skyux/data-manager": "0.0.0-PLACEHOLDER", "@skyux/datetime": "0.0.0-PLACEHOLDER", "@skyux/errors": "0.0.0-PLACEHOLDER", diff --git a/libs/components/code-examples/project.json b/libs/components/code-examples/project.json index 9903ca20af..58ed832e87 100644 --- a/libs/components/code-examples/project.json +++ b/libs/components/code-examples/project.json @@ -40,6 +40,7 @@ "tsConfig": "libs/components/code-examples/tsconfig.spec.json", "karmaConfig": "libs/components/code-examples/karma.conf.js", "styles": [ + "libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss", "libs/components/theme/src/lib/styles/sky.scss", "libs/components/theme/src/lib/styles/themes/modern/styles.scss" ], diff --git a/libs/components/code-examples/routes/src/index.ts b/libs/components/code-examples/routes/src/index.ts index afc6a12efd..1d41e5e10b 100644 --- a/libs/components/code-examples/routes/src/index.ts +++ b/libs/components/code-examples/routes/src/index.ts @@ -225,6 +225,34 @@ export const routes: Routes = [ ({ CoreNumericBasicExampleComponent: c }) => c, ), }, + { + path: 'DataGridAsynchronousDataComponent', + loadComponent: () => + import('@skyux/code-examples').then( + ({ DataGridAsynchronousDataComponent: c }) => c, + ), + }, + { + path: 'DataGridBasicExampleComponent', + loadComponent: () => + import('@skyux/code-examples').then( + ({ DataGridBasicExampleComponent: c }) => c, + ), + }, + { + path: 'DataGridDataManagerExampleComponent', + loadComponent: () => + import('@skyux/code-examples').then( + ({ DataGridDataManagerExampleComponent: c }) => c, + ), + }, + { + path: 'DataGridPagingComponent', + loadComponent: () => + import('@skyux/code-examples').then( + ({ DataGridPagingComponent: c }) => c, + ), + }, { path: 'DataManagerBasicExampleComponent', loadComponent: () => @@ -323,6 +351,13 @@ export const routes: Routes = [ ({ FilterBarSelectableExampleComponent: c }) => c, ), }, + { + path: 'FilteredDataGridComponent', + loadComponent: () => + import('@skyux/code-examples').then( + ({ FilteredDataGridComponent: c }) => c, + ), + }, { path: 'FlyoutBasicExampleComponent', loadComponent: () => diff --git a/libs/components/code-examples/src/index.ts b/libs/components/code-examples/src/index.ts index 78af68d8e0..c639aa936a 100644 --- a/libs/components/code-examples/src/index.ts +++ b/libs/components/code-examples/src/index.ts @@ -29,6 +29,11 @@ export { CoreIdExampleComponent } from './lib/modules/core/id/example.component' export { CoreMediaQueryBasicExampleComponent } from './lib/modules/core/media-query/basic/example.component'; export { CoreMediaQueryResponsiveHostExampleComponent } from './lib/modules/core/media-query/responsive-host/example.component'; export { CoreNumericBasicExampleComponent } from './lib/modules/core/numeric/basic/example.component'; +export { DataGridBasicExampleComponent } from './lib/modules/data-grid/basic/example.component'; +export { FilteredDataGridComponent } from './lib/modules/data-grid/filtered/example.component'; +export { DataGridPagingComponent } from './lib/modules/data-grid/paging/example.component'; +export { DataGridDataManagerExampleComponent } from './lib/modules/data-grid/data-manager/example.component'; +export { DataGridAsynchronousDataComponent } from './lib/modules/data-grid/asynchronous-data/example.component'; export { DataManagerBasicExampleComponent } from './lib/modules/data-manager/data-manager/basic/example.component'; export { DatetimeDatePipeBasicExampleComponent } from './lib/modules/datetime/date-pipe/basic/example.component'; export { DatetimeDateRangePickerBasicExampleComponent } from './lib/modules/datetime/date-range-picker/basic/example.component'; diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/data-sort-and-filter.ts b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/data-sort-and-filter.ts new file mode 100644 index 0000000000..f2f007d1e3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/data-sort-and-filter.ts @@ -0,0 +1,138 @@ +import { SkyDataGridNumberRangeFilterValue } from '@skyux/data-grid'; +import { SkyDataManagerSortOption } from '@skyux/data-manager'; +import { SkyDateRange } from '@skyux/datetime'; +import { SkyFilterStateFilterItem } from '@skyux/lists'; + +import { Employee } from './data'; + +export function dataSortAndFilter( + allEmployees: Employee[], + appliedFilters: SkyFilterStateFilterItem< + string | SkyDataGridNumberRangeFilterValue | SkyDateRange | boolean + >[], + sort: SkyDataManagerSortOption | undefined, + searchText: string | undefined, +): Employee[] { + const normalSearch = searchText?.normalize().toLowerCase(); + const records = allEmployees.filter((employee: Employee) => { + let includeEmployee = true; + if (normalSearch) { + includeEmployee &&= Object.values(employee).some((value) => + String(value ?? '') + .normalize() + .toLowerCase() + .includes(normalSearch), + ); + } + for (const filterItem of appliedFilters) { + switch (filterItem.filterId) { + case 'name': + includeEmployee &&= filterByName( + filterItem as SkyFilterStateFilterItem, + employee, + ); + break; + case 'salary': + includeEmployee &&= filterBySalary( + filterItem as SkyFilterStateFilterItem, + employee, + ); + break; + case 'startDate': + includeEmployee &&= filterByDate( + filterItem as SkyFilterStateFilterItem, + employee, + ); + break; + case 'hideInactive': + includeEmployee &&= filterByActive( + filterItem as SkyFilterStateFilterItem, + employee, + ); + break; + default: + } + } + return includeEmployee; + }); + if (sort) { + switch (sort.propertyName) { + case 'name': + records.sort( + (a, b) => a.name.localeCompare(b.name) * (sort.descending ? -1 : 1), + ); + break; + case 'salary': + records.sort( + (a, b) => (a.salary - b.salary) * (sort.descending ? -1 : 1), + ); + break; + case 'startDate': + records.sort( + (a, b) => + (new Date(a.startDate).getTime() - + new Date(b.startDate).getTime()) * + (sort.descending ? -1 : 1), + ); + break; + case 'active': + records.sort( + (a, b) => + (Number(a.active) - Number(b.active)) * (sort.descending ? -1 : 1), + ); + break; + default: + } + } + return records; +} + +function filterByName( + filterItem: SkyFilterStateFilterItem, + employee: Employee, +): boolean { + return ( + !filterItem.filterValue?.value || + employee.name + .normalize() + .toLowerCase() + .includes(filterItem.filterValue.value.normalize().toLowerCase()) + ); +} + +function filterBySalary( + filterItem: SkyFilterStateFilterItem, + employee: Employee, +): boolean { + const filterValue: SkyDataGridNumberRangeFilterValue | undefined = + filterItem.filterValue?.value; + return ( + !filterValue || + ((Number.isNaN(Number(filterValue.from ?? undefined)) || + employee.salary >= Number(filterValue.from)) && + (Number.isNaN(Number(filterValue.to ?? undefined)) || + employee.salary <= Number(filterValue.to))) + ); +} + +function filterByDate( + filterItem: SkyFilterStateFilterItem, + employee: Employee, +): boolean { + const filterValue: SkyDateRange | undefined = filterItem.filterValue?.value; + // This is simplistic and does not account for time zones. + return ( + !filterValue || + ((!filterValue.startDate || + new Date(employee.startDate) >= filterValue.startDate) && + (!filterValue.endDate || + new Date(employee.startDate) <= filterValue.endDate)) + ); +} + +function filterByActive( + filterItem: SkyFilterStateFilterItem, + employee: Employee, +): boolean { + return !filterItem.filterValue?.value || employee.active; +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/data.ts b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/data.ts new file mode 100644 index 0000000000..169d6792a7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/data.ts @@ -0,0 +1,130 @@ +import { + SkyDataGridNumberRangeFilterValue, + SkyDataGridPageRequest, +} from '@skyux/data-grid'; +import { SkyDataManagerState } from '@skyux/data-manager'; +import { SkyDateRange } from '@skyux/datetime'; +import { SkyFilterState, SkyFilterStateFilterItem } from '@skyux/lists'; + +import { Observable, delay, of } from 'rxjs'; + +import { dataSortAndFilter } from './data-sort-and-filter'; + +export interface Employee { + id: string; + name: string; + department: string; + salary: number; + startDate: string; + active: boolean; +} + +const employees = [ + { + id: '1', + name: 'Alice Johnson', + department: 'Engineering', + salary: 95000, + startDate: '2020-03-15', + active: true, + }, + { + id: '2', + name: 'Bob Smith', + department: 'Sales', + salary: 72000, + startDate: '2019-07-22', + active: true, + }, + { + id: '3', + name: 'Carol Williams', + department: 'Engineering', + salary: 110000, + startDate: '2018-01-10', + active: true, + }, + { + id: '4', + name: 'David Brown', + department: 'Marketing', + salary: 65000, + startDate: '2021-09-01', + active: false, + }, + { + id: '5', + name: 'Eva Martinez', + department: 'Engineering', + salary: 88000, + startDate: '2020-11-30', + active: true, + }, + { + id: '6', + name: 'Frank Garcia', + department: 'Sales', + salary: 78000, + startDate: '2017-04-18', + active: true, + }, + { + id: '7', + name: 'Grace Lee', + department: 'Marketing', + salary: 71000, + startDate: '2022-02-14', + active: true, + }, + { + id: '8', + name: 'Henry Wilson', + department: 'Engineering', + salary: 125000, + startDate: '2016-08-05', + active: false, + }, + { + id: '9', + name: 'Ivy Chen', + department: 'Sales', + salary: 82000, + startDate: '2019-12-20', + active: true, + }, + { + id: '10', + name: 'Jack Taylor', + department: 'Marketing', + salary: 58000, + startDate: '2023-01-09', + active: true, + }, +]; + +export function remoteService(params: { + dataManagerUpdates: SkyDataManagerState | undefined; + pageRequest: SkyDataGridPageRequest | undefined; +}): Observable<{ data: Employee[] | null; count: number } | null> { + if (params.pageRequest?.pageSize) { + const pageNumber = params.pageRequest.pageNumber; + const pageSize = params.pageRequest.pageSize; + const data = dataSortAndFilter( + employees, + (( + params.dataManagerUpdates?.filterData?.filters as + | SkyFilterState + | undefined + )?.appliedFilters ?? []) as SkyFilterStateFilterItem< + string | SkyDataGridNumberRangeFilterValue | SkyDateRange | boolean + >[], + params.dataManagerUpdates?.activeSortOption, + params.dataManagerUpdates?.searchText ?? '', + ); + return of({ + data: data.slice((pageNumber - 1) * pageSize, pageNumber * pageSize), + count: data.length, + }).pipe(delay(800)); + } + return of(null).pipe(delay(800)); +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/example.component.html b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/example.component.html new file mode 100644 index 0000000000..0aff6322ed --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/example.component.html @@ -0,0 +1,87 @@ +@let records = recordsToShow | async; + + + + + + + + + + + + + + + + + + + {{ + value + | skyNumeric: { format: 'currency', iso: 'USD', truncate: false } + }} + + + {{ + value | skyDate: 'mediumDate' + }} + + + + + diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/example.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/example.component.ts new file mode 100644 index 0000000000..c0c2af5077 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/example.component.ts @@ -0,0 +1,104 @@ +import { AsyncPipe } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + signal, +} from '@angular/core'; +import { toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { SkyNumericPipe } from '@skyux/core'; +import { SkyDataGridModule, SkyDataGridPageRequest } from '@skyux/data-grid'; +import { + SkyDataManagerModule, + SkyDataManagerService, + SkyDataManagerState, +} from '@skyux/data-manager'; +import { SkyDatePipe } from '@skyux/datetime'; +import { SkyFilterBarModule } from '@skyux/filter-bar'; +import { SkyListSummaryModule } from '@skyux/lists'; + +import { switchMap } from 'rxjs'; + +import { remoteService } from './data'; +import { HideInactiveFilterModalComponent } from './hide-inactive-filter-modal.component'; +import { NameFilterModalComponent } from './name-filter-modal.component'; +import { SalaryFilterModalComponent } from './salary-filter-modal.component'; +import { StartDateFilterModalComponent } from './start-date-filter-modal.component'; + +/** + * @title Asynchronous data loading + */ +@Component({ + selector: 'app-data-grid-asynchronous-data', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SkyDataManagerService], + imports: [ + SkyDataGridModule, + SkyDataManagerModule, + SkyDatePipe, + SkyFilterBarModule, + SkyListSummaryModule, + SkyNumericPipe, + AsyncPipe, + ], + templateUrl: './example.component.html', +}) +export class DataGridAsynchronousDataComponent { + protected readonly nameFilterModal = NameFilterModalComponent; + protected readonly salaryFilterModal = SalaryFilterModalComponent; + protected readonly hideInactiveModal = HideInactiveFilterModalComponent; + protected readonly startDateFilterModal = StartDateFilterModalComponent; + protected readonly pageRequest = signal( + undefined, + ); + + protected readonly viewId = 'dataGridWithCustomFilters' as const; + + // Computed client side in this example, but could be an HTTP resource where parameters are sent to the server for determining data to show. + readonly #requestParameters = toObservable( + computed(() => ({ + dataManagerUpdates: this.#dataManagerUpdates(), + pageRequest: this.pageRequest(), + })), + ); + protected readonly recordsToShow = this.#requestParameters.pipe( + switchMap(remoteService), + ); + + readonly #dataManagerSvc = inject(SkyDataManagerService); + readonly #dataManagerUpdates = toSignal( + this.#dataManagerSvc.getDataStateUpdates('dataManagerUpdates'), + ); + + constructor() { + this.#dataManagerSvc.initDataManager({ + activeViewId: this.viewId, + dataManagerConfig: {}, + defaultDataState: new SkyDataManagerState({ + filterData: { + filters: {}, + }, + views: [ + { + viewId: this.viewId, + displayedColumnIds: [ + 'name', + 'department', + 'salary', + 'startDate', + 'active', + ], + }, + ], + }), + }); + this.#dataManagerSvc.initDataView({ + id: this.viewId, + name: 'Data Grid View', + iconName: 'table', + searchEnabled: true, + columnPickerEnabled: true, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/hide-inactive-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/hide-inactive-filter-modal.component.ts new file mode 100644 index 0000000000..fc5ac9242e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/hide-inactive-filter-modal.component.ts @@ -0,0 +1,50 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + SkyFilterBarFilterValue, + SkyFilterItemModal, + SkyFilterItemModalInstance, +} from '@skyux/filter-bar'; +import { SkyCheckboxModule } from '@skyux/forms'; +import { SkyModalModule } from '@skyux/modals'; + +@Component({ + selector: 'app-hide-inactive-filter-modal', + imports: [FormsModule, SkyCheckboxModule, SkyModalModule], + template: ` + + + + + + + + + + `, +}) +export class HideInactiveFilterModalComponent implements SkyFilterItemModal { + public readonly modalInstance = inject(SkyFilterItemModalInstance); + readonly #context = this.modalInstance.context; + + public filterLabelText = this.#context.filterLabelText; + public hideInactive = !!this.#context.filterValue?.value; + + public apply(): void { + const filterValue: SkyFilterBarFilterValue | undefined = this.hideInactive + ? { value: true, displayValue: 'Showing active only' } + : undefined; + this.modalInstance.save({ filterValue }); + } + + public cancel(): void { + this.modalInstance.cancel(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/name-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/name-filter-modal.component.ts new file mode 100644 index 0000000000..57dcb25918 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/name-filter-modal.component.ts @@ -0,0 +1,52 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + SkyFilterBarFilterValue, + SkyFilterItemModal, + SkyFilterItemModalInstance, +} from '@skyux/filter-bar'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyModalModule } from '@skyux/modals'; + +@Component({ + selector: 'app-name-filter-modal', + imports: [FormsModule, SkyInputBoxModule, SkyModalModule], + template: ` + + + + + + + + + + + + `, +}) +export class NameFilterModalComponent implements SkyFilterItemModal { + public readonly modalInstance = inject(SkyFilterItemModalInstance); + readonly #context = this.modalInstance.context; + + public filterLabelText = this.#context.filterLabelText; + public nameFilter = (this.#context.filterValue?.value as string) ?? ''; + + public apply(): void { + const filterValue: SkyFilterBarFilterValue | undefined = this.nameFilter + ? { + value: this.nameFilter, + displayValue: `Name contains "${this.nameFilter}"`, + } + : undefined; + this.modalInstance.save({ filterValue }); + } + + public cancel(): void { + this.modalInstance.cancel(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/salary-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/salary-filter-modal.component.ts new file mode 100644 index 0000000000..3675432045 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/salary-filter-modal.component.ts @@ -0,0 +1,146 @@ +import { Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { + SkyAutonumericModule, + SkyAutonumericOptions, +} from '@skyux/autonumeric'; +import { SkyNumericService } from '@skyux/core'; +import { + SkyDataGridNumberRangeFilterFormGroup, + SkyDataGridNumberRangeFilterValue, +} from '@skyux/data-grid'; +import { + SkyFilterItemModal, + SkyFilterItemModalInstance, +} from '@skyux/filter-bar'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyModalError, SkyModalModule } from '@skyux/modals'; + +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'app-salary-filter-modal', + imports: [ + ReactiveFormsModule, + SkyInputBoxModule, + SkyModalModule, + SkyAutonumericModule, + ], + template: ` +
+ + +
+ + + +
+
+ + + +
+
+ + + + + +
+
+ `, +}) +export class SalaryFilterModalComponent implements SkyFilterItemModal { + public readonly modalInstance = inject(SkyFilterItemModalInstance); + readonly #context = this.modalInstance.context; + readonly #fb = inject(FormBuilder); + readonly #existingValue = this.#context.filterValue?.value as + | SkyDataGridNumberRangeFilterValue + | undefined; + readonly #numeric = inject(SkyNumericService); + + public filterLabelText = this.#context.filterLabelText; + + protected readonly form = this.#fb.group( + { + from: this.#fb.control(this.#existingValue?.from ?? null), + to: this.#fb.control(this.#existingValue?.to ?? null), + }, + { + validators: (formGroup: SkyDataGridNumberRangeFilterFormGroup) => { + const min = formGroup.controls.from.value; + const max = formGroup.controls.to.value; + // At least one value must be provided. + if (min === null && max === null) { + return { + salaryRangeRequired: { message: 'Salary Range required' }, + }; + } + // If both values are provided, min must be less than or equal to max. + if (min !== null && max !== null && min >= max) { + return { salaryRangeInvalid: { message: 'Salary Range Invalid' } }; + } + return null; + }, + updateOn: 'change', + }, + ) as SkyDataGridNumberRangeFilterFormGroup; + + protected readonly formErrors = toSignal( + this.form.statusChanges.pipe( + map(() => Object.values(this.form.errors ?? {}) as SkyModalError[]), + ), + ); + protected autonumericOptions: SkyAutonumericOptions = 'dollarPos' as const; + + public apply(): void { + if (this.form.valid) { + const value = { + from: this.form.controls.from.value, + to: this.form.controls.to.value, + } as SkyDataGridNumberRangeFilterValue; + let displayValue = ''; + if (value.from && value.to) { + displayValue = `${this.#format(value.from)} - ${this.#format(value.to)}`; + } else if (value.from) { + displayValue = `From ${this.#format(value.from)}`; + } else if (value.to) { + displayValue = `Up to ${this.#format(value.to)}`; + } + this.modalInstance.save({ filterValue: { value, displayValue } }); + } else { + this.modalInstance.save({ filterValue: undefined }); + } + } + + public clear(): void { + this.modalInstance.save({ filterValue: undefined }); + } + + public cancel(): void { + this.modalInstance.cancel(); + } + + #format(value: number): string { + return this.#numeric.formatNumber(value, { format: 'currency' }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/start-date-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/start-date-filter-modal.component.ts new file mode 100644 index 0000000000..156fdcbf52 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/start-date-filter-modal.component.ts @@ -0,0 +1,91 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + SkyDateRangeCalculation, + SkyDateRangeCalculatorId, + SkyDateRangePickerModule, +} from '@skyux/datetime'; +import { + SkyFilterBarFilterValue, + SkyFilterItemModal, + SkyFilterItemModalInstance, +} from '@skyux/filter-bar'; +import { SkyModalModule } from '@skyux/modals'; + +@Component({ + selector: 'app-start-date-filter-modal', + imports: [FormsModule, SkyDateRangePickerModule, SkyModalModule], + template: ` + + + + + + + + + + + `, +}) +export class StartDateFilterModalComponent implements SkyFilterItemModal { + public readonly modalInstance = inject(SkyFilterItemModalInstance); + readonly #context = this.modalInstance.context; + + public filterLabelText = this.#context.filterLabelText; + public dateRange: SkyDateRangeCalculation | undefined; + + constructor() { + const existingValue = this.#context.filterValue?.value as + | SkyDateRangeCalculation + | undefined; + if (existingValue) { + this.dateRange = existingValue; + } + } + + public apply(): void { + if ( + this.dateRange && + this.dateRange.calculatorId !== SkyDateRangeCalculatorId.AnyTime + ) { + const filterValue: SkyFilterBarFilterValue = { + value: this.dateRange, + displayValue: this.#formatDateRange(this.dateRange), + }; + this.modalInstance.save({ filterValue }); + } else { + this.modalInstance.save({ filterValue: undefined }); + } + } + + public clear(): void { + this.modalInstance.save({ filterValue: undefined }); + } + + public cancel(): void { + this.modalInstance.cancel(); + } + + #formatDateRange(range: SkyDateRangeCalculation): string { + if (range.startDate && range.endDate) { + const start = new Date(range.startDate).toLocaleDateString(); + const end = new Date(range.endDate).toLocaleDateString(); + return `${start} - ${end}`; + } else if (range.startDate) { + return `After ${new Date(range.startDate).toLocaleDateString()}`; + } else if (range.endDate) { + return `Before ${new Date(range.endDate).toLocaleDateString()}`; + } + return 'Date range applied'; + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/basic/data.ts b/libs/components/code-examples/src/lib/modules/data-grid/basic/data.ts new file mode 100644 index 0000000000..25ebd17408 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/basic/data.ts @@ -0,0 +1,157 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface DataGridDemoRow { + id: string; + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const DATA_GRID_DEMO_DATA = [ + { + id: '1', + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + id: '2', + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + id: '3', + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + }, + { + id: '4', + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + id: '5', + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + id: '6', + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + id: '7', + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.html b/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.html new file mode 100644 index 0000000000..a7ffb82ffb --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.html @@ -0,0 +1,91 @@ +@let names = selectedNames(); + + + + Selected rows: {{ names }} + @if (!names) { + no rows selected + } + + + + + + + + + + + {{ row.department?.name }} + + + {{ row.jobTitle?.name }} + + + + + + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.spec.ts new file mode 100644 index 0000000000..da013e7833 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.spec.ts @@ -0,0 +1,78 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyDataGridHarness } from '@skyux/data-grid/testing'; +import { + SkyDropdownHarness, + SkyDropdownMenuHarness, +} from '@skyux/popovers/testing'; + +import { DataGridBasicExampleComponent } from './example.component'; + +describe('Basic data grid example', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DataGridBasicExampleComponent], + }).compileComponents(); + fixture = TestBed.createComponent(DataGridBasicExampleComponent); + loader = TestbedHarnessEnvironment.loader(fixture); + fixture.detectChanges(); + }); + + it('should create the component and show data', async () => { + expect(fixture.componentInstance).toBeDefined(); + const gridHarness = await loader.getHarness( + SkyDataGridHarness.with({ + dataSkyId: 'example-data-grid', + }), + ); + expect(await gridHarness.getDisplayedColumnIds()).toEqual([ + 'ag-Grid-SelectionColumn', + 'context', + 'name', + 'age', + 'startDate', + 'endDate', + 'department', + 'jobTitle', + ]); + expect(await gridHarness.getDisplayedColumnHeaderNames()).toEqual([ + '', + 'Context menu', + 'Name', + 'Age', + 'Start date', + 'End date', + 'Department', + 'Title', + ]); + }); + + it('should show context menu and handle item click', async () => { + expect(fixture.componentInstance).toBeDefined(); + const gridHarness = await loader.getHarness( + SkyDataGridHarness.with({ + dataSkyId: 'example-data-grid', + }), + ); + const menuButtonHarness = await gridHarness.queryHarness( + SkyDropdownHarness.with({ + dataSkyId: 'context-menu-2', + }), + ); + await menuButtonHarness.clickDropdownButton(); + const menuHarness = await TestbedHarnessEnvironment.documentRootLoader( + fixture, + ).getHarness(SkyDropdownMenuHarness); + const deleteButton = await menuHarness.querySelector( + 'button[aria-label="More info for Jane Deere"]', + ); + expect(deleteButton).toBeTruthy(); + const actionSpy = spyOn(window, 'alert').and.stub(); + await deleteButton?.click(); + expect(actionSpy).toHaveBeenCalledWith('More info clicked for Jane Deere'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.ts new file mode 100644 index 0000000000..273b06832a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.ts @@ -0,0 +1,65 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + model, + signal, +} from '@angular/core'; +import { + SkyDataGridModule, + SkyDataGridRowDeleteCancelArgs, + SkyDataGridRowDeleteConfirmArgs, +} from '@skyux/data-grid'; +import { SkyBoxModule } from '@skyux/layout'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { DATA_GRID_DEMO_DATA, DataGridDemoRow } from './data'; + +/** + * @title Basic data grid + */ +@Component({ + selector: 'app-data-grid-basic-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyBoxModule, SkyDataGridModule, SkyDropdownModule], +}) +export class DataGridBasicExampleComponent { + protected readonly gridData = signal(DATA_GRID_DEMO_DATA); + protected readonly rowDeleteIds = model([]); + protected readonly selectedRowIds = model([]); + + protected readonly selectedNames = computed(() => { + const selectedRowIds = this.selectedRowIds(); + return this.gridData() + .filter((row) => selectedRowIds.includes(row.id)) + .map((row: DataGridDemoRow) => row.name) + .sort((a, b) => a.localeCompare(b)) + .join(', '); + }); + + public actionClicked(row: DataGridDemoRow, action: string): void { + if (action === 'Delete') { + this.rowDeleteIds.update((rowDeleteIds) => [ + ...new Set([...rowDeleteIds, row.id]), + ]); + } else { + alert(`${action} clicked for ${row.name}`); + } + } + + protected rowDeleteConfirm($event: SkyDataGridRowDeleteConfirmArgs): void { + this.gridData.update((gridData) => + gridData.filter(({ id }) => id !== $event.id), + ); + this.rowDeleteIds.update((rowDeleteIds) => + rowDeleteIds.filter((id) => id !== $event.id), + ); + } + + protected rowDeleteCancel($event: SkyDataGridRowDeleteCancelArgs): void { + this.rowDeleteIds.update((rowDeleteIds) => + rowDeleteIds.filter((id) => id !== $event.id), + ); + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/data-manager/data.ts b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/data.ts new file mode 100644 index 0000000000..69feb368ec --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/data.ts @@ -0,0 +1,159 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface DataGridDemoRow { + id: string; + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: string; + jobTitle?: string; +} + +export const DATA_GRID_DEMO_DATA: DataGridDemoRow[] = [ + { + id: '4b7f07b6-d8d3-41cd-84ad-f3ed51cee5c0', + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3].name, + jobTitle: JOB_TITLES['Customer Support'][1].name, + }, + { + id: 'aea50a38-aa1e-44e0-94b5-52d3f577767f', + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2].name, + jobTitle: JOB_TITLES['Engineering'][2].name, + }, + { + id: 'e74afbe4-5016-4a20-9803-30a301835c4f', + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1].name, + jobTitle: JOB_TITLES['Sales'][1].name, + }, + { + id: '0274faf9-388e-497d-bced-f2bef3eafcfd', + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2].name, + jobTitle: JOB_TITLES['Engineering'][4].name, + }, + { + id: '09b7da69-0272-4fe0-ace3-658a6d8f175c', + selected: true, + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0].name, + jobTitle: JOB_TITLES['Marketing'][2].name, + }, + { + id: '3accf076-fff1-4229-bad3-7d2d42d2c42a', + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2].name, + jobTitle: JOB_TITLES['Engineering'][0].name, + }, + { + id: 'a8456cf4-4f8d-40ee-a91a-ece9c2327fe4', + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2].name, + jobTitle: JOB_TITLES['Engineering'][3].name, + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.component.html b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.component.html new file mode 100644 index 0000000000..5cda4e6d63 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.component.html @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.component.ts new file mode 100644 index 0000000000..6e7e2fc4a5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.component.ts @@ -0,0 +1,129 @@ +import { I18nPluralPipe } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + inject, + model, + signal, +} from '@angular/core'; +import { + SkyDataGridFilterValue, + SkyDataGridModule, + SkyDataGridRowDeleteCancelArgs, + SkyDataGridRowDeleteConfirmArgs, +} from '@skyux/data-grid'; +import { + SkyDataManagerModule, + SkyDataManagerService, + SkyDataManagerState, +} from '@skyux/data-manager'; +import { + SkyFilterBarFilterItem, + SkyFilterBarModule, + SkyFilterItemLookupSearchAsyncArgs, +} from '@skyux/filter-bar'; +import { SkyListSummaryModule } from '@skyux/lists'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { DATA_GRID_DEMO_DATA, DataGridDemoRow } from './data'; +import { ExampleService } from './example.service'; +import { SalesModalComponent } from './sales-modal.component'; + +/** + * @title Data grid with data manager + */ +@Component({ + selector: 'app-data-grid-data-manager-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SkyDataManagerService], + imports: [ + I18nPluralPipe, + SkyDataGridModule, + SkyDataManagerModule, + SkyDropdownModule, + SkyFilterBarModule, + SkyListSummaryModule, + ], +}) +export class DataGridDataManagerExampleComponent { + protected readonly appliedFilters = model< + SkyFilterBarFilterItem[] + >([]); + protected readonly gridData = signal(DATA_GRID_DEMO_DATA); + protected readonly recordCount = model(DATA_GRID_DEMO_DATA.length); + protected readonly rowDeleteIds = model([]); + protected readonly salesModal = SalesModalComponent; + protected readonly viewId = 'dataGridWithDataManagerView' as const; + + readonly #dataManagerSvc = inject(SkyDataManagerService); + readonly #exampleSvc = inject(ExampleService); + + constructor() { + this.#dataManagerSvc.initDataManager({ + activeViewId: this.viewId, + dataManagerConfig: {}, + defaultDataState: new SkyDataManagerState({ + filterData: { + filters: {}, + }, + views: [ + { + viewId: this.viewId, + displayedColumnIds: [ + 'context', + 'name', + 'age', + 'startDate', + 'endDate', + 'department', + 'jobTitle', + ], + }, + ], + }), + }); + this.#dataManagerSvc.initDataView({ + id: this.viewId, + name: 'Data Grid View', + iconName: 'table', + searchEnabled: true, + columnPickerEnabled: true, + }); + } + + protected onJobTitleSearchAsync( + args: SkyFilterItemLookupSearchAsyncArgs, + ): void { + // In a real-world application the search service might return an Observable + // created by calling HttpClient.get(). Assigning that Observable to the result + // allows the lookup component to cancel the web request if it does not complete + // before the user searches again. + args.result = this.#exampleSvc.search(args.searchText); + } + + public actionClicked(row: DataGridDemoRow, action: string): void { + if (action === 'Delete') { + this.rowDeleteIds.update((rowDeleteIds) => [ + ...new Set([...rowDeleteIds, row.id]), + ]); + } else { + alert(`${action} clicked for ${row.name}`); + } + } + + protected rowDeleteConfirm($event: SkyDataGridRowDeleteConfirmArgs): void { + this.gridData.update((gridData) => + gridData.filter(({ id }) => id !== $event.id), + ); + this.rowDeleteIds.update((rowDeleteIds) => + rowDeleteIds.filter((id) => id !== $event.id), + ); + } + + protected rowDeleteCancel($event: SkyDataGridRowDeleteCancelArgs): void { + this.rowDeleteIds.update((rowDeleteIds) => + rowDeleteIds.filter((id) => id !== $event.id), + ); + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.service.ts b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.service.ts new file mode 100644 index 0000000000..f091746772 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { SkyFilterItemLookupSearchAsyncResult } from '@skyux/filter-bar'; + +import { Observable, delay, of } from 'rxjs'; + +import { JOB_TITLES } from './data'; + +@Injectable({ + providedIn: 'root', +}) +export class ExampleService { + #jobs = Object.values(JOB_TITLES).flat(); + + public search( + searchText: string, + ): Observable { + searchText = searchText.toUpperCase(); + + const matchingJobs = this.#jobs + .filter((job) => job.name?.toUpperCase().includes(searchText)) + .map((job) => Object.assign(`${job.name}`, job)); + + // Simulate a network call with latency. A real-world application might + // use Angular's HttpClient to create an Observable from a call to a + // web service. + return of({ + hasMore: false, + items: matchingJobs, + totalCount: matchingJobs.length, + }).pipe(delay(800)); + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/data-manager/sales-modal.component.html b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/sales-modal.component.html new file mode 100644 index 0000000000..8603b6d8f3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/sales-modal.component.html @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/data-grid/data-manager/sales-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/sales-modal.component.ts new file mode 100644 index 0000000000..4b859f66ea --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/sales-modal.component.ts @@ -0,0 +1,57 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + SkyFilterBarFilterValue, + SkyFilterItemModal, + SkyFilterItemModalInstance, +} from '@skyux/filter-bar'; +import { SkyCheckboxModule } from '@skyux/forms'; +import { SkyModalModule } from '@skyux/modals'; + +@Component({ + selector: 'app-sales-modal', + templateUrl: './sales-modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [FormsModule, SkyCheckboxModule, SkyModalModule], +}) +export class SalesModalComponent implements SkyFilterItemModal { + public readonly modalInstance = inject(SkyFilterItemModalInstance); + + protected hideSales = false; + protected modalLabel: string; + + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #context = this.modalInstance.context; + + constructor() { + this.modalLabel = this.#context.filterLabelText; + + if (this.#context.filterValue) { + this.hideSales = !!this.#context.filterValue.value; + } + + this.#changeDetectorRef.markForCheck(); + } + + protected applyFilters(): void { + let result: SkyFilterBarFilterValue | undefined; + + if (this.hideSales) { + result = { + value: 'Sales', + displayValue: 'True', + }; + } + + this.modalInstance.save({ filterValue: result }); + } + + protected cancel(): void { + this.modalInstance.cancel(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/filtered/data.ts b/libs/components/code-examples/src/lib/modules/data-grid/filtered/data.ts new file mode 100644 index 0000000000..5640173be0 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/filtered/data.ts @@ -0,0 +1,91 @@ +export interface Employee { + id: string; + name: string; + department: string; + salary: number; + startDate: string; + active: boolean; +} + +export const employees = [ + { + id: '1', + name: 'Alice Johnson', + department: 'Engineering', + salary: 95000, + startDate: '2020-03-15', + active: true, + }, + { + id: '2', + name: 'Bob Smith', + department: 'Sales', + salary: 72000, + startDate: '2019-07-22', + active: true, + }, + { + id: '3', + name: 'Carol Williams', + department: 'Engineering', + salary: 110000, + startDate: '2018-01-10', + active: true, + }, + { + id: '4', + name: 'David Brown', + department: 'Marketing', + salary: 65000, + startDate: '2021-09-01', + active: false, + }, + { + id: '5', + name: 'Eva Martinez', + department: 'Engineering', + salary: 88000, + startDate: '2020-11-30', + active: true, + }, + { + id: '6', + name: 'Frank Garcia', + department: 'Sales', + salary: 78000, + startDate: '2017-04-18', + active: true, + }, + { + id: '7', + name: 'Grace Lee', + department: 'Marketing', + salary: 71000, + startDate: '2022-02-14', + active: true, + }, + { + id: '8', + name: 'Henry Wilson', + department: 'Engineering', + salary: 125000, + startDate: '2016-08-05', + active: false, + }, + { + id: '9', + name: 'Ivy Chen', + department: 'Sales', + salary: 82000, + startDate: '2019-12-20', + active: true, + }, + { + id: '10', + name: 'Jack Taylor', + department: 'Marketing', + salary: 58000, + startDate: '2023-01-09', + active: true, + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/data-grid/filtered/example.component.html b/libs/components/code-examples/src/lib/modules/data-grid/filtered/example.component.html new file mode 100644 index 0000000000..1e559ec631 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/filtered/example.component.html @@ -0,0 +1,75 @@ + + + + + + + + + + + + {{ + value | skyNumeric: { format: 'currency', iso: 'USD', truncate: false } + }} + + + {{ + value | skyDate: 'mediumDate' + }} + + + diff --git a/libs/components/code-examples/src/lib/modules/data-grid/filtered/example.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/filtered/example.component.ts new file mode 100644 index 0000000000..7b6ff7430d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/filtered/example.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { SkyNumericPipe } from '@skyux/core'; +import { SkyDataGridFilterValue, SkyDataGridModule } from '@skyux/data-grid'; +import { SkyDatePipe } from '@skyux/datetime'; +import { SkyFilterBarModule } from '@skyux/filter-bar'; +import { SkyFilterStateFilterItem } from '@skyux/lists'; + +import { Employee, employees } from './data'; +import { HideInactiveFilterModalComponent } from './hide-inactive-filter-modal.component'; +import { NameFilterModalComponent } from './name-filter-modal.component'; +import { SalaryFilterModalComponent } from './salary-filter-modal.component'; +import { StartDateFilterModalComponent } from './start-date-filter-modal.component'; + +/** + * @title Filtered data grid + */ +@Component({ + selector: 'app-filtered-data-grid', + imports: [SkyDataGridModule, SkyFilterBarModule, SkyNumericPipe, SkyDatePipe], + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './example.component.html', +}) +export class FilteredDataGridComponent { + protected readonly nameFilterModal = NameFilterModalComponent; + protected readonly salaryFilterModal = SalaryFilterModalComponent; + protected readonly hideInactiveModal = HideInactiveFilterModalComponent; + protected readonly startDateFilterModal = StartDateFilterModalComponent; + + protected readonly appliedFilters = signal< + SkyFilterStateFilterItem[] + >([]); + + protected readonly allEmployees: Employee[] = employees; +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/filtered/hide-inactive-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/filtered/hide-inactive-filter-modal.component.ts new file mode 100644 index 0000000000..fc5ac9242e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/filtered/hide-inactive-filter-modal.component.ts @@ -0,0 +1,50 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + SkyFilterBarFilterValue, + SkyFilterItemModal, + SkyFilterItemModalInstance, +} from '@skyux/filter-bar'; +import { SkyCheckboxModule } from '@skyux/forms'; +import { SkyModalModule } from '@skyux/modals'; + +@Component({ + selector: 'app-hide-inactive-filter-modal', + imports: [FormsModule, SkyCheckboxModule, SkyModalModule], + template: ` + + + + + + + + + + `, +}) +export class HideInactiveFilterModalComponent implements SkyFilterItemModal { + public readonly modalInstance = inject(SkyFilterItemModalInstance); + readonly #context = this.modalInstance.context; + + public filterLabelText = this.#context.filterLabelText; + public hideInactive = !!this.#context.filterValue?.value; + + public apply(): void { + const filterValue: SkyFilterBarFilterValue | undefined = this.hideInactive + ? { value: true, displayValue: 'Showing active only' } + : undefined; + this.modalInstance.save({ filterValue }); + } + + public cancel(): void { + this.modalInstance.cancel(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/filtered/name-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/filtered/name-filter-modal.component.ts new file mode 100644 index 0000000000..57dcb25918 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/filtered/name-filter-modal.component.ts @@ -0,0 +1,52 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + SkyFilterBarFilterValue, + SkyFilterItemModal, + SkyFilterItemModalInstance, +} from '@skyux/filter-bar'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyModalModule } from '@skyux/modals'; + +@Component({ + selector: 'app-name-filter-modal', + imports: [FormsModule, SkyInputBoxModule, SkyModalModule], + template: ` + + + + + + + + + + + + `, +}) +export class NameFilterModalComponent implements SkyFilterItemModal { + public readonly modalInstance = inject(SkyFilterItemModalInstance); + readonly #context = this.modalInstance.context; + + public filterLabelText = this.#context.filterLabelText; + public nameFilter = (this.#context.filterValue?.value as string) ?? ''; + + public apply(): void { + const filterValue: SkyFilterBarFilterValue | undefined = this.nameFilter + ? { + value: this.nameFilter, + displayValue: `Name contains "${this.nameFilter}"`, + } + : undefined; + this.modalInstance.save({ filterValue }); + } + + public cancel(): void { + this.modalInstance.cancel(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/filtered/salary-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/filtered/salary-filter-modal.component.ts new file mode 100644 index 0000000000..3675432045 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/filtered/salary-filter-modal.component.ts @@ -0,0 +1,146 @@ +import { Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { + SkyAutonumericModule, + SkyAutonumericOptions, +} from '@skyux/autonumeric'; +import { SkyNumericService } from '@skyux/core'; +import { + SkyDataGridNumberRangeFilterFormGroup, + SkyDataGridNumberRangeFilterValue, +} from '@skyux/data-grid'; +import { + SkyFilterItemModal, + SkyFilterItemModalInstance, +} from '@skyux/filter-bar'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyModalError, SkyModalModule } from '@skyux/modals'; + +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'app-salary-filter-modal', + imports: [ + ReactiveFormsModule, + SkyInputBoxModule, + SkyModalModule, + SkyAutonumericModule, + ], + template: ` +
+ + +
+ + + +
+
+ + + +
+
+ + + + + +
+
+ `, +}) +export class SalaryFilterModalComponent implements SkyFilterItemModal { + public readonly modalInstance = inject(SkyFilterItemModalInstance); + readonly #context = this.modalInstance.context; + readonly #fb = inject(FormBuilder); + readonly #existingValue = this.#context.filterValue?.value as + | SkyDataGridNumberRangeFilterValue + | undefined; + readonly #numeric = inject(SkyNumericService); + + public filterLabelText = this.#context.filterLabelText; + + protected readonly form = this.#fb.group( + { + from: this.#fb.control(this.#existingValue?.from ?? null), + to: this.#fb.control(this.#existingValue?.to ?? null), + }, + { + validators: (formGroup: SkyDataGridNumberRangeFilterFormGroup) => { + const min = formGroup.controls.from.value; + const max = formGroup.controls.to.value; + // At least one value must be provided. + if (min === null && max === null) { + return { + salaryRangeRequired: { message: 'Salary Range required' }, + }; + } + // If both values are provided, min must be less than or equal to max. + if (min !== null && max !== null && min >= max) { + return { salaryRangeInvalid: { message: 'Salary Range Invalid' } }; + } + return null; + }, + updateOn: 'change', + }, + ) as SkyDataGridNumberRangeFilterFormGroup; + + protected readonly formErrors = toSignal( + this.form.statusChanges.pipe( + map(() => Object.values(this.form.errors ?? {}) as SkyModalError[]), + ), + ); + protected autonumericOptions: SkyAutonumericOptions = 'dollarPos' as const; + + public apply(): void { + if (this.form.valid) { + const value = { + from: this.form.controls.from.value, + to: this.form.controls.to.value, + } as SkyDataGridNumberRangeFilterValue; + let displayValue = ''; + if (value.from && value.to) { + displayValue = `${this.#format(value.from)} - ${this.#format(value.to)}`; + } else if (value.from) { + displayValue = `From ${this.#format(value.from)}`; + } else if (value.to) { + displayValue = `Up to ${this.#format(value.to)}`; + } + this.modalInstance.save({ filterValue: { value, displayValue } }); + } else { + this.modalInstance.save({ filterValue: undefined }); + } + } + + public clear(): void { + this.modalInstance.save({ filterValue: undefined }); + } + + public cancel(): void { + this.modalInstance.cancel(); + } + + #format(value: number): string { + return this.#numeric.formatNumber(value, { format: 'currency' }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/filtered/start-date-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/filtered/start-date-filter-modal.component.ts new file mode 100644 index 0000000000..156fdcbf52 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/filtered/start-date-filter-modal.component.ts @@ -0,0 +1,91 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + SkyDateRangeCalculation, + SkyDateRangeCalculatorId, + SkyDateRangePickerModule, +} from '@skyux/datetime'; +import { + SkyFilterBarFilterValue, + SkyFilterItemModal, + SkyFilterItemModalInstance, +} from '@skyux/filter-bar'; +import { SkyModalModule } from '@skyux/modals'; + +@Component({ + selector: 'app-start-date-filter-modal', + imports: [FormsModule, SkyDateRangePickerModule, SkyModalModule], + template: ` + + + + + + + + + + + `, +}) +export class StartDateFilterModalComponent implements SkyFilterItemModal { + public readonly modalInstance = inject(SkyFilterItemModalInstance); + readonly #context = this.modalInstance.context; + + public filterLabelText = this.#context.filterLabelText; + public dateRange: SkyDateRangeCalculation | undefined; + + constructor() { + const existingValue = this.#context.filterValue?.value as + | SkyDateRangeCalculation + | undefined; + if (existingValue) { + this.dateRange = existingValue; + } + } + + public apply(): void { + if ( + this.dateRange && + this.dateRange.calculatorId !== SkyDateRangeCalculatorId.AnyTime + ) { + const filterValue: SkyFilterBarFilterValue = { + value: this.dateRange, + displayValue: this.#formatDateRange(this.dateRange), + }; + this.modalInstance.save({ filterValue }); + } else { + this.modalInstance.save({ filterValue: undefined }); + } + } + + public clear(): void { + this.modalInstance.save({ filterValue: undefined }); + } + + public cancel(): void { + this.modalInstance.cancel(); + } + + #formatDateRange(range: SkyDateRangeCalculation): string { + if (range.startDate && range.endDate) { + const start = new Date(range.startDate).toLocaleDateString(); + const end = new Date(range.endDate).toLocaleDateString(); + return `${start} - ${end}`; + } else if (range.startDate) { + return `After ${new Date(range.startDate).toLocaleDateString()}`; + } else if (range.endDate) { + return `Before ${new Date(range.endDate).toLocaleDateString()}`; + } + return 'Date range applied'; + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-grid/paging/data.ts b/libs/components/code-examples/src/lib/modules/data-grid/paging/data.ts new file mode 100644 index 0000000000..62f4f7152e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/paging/data.ts @@ -0,0 +1,146 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export const DATA_GRID_DEMO_DATA = [ + { + id: '1', + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + id: '2', + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + id: '3', + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + }, + { + id: '4', + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + id: '5', + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + id: '6', + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + id: '7', + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/data-grid/paging/example.component.html b/libs/components/code-examples/src/lib/modules/data-grid/paging/example.component.html new file mode 100644 index 0000000000..10f69c7d8c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/paging/example.component.html @@ -0,0 +1,30 @@ + + + + + + + {{ value?.name ?? '' }} + + + {{ value?.name ?? '' }} + + diff --git a/libs/components/code-examples/src/lib/modules/data-grid/paging/example.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/paging/example.component.ts new file mode 100644 index 0000000000..414309a23c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-grid/paging/example.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { SkyDataGridModule } from '@skyux/data-grid'; + +import { DATA_GRID_DEMO_DATA } from './data'; + +/** + * @title Data grid with paging using router query parameters + */ +@Component({ + selector: 'app-data-grid-paging', + imports: [SkyDataGridModule], + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './example.component.html', +}) +export class DataGridPagingComponent { + protected readonly data = DATA_GRID_DEMO_DATA; + + // For demo purposes, only use the query string if we're running the demo in its own SPA as a route and not on the documentation site. + protected readonly pageQueryParam = + inject(ActivatedRoute).component === DataGridPagingComponent ? 'page' : ''; +} diff --git a/libs/components/data-grid/README.md b/libs/components/data-grid/README.md new file mode 100644 index 0000000000..8a47d7d2eb --- /dev/null +++ b/libs/components/data-grid/README.md @@ -0,0 +1,3 @@ +# data-grid + +This library was generated with [Nx](https://nx.dev). diff --git a/libs/components/data-grid/documentation.json b/libs/components/data-grid/documentation.json new file mode 100644 index 0000000000..a12edb2958 --- /dev/null +++ b/libs/components/data-grid/documentation.json @@ -0,0 +1,34 @@ +{ + "$schema": "../manifest-generator/documentation-schema.json", + "groups": { + "data-grid-package": { + "development": { + "docsIds": [ + "SkyDataGridModule", + "SkyDataGridComponent", + "SkyDataGridColumnComponent", + "SkyDataGridFilterOperator", + "SkyDataGridFilterValue", + "SkyDataGridNumberRangeFilterValue", + "SkyDataGridPageRequest", + "SkyDataGridRowDeleteCancelArgs", + "SkyDataGridRowDeleteConfirmArgs", + "SkyDataGridSort" + ], + "primaryDocsId": "SkyDataGridModule" + }, + "testing": { + "docsIds": ["SkyDataGridHarness", "SkyDataGridHarnessFilters"] + }, + "codeExamples": { + "docsIds": [ + "DataGridBasicExampleComponent", + "FilteredDataGridComponent", + "DataGridPagingComponent", + "DataGridDataManagerExampleComponent", + "DataGridAsynchronousDataComponent" + ] + } + } + } +} diff --git a/libs/components/data-grid/eslint.config.js b/libs/components/data-grid/eslint.config.js new file mode 100644 index 0000000000..9aa0870e02 --- /dev/null +++ b/libs/components/data-grid/eslint.config.js @@ -0,0 +1,3 @@ +const config = require('../../../eslint-libs.config'); + +module.exports = config; diff --git a/libs/components/data-grid/karma.conf.js b/libs/components/data-grid/karma.conf.js new file mode 100644 index 0000000000..b4b6c9cbde --- /dev/null +++ b/libs/components/data-grid/karma.conf.js @@ -0,0 +1,16 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +const { join } = require('path'); +const getBaseKarmaConfig = require('../../../karma.conf'); + +module.exports = function (config) { + const baseConfig = getBaseKarmaConfig(); + config.set({ + ...baseConfig, + coverageReporter: { + ...baseConfig.coverageReporter, + dir: join(__dirname, '../../../coverage/libs/components/data-grid'), + }, + }); +}; diff --git a/libs/components/data-grid/ng-package.json b/libs/components/data-grid/ng-package.json new file mode 100644 index 0000000000..5960c389b8 --- /dev/null +++ b/libs/components/data-grid/ng-package.json @@ -0,0 +1,10 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/components/data-grid", + "lib": { + "entryFile": "src/index.ts", + "styleIncludePaths": ["../../.."] + }, + "inlineStyleLanguage": "scss", + "allowedNonPeerDependencies": ["@skyux/icon"] +} diff --git a/libs/components/data-grid/package.json b/libs/components/data-grid/package.json new file mode 100644 index 0000000000..0e45f3e227 --- /dev/null +++ b/libs/components/data-grid/package.json @@ -0,0 +1,35 @@ +{ + "name": "@skyux/data-grid", + "version": "0.0.0-PLACEHOLDER", + "author": "Blackbaud, Inc.", + "keywords": [ + "blackbaud", + "skyux" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/blackbaud/skyux.git" + }, + "bugs": { + "url": "https://github.com/blackbaud/skyux/issues" + }, + "homepage": "https://github.com/blackbaud/skyux#readme", + "peerDependencies": { + "@angular/cdk": "^20.2.14", + "@angular/common": "^20.3.15", + "@angular/core": "^20.3.15", + "@angular/forms": "^20.3.15", + "@angular/router": "^20.3.15", + "@skyux/ag-grid": "^0.0.0-PLACEHOLDER", + "@skyux/core": "^0.0.0-PLACEHOLDER", + "@skyux/data-manager": "^0.0.0-PLACEHOLDER", + "@skyux/datetime": "^0.0.0-PLACEHOLDER", + "@skyux/help-inline": "^0.0.0-PLACEHOLDER", + "@skyux/indicators": "^0.0.0-PLACEHOLDER", + "@skyux/lists": "^0.0.0-PLACEHOLDER", + "ag-grid-angular": "^34.3.1", + "ag-grid-community": "^34.3.1" + }, + "sideEffects": false +} diff --git a/libs/components/data-grid/project.json b/libs/components/data-grid/project.json new file mode 100644 index 0000000000..a151de2498 --- /dev/null +++ b/libs/components/data-grid/project.json @@ -0,0 +1,81 @@ +{ + "name": "data-grid", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "libs/components/data-grid/src", + "prefix": "sky", + "tags": ["component", "npm"], + "targets": { + "build": { + "executor": "@nx/angular:package", + "outputs": ["{workspaceRoot}/dist/{projectRoot}"], + "options": { + "project": "libs/components/data-grid/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "libs/components/data-grid/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "libs/components/data-grid/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production", + "dependsOn": [ + "^build", + { + "projects": ["ag-grid", "core"], + "target": "build" + } + ], + "inputs": [ + "buildInputs", + "^buildInputs", + "{workspaceRoot}/libs/components/data-grid/testing/src/**/*", + "!{workspaceRoot}/libs/components/data-grid/testing/src/**/*.spec.ts", + "!{workspaceRoot}/libs/components/data-grid/testing/src/**/fixtures/**/*" + ] + }, + "test": { + "executor": "@angular-devkit/build-angular:karma", + "options": { + "tsConfig": "libs/components/data-grid/tsconfig.spec.json", + "karmaConfig": "libs/components/data-grid/karma.conf.js", + "styles": [ + "libs/components/theme/src/lib/styles/sky.scss", + "libs/components/theme/src/lib/styles/themes/modern/styles.scss", + "libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss" + ], + "codeCoverage": true, + "codeCoverageExclude": ["**/fixtures/**"], + "polyfills": [ + "zone.js", + "zone.js/testing", + "libs/components/packages/src/polyfills.js" + ], + "inlineStyleLanguage": "scss", + "stylePreprocessorOptions": { + "includePaths": ["{workspaceRoot}"] + } + }, + "configurations": { + "ci": { + "browsers": "ChromeHeadlessNoSandbox", + "codeCoverage": true, + "progress": false, + "sourceMap": true, + "watch": false + } + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "options": { + "lintFilePatterns": [ + "{projectRoot}/src/**/*.ts", + "{projectRoot}/src/**/*.html" + ] + } + } + } +} diff --git a/libs/components/data-grid/src/index.ts b/libs/components/data-grid/src/index.ts new file mode 100644 index 0000000000..10048390c4 --- /dev/null +++ b/libs/components/data-grid/src/index.ts @@ -0,0 +1,13 @@ +export { SkyDataGridComponent } from './lib/modules/data-grid/data-grid.component'; +export { SkyDataGridColumnComponent } from './lib/modules/data-grid/data-grid-column.component'; +export { SkyDataGridModule } from './lib/modules/data-grid/data-grid.module'; +export { SkyDataGridFilterOperator } from './lib/modules/types/data-grid-filter-operator'; +export { SkyDataGridFilterValue } from './lib/modules/types/data-grid-filter-value'; +export { + SkyDataGridNumberRangeFilterValue, + SkyDataGridNumberRangeFilterFormGroup, +} from './lib/modules/types/data-grid-number-range-filter-value'; +export { SkyDataGridSort } from './lib/modules/types/data-grid-sort'; +export { SkyDataGridPageRequest } from './lib/modules/types/data-grid-page-request'; +export { SkyDataGridRowDeleteCancelArgs } from './lib/modules/types/data-grid-row-delete-cancel-args'; +export { SkyDataGridRowDeleteConfirmArgs } from './lib/modules/types/data-grid-row-delete-confirm-args'; diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid-column-inline-help.component.ts b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-column-inline-help.component.ts new file mode 100644 index 0000000000..a327fc0095 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-column-inline-help.component.ts @@ -0,0 +1,36 @@ +import { Component, computed, inject } from '@angular/core'; +import { SkyAgGridHeaderInfo } from '@skyux/ag-grid'; +import { SkyHelpInlineModule } from '@skyux/help-inline'; + +/** + * @internal + */ +@Component({ + selector: 'sky-data-grid-column-inline-help', + imports: [SkyHelpInlineModule], + template: `@if (showHelpInline()) { + + }`, +}) +export class SkyDataGridColumnInlineHelpComponent { + protected readonly info = inject(SkyAgGridHeaderInfo); + + protected readonly showHelpInline = computed( + () => !!this.helpPopoverContent(), + ); + + protected readonly helpPopoverTitle = computed( + () => this.#headerComponentParams().helpPopoverTitle, + ); + + protected readonly helpPopoverContent = computed( + () => this.#headerComponentParams().helpPopoverContent, + ); + + readonly #headerComponentParams = computed( + () => this.info.column?.getColDef().headerComponentParams, + ); +} diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid-column.component.ts b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-column.component.ts new file mode 100644 index 0000000000..b93087cf75 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-column.component.ts @@ -0,0 +1,189 @@ +import { + Component, + TemplateRef, + booleanAttribute, + computed, + contentChild, + effect, + inject, + input, + numberAttribute, +} from '@angular/core'; +import { SkyLogService } from '@skyux/core'; + +import { SkyDataGridFilterOperator } from '../types/data-grid-filter-operator'; + +/** + * Specifies the column information. + * @preview + */ +@Component({ + selector: 'sky-data-grid-column', + template: '', +}) +export class SkyDataGridColumnComponent { + /** + * The unique ID for the column. You must provide either the `columnId` or `field` property + * for every column, but do not provide both. Use `columnId` when the column does not map directly to a field + * in the data set. + */ + public readonly columnId = input(); + + /** + * The data type of the column used for filtering, sorting, and rendering when a template is not provided. + * @default 'text' + */ + public readonly dataType = input<'text' | 'number' | 'date' | 'boolean'>( + 'text', + ); + + /** + * The description for the column. + */ + public readonly description = input(); + + /** + * The property to retrieve cell information from an entry on the grid `data` array. + * You must provide either the `columnId` or `field` property for every column, + * but do not provide both. If `columnId` does not exist on a column, then `field` is the entry + * for the grid `selectedColumnIds` array. + */ + public readonly field = input(); + + /** + * When set, `flexWidth` overrides `width` and works like + * [CSS flex-grow](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/flex-grow), where a column + * with `flexWidth="2"` is twice the width of a column with `flexWidth="1"`, and `flexWidth="0"` does not auto-expand. + */ + public readonly flexWidth = input(-1, { + transform: (value) => numberAttribute(value, -1), + }); + + /** + * Text to display in the column header. + */ + public readonly headingText = input.required(); + + /** + * Whether to prevent `heading` text from being visibly displayed. + * @default false + */ + public readonly headingHidden = input(false, { + transform: booleanAttribute, + }); + + /** + * The title of the help popover. This property only applies when `helpPopoverContent` is + * also specified. + */ + public readonly helpPopoverTitle = input(); + + /** + * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * button is added to the column header. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) + * when clicked using the specified content and optional title. + */ + public readonly helpPopoverContent = input>(); + + /** + * Whether the column is initially hidden when grid `selectedColumnIds` are not provided. + * @default false + */ + public readonly hidden = input(false, { + transform: booleanAttribute, + }); + + /** + * Whether the column can be resized by dragging the column header border. + * @default true + */ + public readonly resizable = input(true); + + /** + * Whether the column sorts the grid when users click the column header. + * @default true + */ + public readonly sortable = input(true); + + /** + * Whether the column is locked. The intent is to display locked columns first + * on the left side of the grid. If set to `true`, then users cannot drag the column + * to another position and or drag other columns before the locked column. + * @default false + */ + public readonly locked = input(false, { + transform: booleanAttribute, + }); + + /** + * The template for a column. This can be assigned as a reference + * to the `template` attribute, or it can be assigned as a child of the `template` element + * inside the `sky-grid-column` component. The template has access to the `value` variable, + * which contains the value passed to the column, and the `row` variable, which contains + * the entire row data. + */ + public readonly template = input>(); + + /** + * The width of the column in pixels. When no width is set, the column width is evenly distributed. + */ + public readonly width = input(0, { + transform: (value) => numberAttribute(value, 0), + }); + + /** + * Whether text in this column should wrap to multiple lines. For best performance, large grids should set a `height` + * and not enable `wrapText` on any column so that rows can be virtually drawn as needed. Not setting a `height` or + * enabling `wrapText` forces the grid to draw every row in order to determine the scroll height. + * @default false + */ + public readonly wrapText = input(false, { + transform: booleanAttribute, + }); + + /** + * The filter ID that maps this column to a filter bar item. + * When set, changes to the corresponding filter will apply to this column. + * Defaults to the column's `field` value. + */ + public readonly filterId = input(); + + /** + * The filter operator to use when applying the filter. + * + * Defaults based on column type: + * + * - text: 'contains' + * - number: 'equals' + * - date: 'equals' + */ + public readonly filterOperator = input< + SkyDataGridFilterOperator | undefined + >(); + + protected readonly templateChild = contentChild(TemplateRef); + + constructor() { + const logger = inject(SkyLogService); + effect(() => { + const columnId = this.columnId(); + const field = this.field(); + if (columnId && field) { + logger.warn( + `A should have either a columnId or a field, but not both.`, + ); + } + }); + } + + /** + * @internal + */ + public readonly cellTemplate = computed | undefined>( + () => { + const template = this.template(); + const templateChild = this.templateChild(); + return template || templateChild; + }, + ); +} diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid-filter.spec.ts b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-filter.spec.ts new file mode 100644 index 0000000000..65c9203784 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-filter.spec.ts @@ -0,0 +1,787 @@ +import { TestBed } from '@angular/core/testing'; +import { SkyLogService } from '@skyux/core'; +import { SkyDateRange } from '@skyux/datetime'; +import { SkyFilterStateFilterItem } from '@skyux/lists'; + +import { SkyDataGridFilterOperator } from '../types/data-grid-filter-operator'; +import { SkyDataGridFilterValue } from '../types/data-grid-filter-value'; +import { SkyDataGridNumberRangeFilterValue } from '../types/data-grid-number-range-filter-value'; + +import { doesFilterPass } from './data-grid-filter'; + +interface TestDataRow { + id: string; + test?: string; + text?: string; + num?: number; + date?: Date | string; + bool?: boolean; +} + +interface TestColumnFilter { + filterId: string | undefined; + field: keyof TestDataRow | undefined; + filterOperator: SkyDataGridFilterOperator | undefined; + type: 'text' | 'number' | 'date' | 'boolean'; +} + +/** + * Helper function to call doesFilterPass with consistent typing. + */ +function testFilterPass( + filters: SkyFilterStateFilterItem[], + data: TestDataRow | undefined, + columns: TestColumnFilter[], + logger: SkyLogService, +): boolean { + return doesFilterPass( + filters, + data as TestDataRow, + columns, + logger, + ); +} + +describe('data-grid-filter', () => { + let logService: SkyLogService; + let logSpy: jasmine.Spy; + + beforeEach(() => { + TestBed.configureTestingModule({}); + logService = TestBed.inject(SkyLogService); + logSpy = spyOn(logService, 'warn'); + }); + + it('should apply filters for data grid', () => { + expect( + testFilterPass( + [{ filterId: 'test', filterValue: { value: 'example' } }], + { id: '1', test: 'example' }, + [ + { + filterId: undefined, + field: 'test', + filterOperator: 'contains', + type: 'text', + }, + ], + logService, + ), + ).toBeTrue(); + expect(logSpy).not.toHaveBeenCalled(); + }); + + it('should pass if column is not found', () => { + expect( + testFilterPass( + [{ filterId: 'missing', filterValue: { value: 'example' } }], + { id: '1', test: 'example' }, + [ + { + filterId: 'test', + field: 'test', + filterOperator: 'contains', + type: 'text', + }, + ], + logService, + ), + ).toBeTrue(); + }); + + it('should pass if filter value is undefined', () => { + expect( + testFilterPass( + [ + { + filterId: 'test', + filterValue: { value: undefined as unknown as string }, + }, + ], + { id: '1', test: 'example' }, + [ + { + filterId: 'test', + field: 'test', + filterOperator: 'contains', + type: 'text', + }, + ], + logService, + ), + ).toBeTrue(); + }); + + it('should not pass if data is undefined', () => { + expect( + testFilterPass( + [{ filterId: 'test', filterValue: { value: 'example' } }], + undefined, + [ + { + filterId: 'test', + field: 'test', + filterOperator: 'contains', + type: 'text', + }, + ], + logService, + ), + ).toBeFalse(); + }); + + it('should not pass if data field is undefined', () => { + expect( + testFilterPass( + [{ filterId: 'test', filterValue: { value: 'example' } }], + { id: '1', test: undefined }, + [ + { + filterId: 'test', + field: 'test', + filterOperator: 'contains', + type: 'text', + }, + ], + logService, + ), + ).toBeFalse(); + }); + + describe('text filters', () => { + function createTextColumn( + operator: SkyDataGridFilterOperator | undefined, + ): TestColumnFilter[] { + return [ + { + filterId: 'text', + field: 'text', + filterOperator: operator, + type: 'text', + }, + ]; + } + + it('should filter with "equals"', () => { + const cols = createTextColumn('equals'); + // Match + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'abc' } }], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeTrue(); + // No match + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'abc' } }], + { id: '1', text: 'def' }, + cols, + logService, + ), + ).toBeFalse(); + // Case insensitive + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'ABC' } }], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeTrue(); + }); + + it('should filter with "notEqual"', () => { + const cols = createTextColumn('notEqual'); + // Match (fail) + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'abc' } }], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeFalse(); + // No match (pass) + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'abc' } }], + { id: '1', text: 'def' }, + cols, + logService, + ), + ).toBeTrue(); + }); + + it('should filter with "contains"', () => { + const cols = createTextColumn('contains'); + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'bc' } }], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'de' } }], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeFalse(); + }); + + it('should filter with "notContains"', () => { + const cols = createTextColumn('notContains'); + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'bc' } }], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeFalse(); + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'de' } }], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeTrue(); + }); + + it('should filter with "startsWith"', () => { + const cols = createTextColumn('startsWith'); + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'ab' } }], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'bc' } }], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeFalse(); + }); + + it('should filter with "endsWith"', () => { + const cols = createTextColumn('endsWith'); + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'bc' } }], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'ab' } }], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeFalse(); + }); + + it('should handle undefined/null values gracefully', () => { + const cols = createTextColumn('contains'); + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'null' } }], + { id: '1', text: 'null' }, + cols, + logService, + ), + ).toBeTrue(); // 'null' string matches null + }); + + it('should warn and pass for unsupported operator', () => { + const cols = createTextColumn( + 'unknown' as unknown as SkyDataGridFilterOperator, + ); + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: 'abc' } }], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeTrue(); + expect(logSpy).toHaveBeenCalledWith( + 'Unsupported text filter operator: unknown', + ); + }); + + it('should filter with array values (OR logic)', () => { + const cols = createTextColumn('equals'); + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: ['abc', 'xyz'] } }], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeTrue(); + + expect( + testFilterPass( + [{ filterId: 'text', filterValue: { value: ['xyz', 'uvw'] } }], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeFalse(); + }); + + it('should support "val" property for filter value', () => { + const cols = createTextColumn('equals'); + expect( + testFilterPass( + [ + { filterId: 'text', filterValue: { val: 'abc' } }, + ] as unknown as SkyFilterStateFilterItem[], + { id: '1', text: 'abc' }, + cols, + logService, + ), + ).toBeTrue(); + }); + }); + + describe('number filters', () => { + function createNumColumn( + operator: SkyDataGridFilterOperator | undefined, + ): TestColumnFilter[] { + return [ + { + filterId: 'num', + field: 'num', + filterOperator: operator, + type: 'number', + }, + ]; + } + + it('should filter single values with basic operators', () => { + // equals + let cols = createNumColumn('equals'); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: { value: 10 } }], + { id: '1', num: 10 }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: { value: 10 } }], + { id: '1', num: 11 }, + cols, + logService, + ), + ).toBeFalse(); + + // notEqual + cols = createNumColumn('notEqual'); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: { value: 10 } }], + { id: '1', num: 11 }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: { value: 10 } }], + { id: '1', num: 10 }, + cols, + logService, + ), + ).toBeFalse(); + + // lessThan + cols = createNumColumn('lessThan'); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: { value: 10 } }], + { id: '1', num: 9 }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: { value: 10 } }], + { id: '1', num: 10 }, + cols, + logService, + ), + ).toBeFalse(); + + // lessThanOrEqual + cols = createNumColumn('lessThanOrEqual'); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: { value: 10 } }], + { id: '1', num: 10 }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: { value: 10 } }], + { id: '1', num: 11 }, + cols, + logService, + ), + ).toBeFalse(); + + // greaterThan + cols = createNumColumn('greaterThan'); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: { value: 10 } }], + { id: '1', num: 11 }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: { value: 10 } }], + { id: '1', num: 10 }, + cols, + logService, + ), + ).toBeFalse(); + + // greaterThanOrEqual + cols = createNumColumn('greaterThanOrEqual'); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: { value: 10 } }], + { id: '1', num: 10 }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: { value: 10 } }], + { id: '1', num: 9 }, + cols, + logService, + ), + ).toBeFalse(); + }); + + it('should filter number ranges', () => { + const cols = createNumColumn(undefined); + const rangeFilter = ( + from: number | null, + to: number | null, + ): { value: SkyDataGridNumberRangeFilterValue } => ({ + value: { from, to } as SkyDataGridNumberRangeFilterValue, + }); + // In range + expect( + testFilterPass( + [{ filterId: 'num', filterValue: rangeFilter(5, 15) }], + { id: '1', num: 10 }, + cols, + logService, + ), + ).toBeTrue(); + // Out range (low) + expect( + testFilterPass( + [{ filterId: 'num', filterValue: rangeFilter(5, 15) }], + { id: '1', num: 4 }, + cols, + logService, + ), + ).toBeFalse(); + // Out range (high) + expect( + testFilterPass( + [{ filterId: 'num', filterValue: rangeFilter(5, 15) }], + { id: '1', num: 16 }, + cols, + logService, + ), + ).toBeFalse(); + // Open ended (from) + expect( + testFilterPass( + [{ filterId: 'num', filterValue: rangeFilter(5, null) }], + { id: '1', num: 6 }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: rangeFilter(5, null) }], + { id: '1', num: 4 }, + cols, + logService, + ), + ).toBeFalse(); + // Open ended (to) + expect( + testFilterPass( + [{ filterId: 'num', filterValue: rangeFilter(null, 15) }], + { id: '1', num: 14 }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: rangeFilter(null, 15) }], + { id: '1', num: 16 }, + cols, + logService, + ), + ).toBeFalse(); + }); + + it('should warn and pass for unsupported operator', () => { + const cols = createNumColumn( + 'unknown' as unknown as SkyDataGridFilterOperator, + ); + expect( + testFilterPass( + [{ filterId: 'num', filterValue: { value: 10 } }], + { id: '1', num: 10 }, + cols, + logService, + ), + ).toBeTrue(); // defaults to false in numericFilter but !false is true here? + // Wait, numericFilter default returns false. !false is true. Correct. + expect(logSpy).toHaveBeenCalledWith( + 'Unsupported number or date filter operator: unknown', + ); + }); + }); + + describe('date filters', () => { + function createDateColumn( + operator: SkyDataGridFilterOperator | undefined, + ): TestColumnFilter[] { + return [ + { + filterId: 'date', + field: 'date', + filterOperator: operator, + type: 'date', + }, + ]; + } + + function dateRangeFilter( + startDate: Date | undefined, + endDate?: Date, + ): { value: SkyDateRange } { + return { value: { startDate, endDate } as SkyDateRange }; + } + + it('should filter single dates', () => { + const date = new Date(2022, 1, 1); + const sameDate = new Date(2022, 1, 1, 12, 0); // same day, different time + const nextDate = new Date(2022, 1, 2); + + // equals + const cols = createDateColumn('equals'); + expect( + testFilterPass( + [{ filterId: 'date', filterValue: { value: date } }], + { id: '1', date: sameDate }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'date', filterValue: { value: date } }], + { id: '1', date: nextDate }, + cols, + logService, + ), + ).toBeFalse(); + + // date string handling + expect( + testFilterPass( + [{ filterId: 'date', filterValue: { value: '2022-02-01' } }], // Note: Timezone sensitive if not careful, but zeroHour uses local parts + { id: '1', date: date }, // Date in local time + cols, + logService, + ), + ).toBeTrue(); // assuming '2022-02-01' parses to same local day or constructed similarly + }); + + it('should filter date ranges', () => { + const cols = createDateColumn(undefined); + const start = new Date(2022, 1, 1); + const end = new Date(2022, 1, 10); + const mid = new Date(2022, 1, 5); + const midDay = new Date(2022, 1, 5, 12, 0, 0); + const endDay = new Date(2022, 1, 10, 23, 59, 59); + + const before = new Date(2022, 0, 1); + const after = new Date(2022, 2, 1); + + // In range + expect( + testFilterPass( + [{ filterId: 'date', filterValue: dateRangeFilter(start, end) }], + { id: '1', date: mid }, + cols, + logService, + ), + ).toBeTrue(); + + // Mid day should pass + expect( + testFilterPass( + [{ filterId: 'date', filterValue: dateRangeFilter(start, end) }], + { id: '1', date: midDay }, + cols, + logService, + ), + ).toBeTrue(); + + // End day (end of day) should pass + expect( + testFilterPass( + [{ filterId: 'date', filterValue: dateRangeFilter(start, end) }], + { id: '1', date: endDay }, + cols, + logService, + ), + ).toBeTrue(); + + // Out range + expect( + testFilterPass( + [{ filterId: 'date', filterValue: dateRangeFilter(start, end) }], + { id: '1', date: before }, + cols, + logService, + ), + ).toBeFalse(); + expect( + testFilterPass( + [{ filterId: 'date', filterValue: dateRangeFilter(start, end) }], + { id: '1', date: after }, + cols, + logService, + ), + ).toBeFalse(); + + // Open ended + expect( + testFilterPass( + [{ filterId: 'date', filterValue: dateRangeFilter(start) }], + { id: '1', date: mid }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'date', filterValue: dateRangeFilter(start) }], + { id: '1', date: before }, + cols, + logService, + ), + ).toBeFalse(); + }); + }); + + describe('boolean filters', () => { + function createBoolColumn( + operator: SkyDataGridFilterOperator | undefined, + ): TestColumnFilter[] { + return [ + { + filterId: 'bool', + field: 'bool', + filterOperator: operator, + type: 'boolean', + }, + ]; + } + + it('should filter boolean values', () => { + // equals + let cols = createBoolColumn('equals'); + expect( + testFilterPass( + [{ filterId: 'bool', filterValue: { value: true } }], + { id: '1', bool: true }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'bool', filterValue: { value: true } }], + { id: '1', bool: false }, + cols, + logService, + ), + ).toBeFalse(); + + // notEqual + cols = createBoolColumn('notEqual'); + expect( + testFilterPass( + [{ filterId: 'bool', filterValue: { value: true } }], + { id: '1', bool: false }, + cols, + logService, + ), + ).toBeTrue(); + expect( + testFilterPass( + [{ filterId: 'bool', filterValue: { value: true } }], + { id: '1', bool: true }, + cols, + logService, + ), + ).toBeFalse(); + }); + + it('should warn for unsupported boolean operator', () => { + const cols = createBoolColumn('contains'); + expect( + testFilterPass( + [{ filterId: 'bool', filterValue: { value: true } }], + { id: '1', bool: true }, + cols, + logService, + ), + ).toBeTrue(); + expect(logSpy).toHaveBeenCalledWith( + 'Unsupported boolean filter operator: contains', + ); + }); + }); +}); diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid-filter.ts b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-filter.ts new file mode 100644 index 0000000000..06a647fe96 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-filter.ts @@ -0,0 +1,287 @@ +import { SkyLogService } from '@skyux/core'; +import { SkyDateRange } from '@skyux/datetime'; +import { SkyFilterStateFilterItem } from '@skyux/lists'; + +import { SkyDataGridFilterOperator } from '../types/data-grid-filter-operator'; +import { SkyDataGridFilterValue } from '../types/data-grid-filter-value'; +import { SkyDataGridNumberRangeFilterValue } from '../types/data-grid-number-range-filter-value'; + +interface SkyDataGridColumnFilter< + T extends Record<'id', string> = Record<'id', string> & + Record, +> { + filterId: string | undefined; + field: keyof T | undefined; + filterOperator: SkyDataGridFilterOperator | undefined; + type: 'text' | 'number' | 'date' | 'boolean'; +} + +/** + * @internal + */ +export function doesFilterPass< + T extends Record<'id', string> = Record<'id', string> & + Record, +>( + filterItems: SkyFilterStateFilterItem[], + data: Record<'id', string> & Partial, + columns: readonly SkyDataGridColumnFilter[], + logger: SkyLogService, +): boolean { + return filterItems.every((filterItem) => + doesSingleFilterPass(filterItem, data, columns, logger), + ); +} + +function doesSingleFilterPass< + T extends Record<'id', string> = Record<'id', string> & + Record, +>( + filter: SkyFilterStateFilterItem, + data: Record<'id', string> & Partial, + columns: readonly SkyDataGridColumnFilter[], + logger: SkyLogService, +): boolean { + // Find column with matching filterId + let column = columns.find((col) => col.filterId === filter.filterId); + column ??= columns.find((col) => col.field === filter.filterId); + if (!column || filter.filterValue?.value === undefined) { + return true; + } + + const rowValue = + column?.field && data && column?.field in data + ? data[column?.field as keyof T] + : undefined; + const filterValue = filter.filterValue.value as + | SkyDataGridFilterValue + | undefined; + const filterOperator = column?.filterOperator as + | SkyDataGridFilterOperator + | undefined; + + switch (column?.type) { + case 'text': + return doesTextFilterPass( + filterOperator ?? 'contains', + filterValue, + String(rowValue ?? ''), + logger, + ); + case 'number': + return doesNumericFilterPass( + filterValue as string | number | SkyDataGridNumberRangeFilterValue, + rowValue as string | number, + filterOperator, + logger, + ); + case 'date': + return doesDateFilterPass( + filterValue as SkyDateRange | Date | string, + rowValue as Date | string, + filterOperator, + logger, + ); + case 'boolean': + return doesBooleanFilterPass( + filterValue, + rowValue, + filterOperator, + logger, + ); + } +} + +function doesBooleanFilterPass( + filterValue: unknown, + rowValue: unknown, + filterOperator: SkyDataGridFilterOperator | undefined, + logger: SkyLogService, +): boolean { + if ( + filterOperator === 'notEqual' && + Boolean(rowValue) === Boolean(filterValue) + ) { + return false; + } else if ( + (filterOperator ?? 'equals') === 'equals' && + Boolean(rowValue) !== Boolean(filterValue) + ) { + return false; + } else if ( + filterOperator && + !['equals', 'notEqual'].includes(filterOperator) + ) { + logger.warn(`Unsupported boolean filter operator: ${filterOperator}`); + } + return true; +} + +function doesDateFilterPass( + filterValue: SkyDateRange | Date | string, + rowValue: Date | string, + filterOperator: SkyDataGridFilterOperator | undefined, + logger: SkyLogService, +): boolean { + const rowDate = asDate(rowValue); + if (isDateRangeFilter(filterValue)) { + const range = filterValue as SkyDateRange; + return !( + (range.startDate && zeroHour(rowDate) < zeroHour(range.startDate)) || + (range.endDate && zeroHour(rowDate) > zeroHour(range.endDate)) + ); + } else { + const filterDate = asDate(filterValue); + return numericFilter( + filterOperator ?? 'equals', + zeroHour(filterDate), + zeroHour(rowDate), + logger, + ); + } +} + +/** + * Date object at UTC midnight. + */ +function asDate(value: Date | string): Date { + const date = + value instanceof Date ? new Date(value) : new Date(value as string); + date.setHours( + date.getHours() + Math.floor(date.getTimezoneOffset() / 60), + date.getMinutes() + (date.getTimezoneOffset() % 60), + ); + date.setTime(zeroHour(date)); + return date; +} + +/** + * Timestamp at UTC midnight. + */ +function zeroHour(value: Date): number { + return Date.UTC(value.getFullYear(), value.getMonth(), value.getDate()); +} + +function doesNumericFilterPass( + filterValue: SkyDataGridNumberRangeFilterValue | string | number, + rowValue: string | number, + filterOperator: SkyDataGridFilterOperator | undefined, + logger: SkyLogService, +): boolean { + if (isNumberRangeFilter(filterValue)) { + if ( + typeof (filterValue.from ?? undefined) !== 'undefined' && + Number(rowValue) < Number(filterValue.from) + ) { + // If the low end is defined and the value is less than the low end. + return false; + } + if ( + typeof (filterValue.to ?? undefined) !== 'undefined' && + Number(rowValue) > Number(filterValue.to) + ) { + // If the high end is defined and the value is greater than the high end. + return false; + } + } else { + return numericFilter( + filterOperator ?? 'equals', + Number(filterValue), + Number(rowValue), + logger, + ); + } + return true; +} + +/** + * Ensures the provided value matches the number range filter shape. It must have a `from` and `to` property, + * with at least one being a number (both can be numbers). + */ +function isNumberRangeFilter( + value: unknown, +): value is SkyDataGridNumberRangeFilterValue { + return ( + typeof value === 'object' && + value !== null && + 'from' in value && + 'to' in value && + (typeof (value as SkyDataGridNumberRangeFilterValue).from === 'number' || + typeof (value as SkyDataGridNumberRangeFilterValue).to === 'number') && + (typeof (value as SkyDataGridNumberRangeFilterValue).from === 'number' || + value.from === null) && + (typeof (value as SkyDataGridNumberRangeFilterValue).to === 'number' || + value.to === null) + ); +} + +/** + * Ensures the provided value matches the date range filter shape. It must have a `startDate` and/or `endDate` property, + * with at least one being a Date object. + */ +function isDateRangeFilter(value: unknown): value is SkyDateRange { + return ( + typeof value === 'object' && + value !== null && + ('startDate' in value || 'endDate' in value) && + ((value as SkyDateRange).startDate instanceof Date || + (value as SkyDateRange).endDate instanceof Date) + ); +} + +function doesTextFilterPass( + filterOperator: SkyDataGridFilterOperator, + filterValue: unknown, + rowValue: string, + logger: SkyLogService, +): boolean { + const rowString = rowValue.normalize().toLocaleLowerCase(); + const filterArray: string[] = ( + Array.isArray(filterValue) ? filterValue : [filterValue] + ).map((value) => String(value).normalize().toLocaleLowerCase()); + + switch (filterOperator) { + case 'equals': + return filterArray.some((value) => value === rowString); + case 'notEqual': + return filterArray.every((value) => value !== rowString); + case 'contains': + return filterArray.some((value) => rowString.includes(value)); + case 'notContains': + return filterArray.every((value) => !rowString.includes(value)); + case 'startsWith': + return filterArray.some((value) => rowString.startsWith(value)); + case 'endsWith': + return filterArray.some((value) => rowString.endsWith(value)); + default: + logger.warn(`Unsupported text filter operator: ${filterOperator}`); + return true; + } +} + +function numericFilter( + filterOperator: SkyDataGridFilterOperator, + filterValue: number, + rowValue: number, + logger: SkyLogService, +): boolean { + switch (filterOperator) { + case 'equals': + return rowValue === filterValue; + case 'notEqual': + return rowValue !== filterValue; + case 'lessThan': + return rowValue < filterValue; + case 'lessThanOrEqual': + return rowValue <= filterValue; + case 'greaterThan': + return rowValue > filterValue; + case 'greaterThanOrEqual': + return rowValue >= filterValue; + default: + logger.warn( + `Unsupported number or date filter operator: ${filterOperator}`, + ); + return true; + } +} diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.css b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.css new file mode 100644 index 0000000000..a06b21df93 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.css @@ -0,0 +1,4 @@ +:host { + display: block; + max-width: 100%; +} diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.html b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.html new file mode 100644 index 0000000000..5850e8e557 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.html @@ -0,0 +1,49 @@ + +@if (gridOptions(); as gridOptions) { + @if (useDataManager) { + + + + } @else { + + + + } + + @if (pageCount(); as pageCount) { + + } +} diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.spec.ts b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.spec.ts new file mode 100644 index 0000000000..4c0d86118f --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.spec.ts @@ -0,0 +1,1502 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { provideLocationMocks } from '@angular/common/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { ActivatedRoute, Router, provideRouter } from '@angular/router'; +import { SkyAppTestUtility } from '@skyux-sdk/testing'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import { SkyAgGridWrapperHarness } from '@skyux/ag-grid/testing'; +import { SkyLogService } from '@skyux/core'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import { SkyDataManagerHarness } from '@skyux/data-manager/testing'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import { SkyWaitHarness } from '@skyux/indicators/testing'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import { SkyPagingHarness } from '@skyux/lists/testing'; +import { SkySearchHarness } from '@skyux/lookup/testing'; + +import { getGridApi } from 'ag-grid-community'; + +import { SkyDataGridComponent } from './data-grid.component'; +import { DataGridTestComponent } from './fixtures/data-grid-test.component'; +import { DataGridWDataManagerTestComponent } from './fixtures/data-grid-w-data-manager-test.component'; + +describe('SkyDataGridComponent', () => { + describe('without data manager', () => { + let fixture: ComponentFixture; + let component: DataGridTestComponent; + let loggerSpy: jasmine.Spy; + let loader: HarnessLoader; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideRouter([]), provideLocationMocks()], + }); + fixture = TestBed.createComponent(DataGridTestComponent); + loader = TestbedHarnessEnvironment.loader(fixture); + component = fixture.componentInstance; + const logger = TestBed.inject(SkyLogService); + loggerSpy = spyOn(logger, 'warn').and.returnValue(undefined); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should destroy and recreate grid', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + expect(component).toBeTruthy(); + + fixture.componentRef.setInput('showAllGrids', false); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.debugElement.queryAll(By.directive(SkyDataGridComponent)), + ).toEqual([]); + + fixture.componentRef.setInput('showAllGrids', true); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.debugElement.queryAll(By.directive(SkyDataGridComponent)) + .length, + ).toEqual(4); + }); + + it('should destroy and recreate columns', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + expect(component).toBeTruthy(); + const grids = await loader.getAllHarnesses(SkyAgGridWrapperHarness); + expect( + await Promise.all( + grids.map((grid) => grid.getDisplayedColumnIds()), + ).then((cols) => + cols.map((id) => id.length).reduce((a, b) => a + b, 0), + ), + ).toEqual(4 * 3 + 3); // 4 grids with 3 columns each, plus 3 extra headers for the multi-select and row delete grid + + fixture.componentRef.setInput('showCol3', false); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + await Promise.all( + grids.map((grid) => grid.getDisplayedColumnIds()), + ).then((cols) => + cols.map((id) => id.length).reduce((a, b) => a + b, 0), + ), + ).toEqual(4 * 2 + 4); // 4 grids with 2 columns each, plus 4 extra headers for multi-select, row delete, and date column + expect(component.visibleColumnIds()).toEqual(['column1', 'column2']); + + fixture.componentRef.setInput('showCol3', true); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + await Promise.all( + grids.map((grid) => grid.getDisplayedColumnIds()), + ).then((cols) => + cols.map((id) => id.length).reduce((a, b) => a + b, 0), + ), + ).toEqual(4 * 3 + 3); // 4 grids with 3 columns each, plus 3 extra headers for the multi-select and row delete grid + expect(component.visibleColumnIds()).toEqual([ + 'column1', + 'column2', + 'column3', + ]); + + fixture.componentRef.setInput('showAllColumns', false); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.nativeElement.querySelectorAll('sky-ag-grid-wrapper'), + ).toHaveSize(0); + }); + + it('should respond to displayedColumnIds input', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + expect(component).toBeTruthy(); + const grids = await loader.getAllHarnesses(SkyAgGridWrapperHarness); + expect( + await Promise.all( + grids.map((grid) => grid.getDisplayedColumnIds()), + ).then((cols) => cols.flatMap((id) => id).length), + ).toEqual(4 * 3 + 3); // 4 grids with 3 columns each, plus 3 extra headers for the multi-select and row delete grid + + fixture.componentRef.setInput('displayedColumnIds', [ + 'column1', + 'column2', + ]); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + await Promise.all( + grids.map((grid) => grid.getDisplayedColumnIds()), + ).then((cols) => cols.flatMap((id) => id).length), + ).toEqual(4 * 2 + 1); // 4 grids with 2 columns each, plus 1 extra header for multi-select + expect(component.visibleColumnIds()).toEqual(['column1', 'column2']); + + fixture.componentRef.setInput('displayedColumnIds', [ + 'column1', + 'column2', + 'column3', + ]); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + await Promise.all( + grids.map((grid) => grid.getDisplayedColumnIds()), + ).then((cols) => cols.flatMap((id) => id).length), + ).toEqual(4 * 3 + 1); // 4 grids with 3 columns each, plus 1 extra header for multi-select + expect(component.visibleColumnIds()).toEqual([ + 'column1', + 'column2', + 'column3', + ]); + + fixture.componentRef.setInput('showAllColumns', false); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.nativeElement.querySelectorAll('sky-ag-grid-wrapper'), + ).toHaveSize(0); + }); + + it('should handle empty data', async () => { + component.dataForSimpleGrid = undefined; + fixture.detectChanges(); + expect(component).toBeTruthy(); + const waitHarness = + await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyWaitHarness, + ); + await expectAsync(waitHarness.isWaiting()).toBeResolvedTo(false); + expect( + Array.from( + fixture.nativeElement + .querySelector('.ag-viewport') + .querySelectorAll('[role="row"]'), + ), + ).toHaveSize(0); + }); + + it('should handle data changing from populated to undefined', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + component.dataForSimpleGrid = undefined; + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(0); + }); + + it('should handle data changing from undefined to populated', async () => { + component.dataForSimpleGrid = undefined; + fixture.detectChanges(); + await fixture.whenStable(); + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(0); + + component.dataForSimpleGrid = [ + { id: '1', column1: '1', column2: 'Apple', column3: true }, + { id: '2', column1: '01', column2: 'Banana', column3: false }, + ]; + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(2); + }); + + it('should handle data sort', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + const gridElement = fixture.nativeElement.querySelector( + '[data-sky-id="grid"] ag-grid-angular', + ); + const api = getGridApi(gridElement); + expect(api).toBeTruthy(); + expect(api?.getState()?.sort?.sortModel).toBeUndefined(); + + fixture.componentRef.setInput('sortField', { + fieldSelector: 'column1', + descending: false, + }); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getState()?.sort?.sortModel).toEqual([ + { + colId: 'column1', + sort: 'asc', + }, + ]); + + fixture.componentRef.setInput('sortField', { + fieldSelector: 'column1', + descending: true, + }); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getState()?.sort?.sortModel).toEqual([ + { + colId: 'column1', + sort: 'desc', + }, + ]); + + const column2SortButton = gridElement.querySelector( + '.ag-header-cell.ag-header-cell-sortable[col-id="column2"] button.ag-header-cell-label-sortable', + ) as HTMLButtonElement; + expect(column2SortButton).toBeTruthy(); + SkyAppTestUtility.fireDomEvent(column2SortButton, 'click'); + await fixture.whenStable(); + SkyAppTestUtility.fireDomEvent(column2SortButton, 'click'); + await fixture.whenStable(); + + expect(fixture.componentInstance.sortField()).toEqual({ + fieldSelector: 'column2', + descending: true, + }); + expect(api?.getState()?.sort?.sortModel).toEqual([ + { + colId: 'column2', + sort: 'desc', + }, + ]); + + fixture.componentRef.setInput('sortField', undefined); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getState()?.sort?.sortModel).toBeUndefined(); + }); + + it('should update grid options when pageSize changes', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getGridOption('pagination')).toBeFalsy(); + + fixture.componentRef.setInput('pageSize', 2); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getGridOption('pagination')).toBeTruthy(); + expect(api?.getGridOption('paginationPageSize')).toBe(2); + }); + + it('should constrict grid page size when using externalRowCount', async () => { + fixture.componentRef.setInput('pageSize', 2); + fixture.componentRef.setInput('externalRowCount', 123); + fixture.detectChanges(); + await fixture.whenStable(); + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getGridOption('pagination')).toBeFalsy(); + expect(api?.getDisplayedRowCount()).toBe(2); + }); + + it('should reset page number when it is no longer valid', async () => { + fixture.componentRef.setInput('pageSize', 2); + fixture.detectChanges(); + await fixture.whenStable(); + const grid = await loader.getHarness(SkyAgGridWrapperHarness); + expect(await grid.isGridReady()).toBeTrue(); + expect(component.page()).toBe(1); + component.page.set(2); + fixture.detectChanges(); + await fixture.whenStable(); + const gridApi = getGridApi( + fixture.nativeElement.querySelector('ag-grid-angular'), + ); + expect(gridApi?.paginationGetCurrentPage()).toBe(1); // zero-based + fixture.componentRef.setInput('pageSize', 10); + fixture.detectChanges(); + await fixture.whenStable(); + expect(component.page()).toBe(1); + }); + + it('should update grid options when height changes', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getGridOption('domLayout')).toBe('autoHeight'); + + fixture.componentRef.setInput('height', 200); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getGridOption('domLayout')).toBe('normal'); + + fixture.componentRef.setInput('height', 0); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getGridOption('domLayout')).toBe('autoHeight'); + }); + + it('should update grid options when multiselect changes', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getGridOption('rowSelection')).toEqual( + jasmine.objectContaining({ + mode: 'singleRow', + enableClickSelection: false, + enableSelectionWithoutKeys: true, + checkboxes: false, + }), + ); + + fixture.componentRef.setInput('multiselect', true); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getGridOption('rowSelection')).toEqual({ + mode: 'multiRow', + checkboxes: true, + headerCheckbox: true, + checkboxLocation: 'selectionColumn', + }); + }); + + it('should select all rows', async () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + const waitHarness = + await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyWaitHarness, + ); + await expectAsync(waitHarness.isWaiting()).toBeResolvedTo(false); + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="multiselect-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getSelectedNodes()).toHaveSize(0); + api?.selectAll(); + expect(api?.getSelectedNodes()).toHaveSize(7); + }); + + it('should select some rows', async () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + fixture.detectChanges(); + await fixture.whenStable(); + const waitHarnesses = + await TestbedHarnessEnvironment.loader(fixture).getAllHarnesses( + SkyWaitHarness, + ); + await expectAsync( + Promise.all( + waitHarnesses.map((waitHarness) => waitHarness.isWaiting()), + ), + ).toBeResolvedTo([false, false, false, false]); + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="multiselect-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getSelectedNodes()).toHaveSize(0); + component.selectedRowIds.set(['102', '104', '106']); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getSelectedNodes()).toHaveSize(3); + }); + + it('should update selectedRowIds when data changes to remove IDs no longer in data', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + // Select some rows + component.selectedRowIds.set(['101', '102', '103', '104', '105']); + fixture.detectChanges(); + await fixture.whenStable(); + expect(component.selectedRowIds()).toEqual([ + '101', + '102', + '103', + '104', + '105', + ]); + + // Remove some items from the data (remove myId 102, 104) + component.dataForSimpleGridWithMultiselect = [ + { id: '1', column1: '1', column2: 'Apple', column3: true, myId: '101' }, + { + id: '3', + column1: '11', + column2: 'Banana', + column3: true, + myId: '103', + }, + { + id: '5', + column1: '13', + column2: 'Edamame', + column3: true, + myId: '105', + }, + ]; + fixture.detectChanges(); + await fixture.whenStable(); + + // selectedRowIds should be updated to only include IDs still in the data + expect(component.selectedRowIds()).toEqual(['101', '103', '105']); + }); + + it('should update selectedRowIds when data changes from populated to fewer items', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + // Select all rows + component.selectedRowIds.set([ + '101', + '102', + '103', + '104', + '105', + '106', + '107', + ]); + fixture.detectChanges(); + await fixture.whenStable(); + expect(component.selectedRowIds()).toHaveSize(7); + + // Reduce data to just 2 items + component.dataForSimpleGridWithMultiselect = [ + { id: '1', column1: '1', column2: 'Apple', column3: true, myId: '101' }, + { + id: '2', + column1: '01', + column2: 'Banana', + column3: false, + myId: '102', + }, + ]; + fixture.detectChanges(); + await fixture.whenStable(); + + // selectedRowIds should only include IDs that are still in the data + expect(component.selectedRowIds()).toEqual(['101', '102']); + }); + + it('should clear selectedRowIds when data changes to empty', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + // Select some rows + component.selectedRowIds.set(['101', '102', '103']); + fixture.detectChanges(); + await fixture.whenStable(); + expect(component.selectedRowIds()).toEqual(['101', '102', '103']); + + // Clear the data + component.dataForSimpleGridWithMultiselect = []; + fixture.detectChanges(); + await fixture.whenStable(); + + // selectedRowIds should be empty + expect(component.selectedRowIds()).toEqual([]); + }); + + it('should update selectedRowIds when filters are applied to remove IDs of filtered-out rows', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredMultiselectGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-multiselect-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Select some rows (ids 1, 2, 3, 4, 5) + component.selectedRowIds.set(['1', '2', '3', '4', '5']); + fixture.detectChanges(); + await fixture.whenStable(); + expect(component.selectedRowIds()).toEqual(['1', '2', '3', '4', '5']); + + // Apply a filter that only shows rows where column2 starts with 'B' (Banana) + // This should only include rows with ids 2 and 3 + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column2Filter', + filterValue: { value: 'B', displayValue: 'Starts with B' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should have 2 rows displayed + expect(api?.getDisplayedRowCount()).toBe(2); + + // selectedRowIds should only include IDs of visible rows (2 and 3) + expect(component.selectedRowIds()).toEqual(['2', '3']); + }); + + it('should restore selectedRowIds when filters are cleared', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredMultiselectGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-multiselect-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + + // Select rows 2 and 3 (Banana rows) + component.selectedRowIds.set(['2', '3']); + fixture.detectChanges(); + await fixture.whenStable(); + + // Apply filter that shows Banana rows + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column2Filter', + filterValue: { value: 'B', displayValue: 'Starts with B' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(2); + expect(component.selectedRowIds()).toEqual(['2', '3']); + + // Clear filters - all rows should be visible again + fixture.componentRef.setInput('appliedFilters', []); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // selectedRowIds should still be ['2', '3'] since those IDs are still valid + expect(component.selectedRowIds()).toEqual(['2', '3']); + }); + + it('should update selectedRowIds when filter changes to a more restrictive filter', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredMultiselectGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-multiselect-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + + // Select all rows + component.selectedRowIds.set(['1', '2', '3', '4', '5', '6', '7']); + fixture.detectChanges(); + await fixture.whenStable(); + expect(component.selectedRowIds()).toHaveSize(7); + + // Apply a filter that shows rows where column1 contains '1' + // Rows: 1 (column1='1'), 2 (column1='01'), 3 (column1='11'), 4 (column1='12'), 5 (column1='13'), 7 (column1='21') + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column1Filter', + filterValue: { value: '1', displayValue: 'Contains 1' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(6); + expect(component.selectedRowIds()).toEqual([ + '1', + '2', + '3', + '4', + '5', + '7', + ]); + + // Apply a more restrictive filter that shows only rows starting with 'B' + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column1Filter', + filterValue: { value: '1', displayValue: 'Contains 1' }, + }, + { + filterId: 'column2Filter', + filterValue: { value: 'B', displayValue: 'Starts with B' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Only rows 2 and 3 match both filters + expect(api?.getDisplayedRowCount()).toBe(2); + expect(component.selectedRowIds()).toEqual(['2', '3']); + }); + + it('should highlight row', async () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + const waitHarness = await TestbedHarnessEnvironment.loader( + fixture, + ).getHarness(SkyWaitHarness.with({ ancestor: '[data-sky-id="grid"]' })); + await expectAsync(waitHarness.isWaiting()).toBeResolvedTo(false); + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getRowNode('2')?.isSelected()).toBeFalse(); + fixture.componentRef.setInput('rowHighlightedId', '2'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getRowNode('2')?.isSelected()).toBeTrue(); + }); + + it('should handle paging without url changes', async () => { + fixture.componentRef.setInput('pageSize', 4); + fixture.detectChanges(); + await fixture.whenStable(); + expect(component).toBeTruthy(); + const pagingHarness = + await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyPagingHarness, + ); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1); + await pagingHarness.clickNextButton(); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(2); + await pagingHarness.clickPreviousButton(); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1); + }); + + it('should handle paging with url changes', async () => { + fixture.componentRef.setInput('pageSize', 2); + component.pageQueryParam = 'page'; + const router = TestBed.inject(Router); + const navSpy = spyOn(router, 'navigate'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(component).toBeTruthy(); + const pagingHarness = + await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyPagingHarness, + ); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1); + await pagingHarness.clickPageButton(2); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(2); + expect(navSpy).toHaveBeenCalledWith([], { + relativeTo: jasmine.any(ActivatedRoute), + queryParams: { page: 2 }, + queryParamsHandling: 'merge', + }); + navSpy.calls.reset(); + + component.page.set(2); + fixture.detectChanges(); + + await pagingHarness.clickPageButton(1); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1); + expect(navSpy).toHaveBeenCalledWith([], { + relativeTo: jasmine.any(ActivatedRoute), + queryParams: { page: null }, + queryParamsHandling: 'merge', + }); + navSpy.calls.reset(); + + component.page.set(1); + fixture.detectChanges(); + + await pagingHarness.clickPageButton(3); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(3); + expect(navSpy).toHaveBeenCalledWith([], { + relativeTo: jasmine.any(ActivatedRoute), + queryParams: { page: 3 }, + queryParamsHandling: 'merge', + }); + navSpy.calls.reset(); + + component.page.set(3); + fixture.detectChanges(); + + await pagingHarness.clickPageButton(2); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(2); + expect(navSpy).toHaveBeenCalledWith([], { + relativeTo: jasmine.any(ActivatedRoute), + queryParams: { page: 2 }, + queryParamsHandling: 'merge', + }); + navSpy.calls.reset(); + + component.page.set(0); + fixture.detectChanges(); + expect(navSpy).not.toHaveBeenCalledWith([], { + relativeTo: jasmine.any(ActivatedRoute), + queryParams: { page: 0 }, + queryParamsHandling: 'merge', + }); + + component.page.set(Number.POSITIVE_INFINITY); + fixture.detectChanges(); + expect(navSpy).not.toHaveBeenCalledWith([], { + relativeTo: jasmine.any(ActivatedRoute), + queryParams: { page: Number.POSITIVE_INFINITY }, + queryParamsHandling: 'merge', + }); + }); + + describe('apply filters', () => { + it('should not apply filter when externalRowCount is set', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.componentRef.setInput('externalRowCount', 123); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Apply a text filter + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column1Filter', + filterValue: { value: ['1'], displayValue: 'Contains "1"' }, + }, + { + filterId: 'column2Filter', + filterValue: { value: 'Ban', displayValue: 'Starts with Ban' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should not filter rows because externalRowCount is set and data has not been updated. + expect(api?.getDisplayedRowCount()).toBe(7); + }); + + it('should apply text filter to grid', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Apply a text filter + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column1Filter', + filterValue: { value: ['1'], displayValue: 'Contains "1"' }, + }, + { + filterId: 'column2Filter', + filterValue: { value: 'Ban', displayValue: 'Starts with Ban' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should filter to rows where column2 starts with 'Ban' (Banana) + expect(api?.getDisplayedRowCount()).toBe(2); + }); + + it('should apply multiple filters simultaneously', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Apply multiple filters + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column1Filter', + filterValue: { value: '1', displayValue: 'Contains 1' }, + }, + { + filterId: 'column2Filter', + filterValue: { value: 'B', displayValue: 'Starts with B' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should filter to rows where column1 contains '1' AND column2 starts with 'B' + // column1='1' has column2='Apple' (no match) + // column1='01' has column2='Banana' (match) + // column1='11' has column2='Banana' (match) + // column1='12' has column2='Daikon' (no match) + // column1='13' has column2='Edamame' (no match) + expect(api?.getDisplayedRowCount()).toBe(2); + }); + + it('should clear filters when appliedFilters is set to empty', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Apply filter first + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column2Filter', + filterValue: { value: 'Ban', displayValue: 'Starts with Ban' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(2); + + // Clear filters + fixture.componentRef.setInput('appliedFilters', []); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(api?.getDisplayedRowCount()).toBe(7); + }); + + it('should clear filters when appliedFilters is set to undefined', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Apply filter first + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column2Filter', + filterValue: { value: 'Ban', displayValue: 'Starts with Ban' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(2); + + // Clear filters by setting undefined + fixture.componentRef.setInput('appliedFilters', undefined); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(api?.getDisplayedRowCount()).toBe(7); + }); + + it('should ignore filters without matching column filterId', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Apply a filter with non-existent filterId + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'nonExistentFilter', + filterValue: { value: 'test', displayValue: 'Test' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should not filter any rows since the filterId doesn't match any column + expect(api?.getDisplayedRowCount()).toBe(7); + }); + + it('should ignore filters with undefined value', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Apply a filter with undefined value + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column2Filter', + filterValue: undefined, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should not filter any rows since the value is undefined + expect(api?.getDisplayedRowCount()).toBe(7); + }); + + it('should update filter when appliedFilters changes', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + + // Apply initial filter + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column2Filter', + filterValue: { value: 'A', displayValue: 'Starts with A' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should filter to Apple only + expect(api?.getDisplayedRowCount()).toBe(1); + + // Update filter + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column2Filter', + filterValue: { value: 'B', displayValue: 'Starts with B' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should now filter to Banana rows + expect(api?.getDisplayedRowCount()).toBe(2); + }); + + it('should apply text filter with operators', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Apply a text filter using a column without filterOperator (should default to 'contains') + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column2VarOperatorFilter', + filterValue: { value: 'ana', displayValue: 'Contains ana' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should filter to rows where column2 contains 'ana' (Banana) + expect(api?.getDisplayedRowCount()).toBe(2); + + fixture.componentRef.setInput('textFilterOperator', 'startsWith'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(0); + + fixture.componentRef.setInput('textFilterOperator', 'endsWith'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(2); + + fixture.componentRef.setInput('textFilterOperator', 'notContains'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(5); + + fixture.componentRef.setInput('textFilterOperator', 'equals'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(0); + + fixture.componentRef.setInput('textFilterOperator', 'notEqual'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Does not apply to text filters, but verify no errors occur. + fixture.componentRef.setInput('textFilterOperator', 'lessThanOrEqual'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(7); + expect(loggerSpy).toHaveBeenCalledWith( + `Unsupported text filter operator: lessThanOrEqual`, + ); + + // Update filter to use 'startsWith' operator + fixture.componentRef.setInput('textFilterOperator', 'startsWith'); + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column2VarOperatorFilter', + filterValue: { value: 'Ban', displayValue: 'Starts with Ban' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should still filter to rows where column2 starts with 'Ban' (Banana) + expect(api?.getDisplayedRowCount()).toBe(2); + + component.dataForFilteredGrid = [ + ...component.dataForFilteredGrid.map((item) => ({ + ...item, + column2: null, + })), + ]; + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(0); + }); + + it('should apply number filter to grid', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Apply a number filter (equals) + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'numericFilter', + filterValue: { value: 200, displayValue: 'Equals 200' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should filter to rows where numericColumn equals 200 + expect(api?.getDisplayedRowCount()).toBe(1); + + fixture.componentRef.setInput('numberFilterOperator', 'notEqual'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(6); + + fixture.componentRef.setInput( + 'numberFilterOperator', + 'lessThanOrEqual', + ); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(5); + + fixture.componentRef.setInput('numberFilterOperator', 'lessThan'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(4); + + fixture.componentRef.setInput( + 'numberFilterOperator', + 'greaterThanOrEqual', + ); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(3); + + fixture.componentRef.setInput('numberFilterOperator', 'greaterThan'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(2); + + // Does not apply to number filters, but verify no errors occur. + fixture.componentRef.setInput('numberFilterOperator', 'contains'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(7); + expect(loggerSpy).toHaveBeenCalledWith( + `Unsupported number or date filter operator: contains`, + ); + }); + + it('should apply number range filter to grid', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Apply a number range filter + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'numericFilter', + filterValue: { + value: { from: 150, to: 250 }, + displayValue: 'Between 150 and 250', + }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should filter to rows where numericColumn is between 150 and 250 (inclusive) + // Values: 100, 200, 150, 250, 175, 300, 125 -> 200, 150, 250, 175 = 4 rows + expect(api?.getDisplayedRowCount()).toBe(4); + + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'numericFilter', + filterValue: { + value: { from: null, to: 250 }, + displayValue: 'Up to 250', + }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(6); + + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'numericFilter', + filterValue: { + value: { from: 150, to: null }, + displayValue: 'Up from 150', + }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(5); + }); + + it('should apply date filter to grid', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Apply a date filter (equals) + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'dateFilter', + filterValue: { + value: new Date('2024-03-10T00:00:00.000Z'), + displayValue: 'March 10, 2024', + }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should filter to rows where dateColumn equals 2024-03-10 + expect(api?.getDisplayedRowCount()).toBe(1); + }); + + it('should apply date range filter to grid', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Apply a date range filter + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'dateFilter', + filterValue: { + value: { + startDate: new Date('2024-02-01T00:00:00.000Z'), + endDate: new Date('2024-05-01T00:00:00.000Z'), + }, + displayValue: 'Feb 1 to May 1, 2024', + }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should filter to rows where dateColumn is between Feb 1 and May 1, 2024 + // Dates: Jan 15, Feb 20, Mar 10, Apr 5, May 25, Jun 30, Jul 12 + // In range: Feb 20, Mar 10, Apr 5 = 3 rows + expect(api?.getDisplayedRowCount()).toBe(3); + + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'dateFilter', + filterValue: { + value: { + endDate: new Date('2024-05-01T00:00:00.000Z'), + }, + displayValue: 'Before May 1, 2024', + }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(4); + }); + + it('should apply boolean filter', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Apply a boolean filter + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'column3Filter', + filterValue: { value: true, displayValue: 'True' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Boolean filters are not supported by AG Grid, so no filtering should occur + expect(api?.getDisplayedRowCount()).toBe(4); + + fixture.componentRef.setInput('booleanFilterOperator', 'notEqual'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(3); + + // Does not apply to boolean filters, but verify no errors occur. + fixture.componentRef.setInput('booleanFilterOperator', 'contains'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(7); + expect(loggerSpy).toHaveBeenCalledWith( + `Unsupported boolean filter operator: contains`, + ); + }); + + it('should ignore filter when column with filterId does not exist in grid', async () => { + fixture.componentRef.setInput('showAllGrids', false); + fixture.componentRef.setInput('showFilteredGrid', true); + fixture.detectChanges(); + await fixture.whenStable(); + + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="filtered-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + // Apply a filter referencing a non-existent column + fixture.componentRef.setInput('appliedFilters', [ + { + filterId: 'nonExistentColumnFilter', + filterValue: { value: 'test', displayValue: 'Test' }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + + // Should not filter any rows since the column doesn't exist + expect(api?.getDisplayedRowCount()).toBe(7); + }); + }); + }); + + describe('with data manager', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideRouter([]), + provideLocationMocks(), + provideNoopAnimations(), + ], + }); + fixture = TestBed.createComponent(DataGridWDataManagerTestComponent); + loader = TestbedHarnessEnvironment.loader(fixture); + const logger = TestBed.inject(SkyLogService); + spyOn(logger, 'warn'); + }); + + it('should pick up displayed columns from data manager', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + const dm = await loader.getHarness( + SkyDataManagerHarness.with({ dataSkyId: 'my-data-manager' }), + ); + const columnChooser = await dm + .getToolbar() + .then(async (toolbar) => await toolbar.openColumnPicker()); + await columnChooser.clearAll(); + await columnChooser.selectColumns({ titleText: 'Name' }); + await columnChooser.saveAndClose(); + const gridElement = + fixture.nativeElement.querySelector('ag-grid-angular'); + expect(gridElement).toBeTruthy(); + const gridApi = getGridApi(gridElement); + expect(gridApi?.getAllDisplayedColumns()).toHaveSize(1); + }); + + it('should handle displayed columns from data manager when not specified', async () => { + fixture.componentInstance.displayedColumnIds = undefined; + fixture.detectChanges(); + await fixture.whenStable(); + const grid = await loader.getHarness(SkyAgGridWrapperHarness); + expect(await grid.isGridReady()).toBeTrue(); + const gridElement = + fixture.nativeElement.querySelector('ag-grid-angular'); + expect(gridElement).toBeTruthy(); + const gridApi = getGridApi(gridElement); + expect(gridApi?.getColumns()).toHaveSize(2); + expect(await grid.getDisplayedColumnIds()).toHaveSize(2); + }); + + it('should pick up search text from data manager', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + const gridElement = + fixture.nativeElement.querySelector('ag-grid-angular'); + expect(gridElement).toBeTruthy(); + const gridApi = getGridApi(gridElement); + expect(gridApi?.isDestroyed()).toBeFalse(); + const dm = await loader.getHarness( + SkyDataManagerHarness.with({ dataSkyId: 'my-data-manager' }), + ); + const toolbar = await dm.getToolbar(); + const search = (await toolbar.getSearch()) as SkySearchHarness; + expect(search).toBeTruthy(); + if (await search.isCollapsed()) { + await search.clickOpenSearchButton(); + } + await search.enterText('fruit'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(gridApi?.getGridOption('quickFilterText')).toBe('fruit'); + }); + + it('should send sort field update through data manager', async () => { + fixture.componentRef.setInput('sortField', { + fieldSelector: 'category', + descending: false, + }); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.componentInstance.dataManagerState().activeSortOption, + ).toEqual({ + id: 'category', + propertyName: 'category', + label: 'Category', + descending: false, + }); + }); + }); +}); diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.ts b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.ts new file mode 100644 index 0000000000..fa4de5a9cd --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.ts @@ -0,0 +1,897 @@ +import { + coerceArray, + coerceBooleanProperty, + coerceNumberProperty, + coerceStringArray, +} from '@angular/cdk/coercion'; +import { + ChangeDetectionStrategy, + Component, + computed, + contentChildren, + effect, + inject, + input, + model, + output, + signal, + untracked, +} from '@angular/core'; +import { + takeUntilDestroyed, + toObservable, + toSignal, +} from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Router } from '@angular/router'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { SkyLogService, SkyViewkeeperModule } from '@skyux/core'; +import { SkyDataManagerService } from '@skyux/data-manager'; +import { SkyWaitModule } from '@skyux/indicators'; +import { + SkyDataHost, + SkyDataHostService, + SkyFilterStateFilterItem, + SkyPagingModule, +} from '@skyux/lists'; + +import { AgGridAngular } from 'ag-grid-angular'; +import { + AllCommunityModule, + AutoSizeStrategy, + ColDef, + ColumnMovedEvent, + DisplayedColumnsChangedEvent, + GetRowIdParams, + GridApi, + GridOptions, + GridPreDestroyedEvent, + IRowNode, + ModuleRegistry, + RowSelectionOptions, + SelectionChangedEvent, + SortChangedEvent, + SortDirection, +} from 'ag-grid-community'; +import { + EMPTY, + ObservableInput, + distinctUntilChanged, + filter, + fromEvent, + fromEventPattern, + map, + merge, + startWith, + switchMap, + takeUntil, +} from 'rxjs'; + +import { SkyDataGridFilterValue } from '../types/data-grid-filter-value'; +import { SkyDataGridPageRequest } from '../types/data-grid-page-request'; +import { SkyDataGridRowDeleteCancelArgs } from '../types/data-grid-row-delete-cancel-args'; +import { SkyDataGridRowDeleteConfirmArgs } from '../types/data-grid-row-delete-confirm-args'; +import { SkyDataGridSort } from '../types/data-grid-sort'; + +import { SkyDataGridColumnInlineHelpComponent } from './data-grid-column-inline-help.component'; +import { SkyDataGridColumnComponent } from './data-grid-column.component'; +import { doesFilterPass } from './data-grid-filter'; + +ModuleRegistry.registerModules([AllCommunityModule]); + +function arraySorted(arr: string[]): string[] { + return arr.slice().sort((a, b) => a.localeCompare(b)); +} + +function arrayIsEqual( + a: string[] | undefined, + b: string[] | undefined, +): boolean { + if (!Array.isArray(a) || !Array.isArray(b) || a?.length !== b?.length) { + return false; + } + const bSorted = arraySorted(b); + return arraySorted(a).every((v, i) => v === bSorted[i]); +} + +/** + * @preview + */ +@Component({ + selector: 'sky-data-grid', + imports: [ + AgGridAngular, + SkyAgGridModule, + SkyPagingModule, + SkyViewkeeperModule, + SkyWaitModule, + ], + templateUrl: './data-grid.component.html', + styleUrl: './data-grid.component.css', + host: { + '[class.sky-margin-stacked-lg]': 'stacked()', + '[style.width.px]': 'width() || undefined', + }, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SkyDataGridComponent< + T extends Record<'id', string> = Record<'id', string> & + Record, +> { + /** + * The filter state from a + * [`SkyFilterBarComponent`](https://developer.blackbaud.com/skyux/components/filter-bar?docs-active-tab=development#class_sky-filter-bar-component). + * When provided, filters are automatically applied to columns that have matching `filterId` values using the + * respective `SkyDataGridColumnComponent`'s `filterOperator` as the comparator. To use the built-in filters, the + * filter values are: + * + * - For a boolean column, use a `boolean` with `'equals'` or `'notEqual'` as the operator. + * - For a date column, use a [`SkyDateRange`](https://developer.blackbaud.com/skyux/components/date-range-picker?docs-active-tab=development#interface_sky-date-range) or `Date`. + * - For a number column, use a `SkyDataGridNumberRangeFilterValue` or a `number`. + * - For a text column, use `string` or `string[]` as the filter value to match one or more text values. + * + * To provide custom filtering functions, use the `externalRowCount` input and update the `data` input when filters change. + */ + public readonly appliedFilters = input< + SkyFilterStateFilterItem[] + >([]); + + /** + * Enable a compact layout for the grid when using modern theme. Compact layout uses + * a smaller font size and row height to display more data in a smaller space. + * @default false + */ + public readonly compact = input(false, { + transform: coerceBooleanProperty, + }); + + /** + * The data for the grid. Each item requires an `id`, and other properties should map to a `field` of the grid columns. + * When `data` is `null` or `undefined`, the grid will show a loading indicator, and when `data` is an empty array, + * the grid will show a "no rows" message. + */ + public readonly data = input(); + + /** + * The number of records when using a remote data source. + * When `externalRowCount` is set, it is expected that `data` will be updated whenever `pageRequest` emits a new value. + * When both `externalRowCount` and `pageSize` are set, the number of pages is assumed to be `Math.ceil(externalRowCount / pageSize)`. + * If `externalRowCount` is not set, the data grid will page, sort, filter, and apply SKY UX data manager search text to the + * `data` provided, and the row count is assumed to be `data.length`. + */ + public readonly externalRowCount = input(undefined); + + /** + * How the grid fits to its parent. The valid options are `width`, + * which fits the grid to the parent's full width, and `scroll`, which allows the grid + * to exceed the parent's width. If the grid does not have enough columns to fill + * the parent's width, it always stretches to the parent's full width. + * @default 'width' + */ + public readonly fit = input<'width' | 'scroll'>('width'); + + /** + * The height of the grid in CSS pixels. For best performance, large grids should set a `height` value and not enable + * `wrapText` on any column so that rows can be virtually drawn as needed. When `wrapText` is enabled on any column, + * or when `height` is not set, the grid needs to build every row in order to determine the scroll height, creating + * hundreds or thousands of invisible DOM elements and slowing down the browser. + */ + public readonly height = input(0, { + transform: (val: unknown) => coerceNumberProperty(val, 0), + }); + + /** + * The column IDs or fields for columns to hide. Should not be combined with `displayedColumnIds`. + */ + public readonly hiddenColumnIds = input([], { + transform: coerceStringArray, + }); + + /** + * Whether to enable the multiselect feature to display a column of + * checkboxes on the left side of the grid. You can specify a unique ID with + * the `multiselectRowId` property, but multiselect defaults to the `id` property on + * the `data` object. + * @default false + */ + public readonly multiselect = input(false, { + transform: coerceBooleanProperty, + }); + + /** + * The unique ID that matches a property on the `data` object. + * @default 'id' + */ + public readonly multiselectRowId = input('id'); + + /** + * The number of items to display per page. Setting this value enables pagination. + */ + public readonly pageSize = input(0, { + transform: (value: unknown) => coerceNumberProperty(value, 0), + }); + + /** + * The query parameter name that stores the current page number. + * When set, the grid syncs page changes to the URL for deep linking, and there should only be one grid on the page. + */ + public readonly pageQueryParam = input(); + + /** + * The ID of the row to highlight. The ID matches the `multiselectRowId` property + * of the `data` object. Typically, this property is used in conjunction with + * the flyout component to indicate the currently selected row. Other rows + * are de-selected in the grid. + */ + public readonly rowHighlightedId = input(); + + /** + * Whether the data grid is stacked with another element below it. When specified, the appropriate + * vertical spacing is automatically added to the data grid. + * @default false + */ + public readonly stacked = input(false, { + transform: coerceBooleanProperty, + }); + + /** + * Move the horizontal scrollbar to just below the header row. + * @default false + */ + public readonly topScrollEnabled = input(false, { + transform: coerceBooleanProperty, + }); + + /** + * The width of the grid in CSS pixels. When no width is set, the grid will use the width of its container. + */ + public readonly width = input(0, { + transform: (val: unknown) => coerceNumberProperty(val, 0), + }); + + /** + * The column IDs or fields for columns to show. Should not be combined with `hiddenColumnIds`. + */ + public readonly displayedColumnIds = input([], { + transform: coerceStringArray, + }); + + /** + * Fires when columns change. This includes changes to the displayed columns and changes + * to the order of columns. The event emits an array of IDs for the displayed columns that + * reflects the column order. + */ + public readonly displayedColumnIdsChange = output(); + + /** + * The current page number of the grid when `pageSize` has been set. + */ + public readonly page = model(1); + + /** + * The set of IDs for the rows to prompt for delete confirmation. + * The IDs match the `multiselectRowId` properties of the `data` objects. + */ + public readonly rowDeleteIds = model([]); + + /** + * The set of IDs for the rows to select in a multiselect grid. + * The IDs match the `multiselectRowId` properties of the `data` objects. + * Rows with IDs that are not included are de-selected in the grid. + */ + public readonly selectedRowIds = model([]); + + /** + * The sort setting for the grid. + */ + public readonly sortField = model(undefined); + + /** + * Fires when sorting or page number changes. + */ + public readonly pageRequest = output(); + + /** + * Emits a row count after filters are updated. Not used when `externalRowCount` is set. + */ + public readonly rowCountChange = output(); + + /** + * Fires when users cancel the deletion of a row. + */ + public readonly rowDeleteCancel = output(); + + /** + * Fires when users confirm the deletion of a row. + */ + public readonly rowDeleteConfirm = output(); + + protected readonly columns = contentChildren(SkyDataGridColumnComponent); + protected readonly gridApi = signal | undefined>(undefined); + protected readonly gridOptions = computed(() => { + const columnDefs = this.#columnDefs(); + if (columnDefs.length === 0) { + return undefined; + } + const { pagination, paginationPageSize } = untracked(() => + this.#paginationOptions(), + ); + const rowData = untracked(() => this.rowData()); + return this.#gridService.getGridOptions({ + gridOptions: { + columnDefs, + context: { + enableTopScroll: untracked(() => this.topScrollEnabled()), + }, + domLayout: untracked(() => this.height()) ? 'normal' : 'autoHeight', + onGridReady: (args) => { + this.gridApi.set(args.api); + this.gridReady.set(true); + }, + pagination, + paginationPageSize, + suppressPaginationPanel: true, + rowData: rowData.length ? rowData : null, + getRowId: (params: GetRowIdParams) => + params.data[ + untracked(() => this.multiselectRowId()) as keyof T + ] as string, + rowSelection: untracked(() => this.#getRowSelection()), + autoSizeStrategy: untracked(() => this.#getAutoSizeStrategy()), + }, + }) as GridOptions; + }); + + protected readonly gridReady = signal(false); + protected readonly rowData = computed(() => { + const pageSize = this.pageSize(); + const useInternalFilters = this.#useInternalFilters(); + let data = this.data() ?? []; + if (pageSize > 0 && !useInternalFilters) { + data = data.slice(0, pageSize); + } + return data; + }); + + protected readonly isExternalFilterPresent = computed(() => { + const hasFilters = (this.appliedFilters() ?? []).length > 0; + const useInternalFilters = this.#useInternalFilters(); + return hasFilters && useInternalFilters; + }); + + protected readonly doesExternalFilterPass = computed( + (): ((node: Pick, 'data'>) => boolean) => { + const appliedFilters = this.appliedFilters(); + const columns = this.columns().map((col) => ({ + filterId: col.filterId(), + field: col.field() as keyof T | undefined, + filterOperator: col.filterOperator(), + type: col.dataType(), + })); + return (node: Pick, 'data'>): boolean => + doesFilterPass( + appliedFilters, + node.data as Record<'id', string> & Partial, + columns, + this.#logger, + ); + }, + ); + + protected readonly pageCount = computed(() => { + const dataLength = this.rowData().length; + const externalRowCount = this.externalRowCount(); + const pageSize = this.pageSize(); + const gridReady = this.gridReady(); + if (!gridReady || pageSize === 0) { + return 0; + } + return Math.ceil((externalRowCount ?? dataLength) / pageSize); + }); + + protected readonly useDataManager = !!inject(SkyDataManagerService, { + optional: true, + }); + protected readonly viewId = computed( + () => this.#dataHostService?.hostId() ?? '', + ); + protected readonly skyViewkeeper = computed(() => { + // Only used when not using SkyDataManagerService because data manager handles SkyViewkeeper. + const classes = ['.ag-header']; + if (this.topScrollEnabled()) { + classes.push('.ag-body-horizontal-scroll'); + } + return classes; + }); + + readonly #activatedRoute = inject(ActivatedRoute, { optional: true }); + readonly #dataHostService = inject(SkyDataHostService, { optional: true }); + readonly #dataHostServiceUpdates = toSignal( + this.#dataHostService?.getDataHostUpdates('SkyDataGridComponent') ?? EMPTY, + ); + readonly #dataHostDisplayedColumnIds = computed( + () => this.#dataHostServiceUpdates()?.displayedColumnIds ?? [], + ); + readonly #dataHostSearchText = computed( + () => this.#dataHostServiceUpdates()?.searchText ?? '', + ); + readonly #gridService = inject(SkyAgGridService); + readonly #logger = inject(SkyLogService); + readonly #router = inject(Router, { optional: true }); + + readonly #columnDefs = computed[]>(() => { + const columns = this.columns(); + const sort = this.sortField(); + const displayed = this.#displayedColumnIds(); + const hidden = this.hiddenColumnIds().filter(Boolean); + return columns.map((col) => + this.#createColDef(col, displayed, hidden, sort), + ); + }); + readonly #displayedColumnIds = computed(() => { + const displayedColumnIds = this.displayedColumnIds().filter(Boolean); + const dataHostDisplayedColumnIds = this.#dataHostDisplayedColumnIds(); + const notHidden = this.columns() + .filter((col) => !col.hidden()) + .map((col) => this.#getColumnIdOrField(col)); + if (dataHostDisplayedColumnIds.length > 0) { + return dataHostDisplayedColumnIds; + } + if (displayedColumnIds.length > 0) { + return displayedColumnIds; + } + return notHidden; + }); + + readonly #dataHost = computed(() => { + const sortField = this.sortField(); + const id = this.viewId(); + const dataHost = untracked(() => this.#dataHostServiceUpdates()); + if (dataHost) { + return { + activeSortOption: sortField + ? { + propertyName: sortField.fieldSelector, + descending: !!sortField.descending, + } + : undefined, + displayedColumnIds: this.#displayedColumnIds(), + id, + page: this.page(), + searchText: dataHost.searchText, + selectedIds: this.selectedRowIds(), + }; + } + return undefined; + }); + + readonly #gridDestroyed = toObservable(this.gridApi).pipe( + filter(Boolean), + switchMap((api) => + fromEventPattern((handler) => + api.addEventListener('gridPreDestroyed', handler), + ), + ), + ); + readonly #gridSelectedRowIds = toObservable(this.gridApi).pipe( + filter(Boolean), + switchMap((api) => + fromEvent(api, 'selectionChanged').pipe( + takeUntil(this.#gridDestroyed), + map((selection) => + arraySorted(this.#getRowIds(selection.selectedNodes)), + ), + distinctUntilChanged(arrayIsEqual), + ), + ), + ); + readonly #gridDisplayedColumnIds = toObservable(this.gridApi).pipe( + filter(Boolean), + switchMap((api) => + merge( + fromEvent(api, 'columnMoved').pipe( + takeUntil(this.#gridDestroyed), + map((columnsEvent) => + columnsEvent.api + .getAllDisplayedColumns() + .map((col) => col.getColId()), + ), + ), + fromEvent( + api, + 'displayedColumnsChanged', + ).pipe( + takeUntil(this.#gridDestroyed), + map((columnsEvent) => + columnsEvent.api + .getAllDisplayedColumns() + .map((col) => col.getColId()), + ), + distinctUntilChanged(arrayIsEqual), + ), + ), + ), + ); + readonly #gridSortChange = toObservable(this.gridApi).pipe( + filter(Boolean), + switchMap((api) => + fromEvent(api, 'sortChanged').pipe( + takeUntil(this.#gridDestroyed), + map((sortEvent): SkyDataGridSort | undefined => { + const sortColumn = sortEvent?.columns?.find((col) => !!col.getSort()); + if (sortColumn) { + return { + descending: sortColumn.getSort() === 'desc', + fieldSelector: sortColumn.getColId(), + }; + } + return undefined; + }), + ), + ), + ); + readonly #paginationOptions = computed(() => { + const pageSize = this.pageSize(); + const hasPageSize = pageSize > 0; + const useInternalFilters = this.#useInternalFilters(); + const pagination = hasPageSize && useInternalFilters; + const paginationPageSize = (pagination && pageSize) || undefined; + return { + pagination, + paginationPageSize, + }; + }); + readonly #useInternalFilters = computed(() => { + const externalRowCount = this.externalRowCount(); + return typeof externalRowCount === 'undefined'; + }); + readonly #queryParamPage = toSignal( + toObservable(this.pageQueryParam).pipe( + switchMap( + (pageQueryParam): ObservableInput => + pageQueryParam && this.#activatedRoute + ? this.#activatedRoute.queryParamMap.pipe( + startWith(this.#activatedRoute.snapshot.queryParamMap), + map((params) => + coerceNumberProperty(params.get(pageQueryParam), 1), + ), + ) + : [], + ), + ), + { initialValue: 1 }, + ); + + constructor() { + // Update specific grid options after the grid has been loaded. + effect(() => { + const api = untracked(() => this.gridApi()); + const columnDefs = this.#columnDefs(); + api?.setGridOption('columnDefs', columnDefs); + }); + effect(() => { + const api = untracked(() => this.gridApi()); + const height = this.height(); + api?.setGridOption('domLayout', height ? 'normal' : 'autoHeight'); + }); + effect(() => { + const api = untracked(() => this.gridApi()); + const loading = (this.data() ?? 'loading') === 'loading'; + api?.setGridOption('loading', loading); + }); + effect(() => { + const api = untracked(() => this.gridApi()); + const { pagination, paginationPageSize } = this.#paginationOptions(); + api?.setGridOption('pagination', pagination); + api?.setGridOption('paginationPageSize', paginationPageSize); + }); + effect(() => { + const api = untracked(() => this.gridApi()); + const rowData = this.rowData(); + api?.setGridOption('rowData', rowData); + }); + effect(() => { + const api = untracked(() => this.gridApi()); + const rowSelection = this.#getRowSelection(); + api?.setGridOption('rowSelection', rowSelection); + }); + + // Apply inputs once the grid is loaded and on subsequent changes. + effect(() => { + const api = this.gridApi(); + const multiselectRowId = this.multiselectRowId(); + const validRowIds = this.rowData().map((row): string => + String(row[multiselectRowId as keyof T]), + ); + const selectedRowIds = coerceStringArray(this.selectedRowIds()); + const validSelectedRowIds = selectedRowIds.filter((id) => + validRowIds.includes(id), + ); + if (!arrayIsEqual(validSelectedRowIds, selectedRowIds)) { + this.selectedRowIds.set(validSelectedRowIds); + } + const currentSelectedRowIds = this.#getRowIds(api?.getSelectedNodes()); + if (!arrayIsEqual(validSelectedRowIds, currentSelectedRowIds)) { + api?.deselectAll(); + validSelectedRowIds.forEach((rowId) => + api?.getRowNode(rowId)?.setSelected(true), + ); + } + }); + effect(() => { + const api = this.gridApi(); + const rowHighlightedId = this.rowHighlightedId(); + this.data(); + if (rowHighlightedId) { + const rowNode = api?.getRowNode(rowHighlightedId); + if (rowNode?.isSelected() === false) { + rowNode?.setSelected(true, true); + } + } + }); + effect(() => { + const api = this.gridApi(); + const page = this.page(); + const pageCount = this.pageCount(); + if (!pageCount || !api) { + return; + } + if (page < 1 || page > pageCount) { + this.page.set(1); + } else { + api.paginationGoToPage(page - 1); + } + }); + + // Sync page from URL query parameter. + effect(() => { + const queryParamPage = this.#queryParamPage(); + this.page.set(queryParamPage); + }); + + this.#gridDestroyed.pipe(takeUntilDestroyed()).subscribe(() => { + this.gridApi.set(undefined); + this.gridReady.set(false); + }); + + // Emit updates from the grid. + this.#gridSelectedRowIds + .pipe( + takeUntilDestroyed(), + map((ids) => coerceStringArray(ids)), + filter((rowIds) => !arrayIsEqual(this.selectedRowIds(), rowIds)), + ) + .subscribe((rowIds) => { + this.selectedRowIds.set(rowIds); + }); + this.#gridDisplayedColumnIds + .pipe( + takeUntilDestroyed(), + map((ids) => coerceStringArray(ids)), + ) + .subscribe((columnIds) => { + this.displayedColumnIdsChange.emit(columnIds); + }); + this.#gridSortChange.pipe(takeUntilDestroyed()).subscribe((sortChange) => { + if (sortChange) { + this.sortField.update((sort) => { + if ( + !!sort !== !!sortChange || + !!sort?.descending !== !!sortChange?.descending || + sort?.fieldSelector !== sortChange?.fieldSelector + ) { + return sortChange; + } + return sort; + }); + } + }); + effect(() => { + const isExternalFilterPresent = this.isExternalFilterPresent(); + const doesExternalFilterPass = this.doesExternalFilterPass(); + let rowData = [...this.rowData()]; + const searchText = this.#dataHostSearchText().normalize().toLowerCase(); + const useInternalFilters = this.#useInternalFilters(); + const multiselectRowId = this.multiselectRowId(); + if (useInternalFilters) { + if (isExternalFilterPresent) { + rowData = rowData.filter((data) => doesExternalFilterPass({ data })); + } + if (searchText) { + rowData = rowData.filter((data) => + Object.values(data).some((value) => + String(value ?? '') + .normalize() + .toLowerCase() + .includes(searchText), + ), + ); + } + const validRowIds = rowData.map((row): string => + String(row[multiselectRowId as keyof T]), + ); + const selectedRowIds = coerceStringArray(this.selectedRowIds()); + const validSelectedRowIds = selectedRowIds.filter((id) => + validRowIds.includes(id), + ); + if (!arrayIsEqual(validSelectedRowIds, selectedRowIds)) { + this.selectedRowIds.set(validSelectedRowIds); + } + this.rowCountChange.emit(rowData.length); + } + }); + + effect(() => { + const searchText = this.#dataHostSearchText(); + const api = this.gridApi(); + const useInternalFilters = this.#useInternalFilters(); + if (useInternalFilters) { + api?.setGridOption('quickFilterText', searchText); + } + }); + + effect(() => { + const dataHost = this.#dataHost(); + if (dataHost) { + this.#dataHostService?.updateDataHost(dataHost, 'SkyDataGridComponent'); + } + }); + + effect(() => { + this.pageRequest.emit({ + pageNumber: this.page(), + pageSize: this.pageSize() || undefined, + sortField: this.sortField(), + }); + }, {}); + } + + protected currentPageChange(page: number): void { + if (page && page !== this.page()) { + const pageQueryParam = this.pageQueryParam(); + if (pageQueryParam) { + // When using a query parameter, send the change through the router. + void this.#router?.navigate([], { + relativeTo: this.#activatedRoute, + queryParams: { + [pageQueryParam]: page === 1 ? null : page, + }, + queryParamsHandling: 'merge', + }); + } else { + this.page.set(page); + } + } + } + + #createColDef( + col: SkyDataGridColumnComponent, + displayed: string[], + hidden: string[], + sort: SkyDataGridSort | undefined, + ): ColDef { + const field = col.field(); + const colDef: ColDef = { + colId: col.columnId(), + field, + headerName: col.headingText(), + headerComponentParams: this.#getHeaderComponentParams(col), + hide: this.#hideColumn(col, displayed, hidden), + resizable: col.resizable(), + sortable: col.sortable(), + lockPosition: col.locked(), + suppressMovable: col.locked(), + type: [], + autoHeight: col.wrapText(), + wrapText: col.wrapText(), + sort: this.#getSort(sort, col), + }; + if (col.dataType() === 'date') { + (colDef.type as string[]).push(SkyCellType.Date); + colDef.cellDataType = 'dateString'; + } else if (field && col.dataType() === 'number') { + (colDef.type as string[]).push(SkyCellType.Number); + colDef.cellDataType = 'number'; + colDef.valueGetter = (params): number => Number(params.data[field]); + } else if (col.dataType() === 'boolean') { + colDef.cellDataType = 'boolean'; + } else { + (colDef.type as string[]).push(SkyCellType.Text); + colDef.cellDataType = 'text'; + } + if (col.cellTemplate()) { + (colDef.type as string[]).push(SkyCellType.Template); + colDef.cellRendererParams = { template: col.cellTemplate }; + } + this.#applyColumnWidthSettings(col, colDef); + return colDef; + } + + #applyColumnWidthSettings( + col: SkyDataGridColumnComponent, + colDef: ColDef, + ): void { + if (col.flexWidth() > -1) { + colDef.initialFlex = col.flexWidth(); + } else if (col.width() > 0) { + colDef.initialWidth = col.width(); + } + if (col.width() > 0) { + colDef.minWidth = col.width(); + colDef.suppressSizeToFit = true; + } + if (!col.resizable() || col.flexWidth() === 0) { + colDef.suppressSizeToFit = true; + colDef.suppressAutoSize = true; + } + } + + #getSort( + sort: SkyDataGridSort | undefined, + col: SkyDataGridColumnComponent, + ): SortDirection { + return sort?.fieldSelector === col.field() + ? sort?.descending + ? 'desc' + : 'asc' + : null; + } + + #getHeaderComponentParams(col: SkyDataGridColumnComponent): object { + return { + headerHidden: col.headingHidden(), + helpPopoverTitle: col.helpPopoverTitle(), + helpPopoverContent: col.helpPopoverContent() || col.description(), + inlineHelpComponent: SkyDataGridColumnInlineHelpComponent, + }; + } + + #getColumnIdOrField(col: SkyDataGridColumnComponent): string { + const id = col.columnId(); + const field = col.field() || ''; + return id || field; + } + + #hideColumn( + col: SkyDataGridColumnComponent, + displayed: string[], + hidden: string[], + ): boolean { + return ( + col.hidden() || + (displayed.length > 0 && + !displayed.includes(this.#getColumnIdOrField(col))) || + hidden.includes(this.#getColumnIdOrField(col)) + ); + } + + #getRowIds(rows: (IRowNode | undefined)[] | null | undefined): string[] { + return coerceArray(rows) + .map((node) => node?.id as string) + .filter(Boolean) as string[]; + } + + #getAutoSizeStrategy(): AutoSizeStrategy { + const width = this.width(); + return this.fit() === 'width' || width + ? { + type: 'fitGridWidth', + } + : { + type: 'fitCellContents', + }; + } + + #getRowSelection(): RowSelectionOptions { + return this.multiselect() + ? { + checkboxes: true, + checkboxLocation: 'selectionColumn', + headerCheckbox: true, + mode: 'multiRow', + } + : { + checkboxes: false, + mode: 'singleRow', + }; + } +} diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid.module.ts b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.module.ts new file mode 100644 index 0000000000..bb2bcee5f5 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; + +import { SkyDataGridColumnComponent } from './data-grid-column.component'; +import { SkyDataGridComponent } from './data-grid.component'; + +/** + * @preview + */ +@NgModule({ + exports: [SkyDataGridComponent, SkyDataGridColumnComponent], + imports: [SkyDataGridComponent, SkyDataGridColumnComponent], +}) +export class SkyDataGridModule {} diff --git a/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-test.component.html b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-test.component.html new file mode 100644 index 0000000000..8291d4c8a6 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-test.component.html @@ -0,0 +1,251 @@ +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + @if (showCol3()) { + + } + } + +} +

Grid with multiselect enabled

+ + + + + + + +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + + @if (showCol3()) { + + } + } + +} +

Selected rows: {{ selectedRowIds() | json }}

+ +

Grid with row delete

+ +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + + @if (row.id) { + + + + + + + + } + + + + + @if (showCol3()) { + + } @else { + + } + } + +} + + This is a numeric column. Click here to learn more. + + +

Grid w/ aligned columns and inline help

+ +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + @if (showCol3()) { + + } + } + +} + +@if (showFilteredMultiselectGrid()) { +

Grid with filters and multiselect

+ + + + +} + +@if (showFilteredGrid()) { +

Grid with filters

+ + + + + + + + +} diff --git a/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-test.component.ts b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-test.component.ts new file mode 100644 index 0000000000..def8e8aa79 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-test.component.ts @@ -0,0 +1,200 @@ +import { JsonPipe } from '@angular/common'; +import { + ChangeDetectorRef, + Component, + inject, + input, + model, +} from '@angular/core'; +import { SkyFilterBarFilterItem } from '@skyux/filter-bar'; +import { SkyDropdownModule, SkyPopoverModule } from '@skyux/popovers'; + +import { SkyDataGridFilterOperator } from '../../types/data-grid-filter-operator'; +import { SkyDataGridFilterValue } from '../../types/data-grid-filter-value'; +import { SkyDataGridRowDeleteCancelArgs } from '../../types/data-grid-row-delete-cancel-args'; +import { SkyDataGridRowDeleteConfirmArgs } from '../../types/data-grid-row-delete-confirm-args'; +import { SkyDataGridSort } from '../../types/data-grid-sort'; +import { SkyDataGridColumnComponent } from '../data-grid-column.component'; +import { SkyDataGridComponent } from '../data-grid.component'; + +interface RowModel { + id: string; + column1: string; + column2: string; + column3: boolean; + myId?: string; +} + +interface FilteredRowModel { + id: string; + column1: string; + column2: string | null; + column3: boolean; + numericColumn: number; + dateColumn: string; +} + +@Component({ + selector: 'app-data-grid-test', + templateUrl: './data-grid-test.component.html', + imports: [ + SkyDataGridComponent, + SkyDataGridColumnComponent, + SkyPopoverModule, + SkyDropdownModule, + JsonPipe, + ], +}) +export class DataGridTestComponent { + public dataForRowDeleteGrid: RowModel[] | undefined = [ + { id: '1', column1: '1', column2: 'Apple', column3: true }, + { id: '2', column1: '01', column2: 'Banana', column3: false }, + { id: '3', column1: '11', column2: 'Banana', column3: true }, + { id: '4', column1: '12', column2: 'Daikon', column3: false }, + { id: '5', column1: '13', column2: 'Edamame', column3: true }, + { id: '6', column1: '20', column2: 'Fig', column3: false }, + { id: '7', column1: '21', column2: 'Grape', column3: true }, + ]; + + public dataForSimpleGrid: RowModel[] | undefined = [ + { id: '1', column1: '1', column2: 'Apple', column3: true }, + { id: '2', column1: '01', column2: 'Banana', column3: false }, + { id: '3', column1: '11', column2: 'Banana', column3: true }, + { id: '4', column1: '12', column2: 'Daikon', column3: false }, + { id: '5', column1: '13', column2: 'Edamame', column3: true }, + { id: '6', column1: '20', column2: 'Fig', column3: false }, + { id: '7', column1: '21', column2: 'Grape', column3: true }, + ]; + + public dataForSimpleGridWithMultiselect: RowModel[] | undefined = [ + { id: '1', column1: '1', column2: 'Apple', column3: true, myId: '101' }, + { id: '2', column1: '01', column2: 'Banana', column3: false, myId: '102' }, + { id: '3', column1: '11', column2: 'Banana', column3: true, myId: '103' }, + { id: '4', column1: '12', column2: 'Daikon', column3: false, myId: '104' }, + { id: '5', column1: '13', column2: 'Edamame', column3: true, myId: '105' }, + { id: '6', column1: '20', column2: 'Fig', column3: false, myId: '106' }, + { id: '7', column1: '21', column2: 'Grape', column3: true, myId: '107' }, + ]; + + public dataForFilteredGrid: FilteredRowModel[] = [ + { + id: '1', + column1: '1', + column2: 'Apple', + column3: true, + numericColumn: 100, + dateColumn: new Date('2024-01-15T00:00:00.000Z').toISOString(), + }, + { + id: '2', + column1: '01', + column2: 'Banana', + column3: false, + numericColumn: 200, + dateColumn: new Date('2024-02-20T00:00:00.000Z').toISOString(), + }, + { + id: '3', + column1: '11', + column2: 'Banana', + column3: true, + numericColumn: 150, + dateColumn: new Date('2024-03-10T00:00:00.000Z').toISOString(), + }, + { + id: '4', + column1: '12', + column2: 'Daikon', + column3: false, + numericColumn: 250, + dateColumn: new Date('2024-04-05T00:00:00.000Z').toISOString(), + }, + { + id: '5', + column1: '13', + column2: 'Edamame', + column3: true, + numericColumn: 175, + dateColumn: new Date('2024-05-25T00:00:00.000Z').toISOString(), + }, + { + id: '6', + column1: '20', + column2: 'Fig', + column3: false, + numericColumn: 300, + dateColumn: new Date('2024-06-30T00:00:00.000Z').toISOString(), + }, + { + id: '7', + column1: '21', + column2: 'Grape', + column3: true, + numericColumn: 125, + dateColumn: new Date('2024-07-12T00:00:00.000Z').toISOString(), + }, + ]; + + public readonly displayedColumnIds = input([]); + public readonly appliedFilters = input< + SkyFilterBarFilterItem[] + >([]); + public readonly removeRowIds = model([]); + public readonly rowHighlightedId = model(); + public readonly selectedRowIds = model([]); + public readonly showFilteredGrid = input(false); + protected readonly showFilteredMultiselectGrid = input(false); + public readonly externalRowCount = input(); + public readonly visibleColumnIds = model([]); + public readonly textFilterOperator = input(); + public readonly numberFilterOperator = input(); + public readonly booleanFilterOperator = input(); + + public readonly sortField = model(undefined); + + public readonly multiselect = input(); + public readonly height = input(); + public readonly pageSize = input(); + + public page = model(1); + public pageQueryParam = ''; + + protected readonly showAllColumns = input(true); + protected readonly showAllGrids = input(true); + protected readonly showCol3 = input(true); + protected readonly showCol3HeaderText = input(true); + + readonly #cdr = inject(ChangeDetectorRef); + + public selectAll(): void { + this.selectedRowIds.set( + (this.dataForSimpleGridWithMultiselect ?? []).map( + (item) => item.myId as string, + ), + ); + this.#cdr.markForCheck(); + } + + public clearAll(): void { + this.selectedRowIds.set([]); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public cancelRowDelete(_cancelArgs: SkyDataGridRowDeleteCancelArgs): void { + // noop + } + + public deleteItem(id: string): void { + this.removeRowIds.update((removeRowIds) => [id, ...removeRowIds]); + } + + public finishRowDelete(confirmArgs: SkyDataGridRowDeleteConfirmArgs): void { + this.dataForRowDeleteGrid = (this.dataForRowDeleteGrid ?? []).filter( + (data: RowModel) => data.id !== confirmArgs.id, + ); + } + + public selectRow(): void { + this.selectedRowIds.set(['101', '103', '105']); + } +} diff --git a/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-w-data-manager-test.component.html b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-w-data-manager-test.component.html new file mode 100644 index 0000000000..46d244cb90 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-w-data-manager-test.component.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-w-data-manager-test.component.ts b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-w-data-manager-test.component.ts new file mode 100644 index 0000000000..be77a8dde4 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-w-data-manager-test.component.ts @@ -0,0 +1,90 @@ +import { + ChangeDetectionStrategy, + Component, + OnInit, + inject, + model, +} from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { + SkyDataManagerModule, + SkyDataManagerService, + SkyDataManagerState, +} from '@skyux/data-manager'; + +import { SkyDataGridSort } from '../../types/data-grid-sort'; +import { SkyDataGridModule } from '../data-grid.module'; + +interface RowModel { + id: string; + name: string | null; + category: string | null; +} + +@Component({ + selector: 'app-data-grid-w-data-manager-test', + templateUrl: './data-grid-w-data-manager-test.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDataGridModule, SkyDataManagerModule], + providers: [SkyDataManagerService], +}) +export class DataGridWDataManagerTestComponent implements OnInit { + public data: RowModel[] = [ + { id: '1', name: 'Apple', category: 'Fruit' }, + { id: '2', name: 'Banana', category: 'Fruit' }, + { id: '3', name: 'Carrot', category: 'Vegetable' }, + { id: '4', name: null, category: null }, + ]; + public viewId = 'my-view'; + public readonly dataManagerService = inject(SkyDataManagerService); + + public displayedColumnIds: string[] | undefined = ['name', 'category']; + public searchEnabled = true; + public columnPickerEnabled = true; + + public readonly dataManagerState = toSignal( + this.dataManagerService.getDataStateUpdates( + 'DataGridWDataManagerTestComponent', + ), + { initialValue: new SkyDataManagerState({}) }, + ); + public sortField = model(); + + public ngOnInit(): void { + this.dataManagerService.initDataManager({ + activeViewId: this.viewId, + dataManagerConfig: { + sortOptions: [ + { + id: 'name', + propertyName: 'name', + label: 'Name', + descending: false, + }, + { + id: 'category', + propertyName: 'category', + label: 'Category', + descending: false, + }, + ], + }, + defaultDataState: new SkyDataManagerState({ + views: [ + { + viewId: this.viewId, + displayedColumnIds: this.displayedColumnIds, + }, + ], + }), + }); + + this.dataManagerService.initDataView({ + id: this.viewId, + name: 'Grid View', + iconName: 'table', + searchEnabled: this.searchEnabled, + columnPickerEnabled: this.columnPickerEnabled, + }); + } +} diff --git a/libs/components/data-grid/src/lib/modules/types/data-grid-filter-operator.ts b/libs/components/data-grid/src/lib/modules/types/data-grid-filter-operator.ts new file mode 100644 index 0000000000..b6e51e7208 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/types/data-grid-filter-operator.ts @@ -0,0 +1,17 @@ +/** + * Filter operators that can be used when applying filter-bar filters to AG Grid columns. + */ +export type SkyDataGridFilterOperator = + // Text operators + | 'contains' + | 'notContains' + | 'startsWith' + | 'endsWith' + // Number/Date operators + | 'lessThan' + | 'lessThanOrEqual' + | 'greaterThan' + | 'greaterThanOrEqual' + // Common operators + | 'equals' + | 'notEqual'; diff --git a/libs/components/data-grid/src/lib/modules/types/data-grid-filter-value.ts b/libs/components/data-grid/src/lib/modules/types/data-grid-filter-value.ts new file mode 100644 index 0000000000..eab086c082 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/types/data-grid-filter-value.ts @@ -0,0 +1,12 @@ +import { SkyDateRange } from '@skyux/datetime'; + +import { SkyDataGridNumberRangeFilterValue } from './data-grid-number-range-filter-value'; + +export type SkyDataGridFilterValue = + | SkyDataGridNumberRangeFilterValue + | SkyDateRange + | boolean + | Date + | number + | string + | string[]; diff --git a/libs/components/data-grid/src/lib/modules/types/data-grid-number-range-filter-value.ts b/libs/components/data-grid/src/lib/modules/types/data-grid-number-range-filter-value.ts new file mode 100644 index 0000000000..b9e5145565 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/types/data-grid-number-range-filter-value.ts @@ -0,0 +1,41 @@ +import { FormControl, FormGroup } from '@angular/forms'; + +/** + * Filter value for number range filters. + */ +export type SkyDataGridNumberRangeFilterValue = + | { + /** + * The minimum value of the range. + */ + from: number; + /** + * The maximum value of the range. + */ + to: number; + } + | { + /** + * The minimum value of the range. Null indicates no minimum. + */ + from: null | undefined; + /** + * The maximum value of the range. + */ + to: number; + } + | { + /** + * The minimum value of the range. + */ + from: number; + /** + * The maximum value of the range. Null indicates no maximum. + */ + to: null | undefined; + }; + +export type SkyDataGridNumberRangeFilterFormGroup = FormGroup<{ + from: FormControl; + to: FormControl; +}>; diff --git a/libs/components/data-grid/src/lib/modules/types/data-grid-page-request.ts b/libs/components/data-grid/src/lib/modules/types/data-grid-page-request.ts new file mode 100644 index 0000000000..a6f5f6699a --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/types/data-grid-page-request.ts @@ -0,0 +1,18 @@ +import { SkyDataGridSort } from './data-grid-sort'; + +export interface SkyDataGridPageRequest { + /** + * The current page number (1-based). + */ + pageNumber: number; + + /** + * The number of items per page. When `undefined`, paging is not applied and all items are returned. + */ + pageSize: number | undefined; + + /** + * The data property and direction for sorting. When `undefined`, no sorting is applied. + */ + sortField: SkyDataGridSort | undefined; +} diff --git a/libs/components/data-grid/src/lib/modules/types/data-grid-row-delete-cancel-args.ts b/libs/components/data-grid/src/lib/modules/types/data-grid-row-delete-cancel-args.ts new file mode 100644 index 0000000000..f90ea64e76 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/types/data-grid-row-delete-cancel-args.ts @@ -0,0 +1,6 @@ +import { SkyAgGridRowDeleteCancelArgs } from '@skyux/ag-grid'; + +/** + * Information regarding a row whose deletion has been cancelled. + */ +export type SkyDataGridRowDeleteCancelArgs = SkyAgGridRowDeleteCancelArgs; diff --git a/libs/components/data-grid/src/lib/modules/types/data-grid-row-delete-confirm-args.ts b/libs/components/data-grid/src/lib/modules/types/data-grid-row-delete-confirm-args.ts new file mode 100644 index 0000000000..f62a160a5d --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/types/data-grid-row-delete-confirm-args.ts @@ -0,0 +1,6 @@ +import { SkyAgGridRowDeleteConfirmArgs } from '@skyux/ag-grid'; + +/** + * Information regarding a row whose deletion has been confirmed. + */ +export type SkyDataGridRowDeleteConfirmArgs = SkyAgGridRowDeleteConfirmArgs; diff --git a/libs/components/data-grid/src/lib/modules/types/data-grid-sort.ts b/libs/components/data-grid/src/lib/modules/types/data-grid-sort.ts new file mode 100644 index 0000000000..cc82d62454 --- /dev/null +++ b/libs/components/data-grid/src/lib/modules/types/data-grid-sort.ts @@ -0,0 +1,15 @@ +/** + * Used for applying sort to a `SkyDataGridComponent` as well as receiving updates when the data grid sorting changes. + */ +export interface SkyDataGridSort { + /** + * Whether to apply the sort in descending order. + * @required + */ + descending?: boolean; + /** + * The data property to sort by. + * @required + */ + fieldSelector: string; +} diff --git a/libs/components/data-grid/testing/eslint.config.js b/libs/components/data-grid/testing/eslint.config.js new file mode 100644 index 0000000000..b4c331fb6a --- /dev/null +++ b/libs/components/data-grid/testing/eslint.config.js @@ -0,0 +1,5 @@ +const prettier = require('eslint-config-prettier'); +const baseConfig = require('../../../../eslint-base.config'); +const overrides = require('../../../../eslint-overrides.config'); + +module.exports = [...baseConfig, ...overrides, prettier]; diff --git a/libs/components/data-grid/testing/karma.conf.js b/libs/components/data-grid/testing/karma.conf.js new file mode 100644 index 0000000000..c3ee28e599 --- /dev/null +++ b/libs/components/data-grid/testing/karma.conf.js @@ -0,0 +1,19 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +const { join } = require('path'); +const getBaseKarmaConfig = require('../../../../karma.conf'); + +module.exports = function (config) { + const baseConfig = getBaseKarmaConfig(); + config.set({ + ...baseConfig, + coverageReporter: { + ...baseConfig.coverageReporter, + dir: join( + __dirname, + '../../../../coverage/libs/components/data-grid/testing', + ), + }, + }); +}; diff --git a/libs/components/data-grid/testing/ng-package.json b/libs/components/data-grid/testing/ng-package.json new file mode 100644 index 0000000000..fbafcc4448 --- /dev/null +++ b/libs/components/data-grid/testing/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/libs/components/data-grid/testing/project.json b/libs/components/data-grid/testing/project.json new file mode 100644 index 0000000000..ff334a4d1f --- /dev/null +++ b/libs/components/data-grid/testing/project.json @@ -0,0 +1,47 @@ +{ + "name": "data-grid-testing", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "libs/components/data-grid/testing/src", + "prefix": "sky", + "tags": ["testing"], + "targets": { + "test": { + "executor": "@angular-devkit/build-angular:karma", + "options": { + "tsConfig": "libs/components/data-grid/testing/tsconfig.spec.json", + "karmaConfig": "libs/components/data-grid/testing/karma.conf.js", + "codeCoverage": true, + "codeCoverageExclude": ["**/fixtures/**"], + "styles": [ + "libs/components/theme/src/lib/styles/sky.scss", + "libs/components/theme/src/lib/styles/themes/modern/styles.scss" + ], + "polyfills": [ + "zone.js", + "zone.js/testing", + "libs/components/packages/src/polyfills.js" + ], + "inlineStyleLanguage": "scss", + "stylePreprocessorOptions": { + "includePaths": ["{workspaceRoot}"] + } + }, + "configurations": { + "ci": { + "browsers": "ChromeHeadlessNoSandbox", + "codeCoverage": true, + "progress": false, + "sourceMap": true, + "watch": false + } + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "options": { + "lintFilePatterns": ["{projectRoot}/src/**/*.ts"] + } + } + } +} diff --git a/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.filters.ts b/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.filters.ts new file mode 100644 index 0000000000..695d6006c8 --- /dev/null +++ b/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.filters.ts @@ -0,0 +1,8 @@ +import { SkyHarnessFilters } from '@skyux/core/testing'; + +/** + * A set of criteria that can be used to filter a list of `SkyDataGridHarness` instances. + * @preview + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type +export interface SkyDataGridHarnessFilters extends SkyHarnessFilters {} diff --git a/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.spec.ts b/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.spec.ts new file mode 100644 index 0000000000..c59b70f018 --- /dev/null +++ b/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.spec.ts @@ -0,0 +1,99 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SkyDataGridHarness } from './data-grid-harness'; +import { DataGridTestComponent } from './fixtures/data-grid-test.component'; + +describe('data-grid-harness', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({}); + fixture = TestBed.createComponent(DataGridTestComponent); + }); + + it('should check if the grid is ready', async () => { + fixture.componentRef.setInput('showAllColumns', false); + fixture.detectChanges(); + + const harness = await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyDataGridHarness.with({ dataSkyId: 'grid' }), + ); + await expectAsync(harness.isGridReady()).toBeResolvedTo(false); + + fixture.componentRef.setInput('showAllColumns', true); + fixture.detectChanges(); + + await expectAsync(harness.isGridReady()).toBeResolvedTo(true); + }); + + it('should get columns', async () => { + fixture.detectChanges(); + + const harness = await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyDataGridHarness.with({ dataSkyId: 'grid' }), + ); + await expectAsync(harness.isGridReady()).toBeResolvedTo(true); + await expectAsync(harness.getDisplayedColumnIds()).toBeResolvedTo([ + 'column1', + 'column2', + 'column3', + ]); + await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeResolvedTo([ + 'Column1', + 'Column2', + 'Column3', + ]); + + fixture.componentRef.setInput('showCol3', false); + fixture.detectChanges(); + + await expectAsync(harness.getDisplayedColumnIds()).toBeResolvedTo([ + 'column1', + 'column2', + ]); + await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeResolvedTo([ + 'Column1', + 'Column2', + ]); + }); + + it('should throw error if the grid is not available', async () => { + fixture.componentRef.setInput('showAllColumns', false); + fixture.detectChanges(); + + const harness = await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyDataGridHarness.with({ dataSkyId: 'grid' }), + ); + await expectAsync(harness.isGridReady()).toBeResolvedTo(false); + await expectAsync(harness.getDisplayedColumnIds()).toBeRejectedWith( + 'Unable to retrieve displayed column IDs.', + ); + await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeRejectedWith( + 'Unable to retrieve displayed column header names.', + ); + + fixture.componentRef.setInput('showAllColumns', true); + fixture.detectChanges(); + + await expectAsync(harness.getDisplayedColumnIds()).toBeResolvedTo([ + 'column1', + 'column2', + 'column3', + ]); + await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeResolvedTo([ + 'Column1', + 'Column2', + 'Column3', + ]); + + fixture.componentRef.setInput('showCol3HeaderText', false); + fixture.detectChanges(); + + await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeResolvedTo([ + 'Column1', + 'Column2', + '', + ]); + }); +}); diff --git a/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.ts b/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.ts new file mode 100644 index 0000000000..a28be985e6 --- /dev/null +++ b/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.ts @@ -0,0 +1,59 @@ +import { HarnessPredicate } from '@angular/cdk/testing'; +import { SkyAgGridWrapperHarness } from '@skyux/ag-grid/testing'; +import { SkyQueryableComponentHarness } from '@skyux/core/testing'; + +import { SkyDataGridHarnessFilters } from './data-grid-harness.filters'; + +/** + * Harness for interacting with SKY UX data grid components in tests. + * @preview + */ +export class SkyDataGridHarness extends SkyQueryableComponentHarness { + /** + * @internal + */ + public static hostSelector = 'sky-data-grid'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a + * `SkyDataGridHarness` that meets certain criteria + */ + public static with( + filters: SkyDataGridHarnessFilters, + ): HarnessPredicate { + return SkyDataGridHarness.getDataSkyIdPredicate(filters); + } + + /** + * Checks whether the grid is ready. + */ + public async isGridReady(): Promise { + return await this.#getGridWrapper() + .then(async (grid) => await grid.isGridReady()) + .catch(() => false); + } + + /** + * Retrieves the IDs of the currently displayed columns. + */ + public async getDisplayedColumnIds(): Promise { + return await this.#getGridWrapper() + .then(async (grid) => await grid.getDisplayedColumnIds()) + .catch(() => Promise.reject('Unable to retrieve displayed column IDs.')); + } + + /** + * Retrieves the header names of the currently displayed columns. + */ + public async getDisplayedColumnHeaderNames(): Promise { + return await this.#getGridWrapper() + .then(async (grid) => await grid.getDisplayedColumnHeaderNames()) + .catch(() => + Promise.reject('Unable to retrieve displayed column header names.'), + ); + } + + async #getGridWrapper(): Promise { + return await this.queryHarness(SkyAgGridWrapperHarness); + } +} diff --git a/libs/components/data-grid/testing/src/modules/data-grid/fixtures/data-grid-test.component.html b/libs/components/data-grid/testing/src/modules/data-grid/fixtures/data-grid-test.component.html new file mode 100644 index 0000000000..8a466bffe9 --- /dev/null +++ b/libs/components/data-grid/testing/src/modules/data-grid/fixtures/data-grid-test.component.html @@ -0,0 +1,160 @@ +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + @if (showCol3()) { + + } + } + +} +

Grid with multiselect enabled

+ + + + + + + +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + + @if (showCol3()) { + + } + } + +} +

Selected rows: {{ selectedRowIds() | json }}

+ +

Grid with row delete

+ +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + + @if (row.id) { + + + + + + + + } + + + + + @if (showCol3()) { + + } @else { + + } + } + +} + + This is a numeric column. Click here to learn more. + + +

Grid w/ aligned columns and inline help

+ +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + @if (showCol3()) { + + } + } + +} diff --git a/libs/components/data-grid/testing/src/modules/data-grid/fixtures/data-grid-test.component.ts b/libs/components/data-grid/testing/src/modules/data-grid/fixtures/data-grid-test.component.ts new file mode 100644 index 0000000000..2a92e0abc2 --- /dev/null +++ b/libs/components/data-grid/testing/src/modules/data-grid/fixtures/data-grid-test.component.ts @@ -0,0 +1,115 @@ +import { JsonPipe } from '@angular/common'; +import { + ChangeDetectorRef, + Component, + inject, + input, + model, +} from '@angular/core'; +import { + SkyDataGridColumnComponent, + SkyDataGridComponent, + SkyDataGridRowDeleteCancelArgs, + SkyDataGridRowDeleteConfirmArgs, +} from '@skyux/data-grid'; +import { SkyDropdownModule, SkyPopoverModule } from '@skyux/popovers'; + +interface RowModel { + id: string; + column1: string; + column2: string; + column3: boolean; + myId?: string; +} + +@Component({ + selector: 'app-data-grid-test', + templateUrl: './data-grid-test.component.html', + imports: [ + SkyDataGridComponent, + SkyDataGridColumnComponent, + SkyPopoverModule, + SkyDropdownModule, + JsonPipe, + ], +}) +export class DataGridTestComponent { + public dataForRowDeleteGrid: RowModel[] | undefined = [ + { id: '1', column1: '1', column2: 'Apple', column3: true }, + { id: '2', column1: '01', column2: 'Banana', column3: false }, + { id: '3', column1: '11', column2: 'Banana', column3: true }, + { id: '4', column1: '12', column2: 'Daikon', column3: false }, + { id: '5', column1: '13', column2: 'Edamame', column3: true }, + { id: '6', column1: '20', column2: 'Fig', column3: false }, + { id: '7', column1: '21', column2: 'Grape', column3: true }, + ]; + + public dataForSimpleGrid: RowModel[] | undefined = [ + { id: '1', column1: '1', column2: 'Apple', column3: true }, + { id: '2', column1: '01', column2: 'Banana', column3: false }, + { id: '3', column1: '11', column2: 'Banana', column3: true }, + { id: '4', column1: '12', column2: 'Daikon', column3: false }, + { id: '5', column1: '13', column2: 'Edamame', column3: true }, + { id: '6', column1: '20', column2: 'Fig', column3: false }, + { id: '7', column1: '21', column2: 'Grape', column3: true }, + ]; + + public dataForSimpleGridWithMultiselect: RowModel[] | undefined = [ + { id: '1', column1: '1', column2: 'Apple', column3: true, myId: '101' }, + { id: '2', column1: '01', column2: 'Banana', column3: false, myId: '102' }, + { id: '3', column1: '11', column2: 'Banana', column3: true, myId: '103' }, + { id: '4', column1: '12', column2: 'Daikon', column3: false, myId: '104' }, + { id: '5', column1: '13', column2: 'Edamame', column3: true, myId: '105' }, + { id: '6', column1: '20', column2: 'Fig', column3: false, myId: '106' }, + { id: '7', column1: '21', column2: 'Grape', column3: true, myId: '107' }, + ]; + + public readonly displayedColumns = input([]); + public readonly removeRowIds = model([]); + public readonly rowHighlightedId = model(); + public readonly selectedRowIds = model([]); + public readonly visibleColumnIds = model([]); + + public page = 1; + public pageSize = 0; + public pageQueryParam = ''; + + protected readonly showAllColumns = input(true); + protected readonly showAllGrids = input(true); + protected readonly showCol3 = input(true); + protected readonly showCol3HeaderText = input(true); + + readonly #cdr = inject(ChangeDetectorRef); + + public selectAll(): void { + this.selectedRowIds.set( + (this.dataForSimpleGridWithMultiselect ?? []).map( + (item) => item.myId as string, + ), + ); + this.#cdr.markForCheck(); + } + + public clearAll(): void { + this.selectedRowIds.set([]); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public cancelRowDelete(_cancelArgs: SkyDataGridRowDeleteCancelArgs): void { + // noop + } + + public deleteItem(id: string): void { + this.removeRowIds.update((removeRowIds) => [id, ...removeRowIds]); + } + + public finishRowDelete(confirmArgs: SkyDataGridRowDeleteConfirmArgs): void { + this.dataForRowDeleteGrid = (this.dataForRowDeleteGrid ?? []).filter( + (data: RowModel) => data.id !== confirmArgs.id, + ); + } + + public selectRow(): void { + this.selectedRowIds.set(['101', '103', '105']); + } +} diff --git a/libs/components/data-grid/testing/src/public-api.ts b/libs/components/data-grid/testing/src/public-api.ts new file mode 100644 index 0000000000..9e0c6d26be --- /dev/null +++ b/libs/components/data-grid/testing/src/public-api.ts @@ -0,0 +1,2 @@ +export { SkyDataGridHarness } from './modules/data-grid/data-grid-harness'; +export { SkyDataGridHarnessFilters } from './modules/data-grid/data-grid-harness.filters'; diff --git a/libs/components/data-grid/testing/tsconfig.spec.json b/libs/components/data-grid/testing/tsconfig.spec.json new file mode 100644 index 0000000000..aad2a9f430 --- /dev/null +++ b/libs/components/data-grid/testing/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.spec.json", + "compilerOptions": { + "outDir": "../../../../out-tsc/spec", + "types": ["jasmine", "node"] + }, + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/components/data-grid/tsconfig.json b/libs/components/data-grid/tsconfig.json new file mode 100644 index 0000000000..c63981277b --- /dev/null +++ b/libs/components/data-grid/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.lib.prod.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/components/data-grid/tsconfig.lib.json b/libs/components/data-grid/tsconfig.lib.json new file mode 100644 index 0000000000..4952dcda5f --- /dev/null +++ b/libs/components/data-grid/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true + }, + "exclude": ["src/test.ts", "**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/libs/components/data-grid/tsconfig.lib.prod.json b/libs/components/data-grid/tsconfig.lib.prod.json new file mode 100644 index 0000000000..2a2faa884c --- /dev/null +++ b/libs/components/data-grid/tsconfig.lib.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/libs/components/data-grid/tsconfig.spec.json b/libs/components/data-grid/tsconfig.spec.json new file mode 100644 index 0000000000..bb6f4b9ec9 --- /dev/null +++ b/libs/components/data-grid/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["jasmine", "node"] + }, + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/components/data-manager/src/index.ts b/libs/components/data-manager/src/index.ts index cba2a981a1..92f53159c5 100644 --- a/libs/components/data-manager/src/index.ts +++ b/libs/components/data-manager/src/index.ts @@ -22,6 +22,7 @@ export { SkyDataManagerDockType } from './lib/modules/data-manager/types/data-ma // Obscure names are used to indicate types are not part of public API. export { SkyDataManagerColumnPickerComponent as λ1 } from './lib/modules/data-manager/data-manager-column-picker/data-manager-column-picker.component'; export { SkyDataManagerFilterControllerDirective as λ9 } from './lib/modules/data-manager/data-manager-filters/data-manager-filter-controller.directive'; +export { SkyDataManagerHostControllerDirective as λ10 } from './lib/modules/data-manager/data-manager-host/data-manager-host-controller.directive'; export { SkyDataManagerToolbarLeftItemComponent as λ3 } from './lib/modules/data-manager/data-manager-toolbar/data-manager-toolbar-left-item.component'; export { SkyDataManagerToolbarPrimaryItemComponent as λ4 } from './lib/modules/data-manager/data-manager-toolbar/data-manager-toolbar-primary-item.component'; export { SkyDataManagerToolbarRightItemComponent as λ5 } from './lib/modules/data-manager/data-manager-toolbar/data-manager-toolbar-right-item.component'; diff --git a/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host-controller.directive.spec.ts b/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host-controller.directive.spec.ts new file mode 100644 index 0000000000..354c3850ab --- /dev/null +++ b/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host-controller.directive.spec.ts @@ -0,0 +1,703 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { SkyDataHost } from '@skyux/lists'; + +import { SkyDataManagerService } from '../data-manager.service'; +import { SkyDataManagerState } from '../models/data-manager-state'; + +import { SkyDataManagerHostControllerDirective } from './data-manager-host-controller.directive'; +import { SkyDataManagerHostService } from './data-manager-host.service'; + +@Component({ + template: `
`, + imports: [SkyDataManagerHostControllerDirective], +}) +class TestHostComponent { + public viewId = 'test-view'; +} + +describe('SkyDataManagerHostControllerDirective', () => { + let fixture: ComponentFixture; + let component: TestHostComponent; + let dataManagerService: SkyDataManagerService; + let adapterService: SkyDataManagerHostService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestHostComponent], + providers: [SkyDataManagerService], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + component = fixture.componentInstance; + dataManagerService = TestBed.inject(SkyDataManagerService); + fixture.detectChanges(); + + const directiveDebugEl = fixture.debugElement.query( + By.directive(SkyDataManagerHostControllerDirective), + ); + adapterService = directiveDebugEl.injector.get(SkyDataManagerHostService); + }); + + it('should create and provide the adapter service', () => { + expect(component).toBeTruthy(); + expect(adapterService).toBeTruthy(); + expect(adapterService).toBeInstanceOf(SkyDataManagerHostService); + }); + + it('should update the adapter when the data manager state includes the view', async () => { + const spy = spyOn(adapterService, 'updateDataHost').and.callThrough(); + + const newState = new SkyDataManagerState({ + activeSortOption: undefined, + searchText: 'search-text', + selectedIds: ['1', '2'], + views: [ + { + viewId: component.viewId, + displayedColumnIds: ['col-1'], + additionalData: { + page: 3, + }, + }, + ], + }); + + const sourceId = 'host-controller-test'; + dataManagerService.updateDataState(newState, sourceId); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy).toHaveBeenCalled(); + const [hostState, id] = spy.calls.mostRecent().args; + expect(id).toBe('skyDataManagerHostController'); + expect(hostState).toEqual( + jasmine.objectContaining({ + id: component.viewId, + searchText: 'search-text', + selectedIds: ['1', '2'], + page: 3, + displayedColumnIds: ['col-1'], + activeSortOption: undefined, + }), + ); + }); + + it('should propagate adapter changes back into the data manager state', async () => { + dataManagerService.updateDataManagerConfig({ + sortOptions: [ + { + id: 'name-sort', + label: 'Name (A-Z)', + propertyName: 'name', + descending: false, + }, + { + id: 'name-sort-za', + label: 'Name (Z-A)', + propertyName: 'name', + descending: true, + }, + ], + }); + + const initialState = new SkyDataManagerState({ + activeSortOption: { + id: 'name-sort-za', + label: 'Name', + propertyName: 'name', + descending: true, + }, + searchText: 'initial-search', + views: [ + { + viewId: component.viewId, + }, + ], + }); + + dataManagerService.updateDataState(initialState, 'initial'); + fixture.detectChanges(); + await fixture.whenStable(); + + const spy = spyOn(dataManagerService, 'updateDataState').and.callThrough(); + + const hostState: SkyDataHost = { + id: component.viewId, + displayedColumnIds: ['updated-1', 'updated-2'], + page: 4, + selectedIds: [], + searchText: undefined, + activeSortOption: { propertyName: 'name', descending: false }, + }; + + adapterService.updateDataHost(hostState, 'host-list'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy.calls.count()).toBeGreaterThan(0); + const [updatedState, source] = spy.calls.mostRecent().args; + expect(source).toBe('skyDataManagerHostController--test-view'); + expect(updatedState.searchText).toBeUndefined(); + expect(updatedState.activeSortOption?.propertyName).toBe('name'); + expect(updatedState.activeSortOption?.descending).toBeFalse(); + + const viewState = updatedState.getViewStateById(component.viewId); + expect(viewState?.displayedColumnIds).toEqual(['updated-1', 'updated-2']); + expect(viewState?.additionalData?.page).toBe(4); + }); + + describe('distinct changes for data host updates', () => { + it('should not update adapter when data manager state has no changes', async () => { + const initialState = new SkyDataManagerState({ + searchText: 'search-text', + selectedIds: ['1', '2'], + views: [ + { + viewId: component.viewId, + displayedColumnIds: ['col-1'], + additionalData: { + page: 1, + }, + }, + ], + }); + + dataManagerService.updateDataState(initialState, 'initial'); + fixture.detectChanges(); + await fixture.whenStable(); + + // Wait for the initial sync to the adapter to complete + // The directive will have updated the adapter once with initial values + const spy = spyOn(adapterService, 'updateDataHost').and.callThrough(); + + // Update with identical state values + const identicalState = new SkyDataManagerState({ + searchText: 'search-text', + selectedIds: ['1', '2'], + views: [ + { + viewId: component.viewId, + displayedColumnIds: ['col-1'], + additionalData: { + page: 1, + }, + }, + ], + }); + + dataManagerService.updateDataState(identicalState, 'duplicate-update'); + fixture.detectChanges(); + await fixture.whenStable(); + + // The directive should not update the adapter since the computed host state is identical + expect(spy).not.toHaveBeenCalled(); + }); + + it('should update adapter when searchText changes', async () => { + const initialState = new SkyDataManagerState({ + searchText: 'initial-search', + views: [{ viewId: component.viewId }], + }); + + dataManagerService.updateDataState(initialState, 'initial'); + fixture.detectChanges(); + await fixture.whenStable(); + + const spy = spyOn(adapterService, 'updateDataHost').and.callThrough(); + + const updatedState = new SkyDataManagerState({ + searchText: 'updated-search', + views: [{ viewId: component.viewId }], + }); + + dataManagerService.updateDataState(updatedState, 'search-update'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy).toHaveBeenCalled(); + const [hostState] = spy.calls.mostRecent().args; + expect(hostState.searchText).toBe('updated-search'); + }); + + it('should update adapter when page changes', async () => { + const initialState = new SkyDataManagerState({ + views: [ + { + viewId: component.viewId, + additionalData: { page: 1 }, + }, + ], + }); + + dataManagerService.updateDataState(initialState, 'initial'); + fixture.detectChanges(); + await fixture.whenStable(); + + const spy = spyOn(adapterService, 'updateDataHost').and.callThrough(); + + const updatedState = new SkyDataManagerState({ + views: [ + { + viewId: component.viewId, + additionalData: { page: 2 }, + }, + ], + }); + + dataManagerService.updateDataState(updatedState, 'page-update'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy).toHaveBeenCalled(); + const [hostState] = spy.calls.mostRecent().args; + expect(hostState.page).toBe(2); + }); + + it('should update adapter when displayedColumnIds changes', async () => { + const initialState = new SkyDataManagerState({ + views: [ + { + viewId: component.viewId, + displayedColumnIds: ['col-1', 'col-2'], + }, + ], + }); + + dataManagerService.updateDataState(initialState, 'initial'); + fixture.detectChanges(); + await fixture.whenStable(); + + const spy = spyOn(adapterService, 'updateDataHost').and.callThrough(); + + const updatedState = new SkyDataManagerState({ + views: [ + { + viewId: component.viewId, + displayedColumnIds: ['col-1', 'col-3'], + }, + ], + }); + + dataManagerService.updateDataState(updatedState, 'columns-update'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy).toHaveBeenCalled(); + const [hostState] = spy.calls.mostRecent().args; + expect(hostState.displayedColumnIds).toEqual(['col-1', 'col-3']); + }); + + it('should update adapter when displayedColumnIds length changes', async () => { + const initialState = new SkyDataManagerState({ + views: [ + { + viewId: component.viewId, + displayedColumnIds: ['col-1', 'col-2'], + }, + ], + }); + + dataManagerService.updateDataState(initialState, 'initial'); + fixture.detectChanges(); + await fixture.whenStable(); + + const spy = spyOn(adapterService, 'updateDataHost').and.callThrough(); + + const updatedState = new SkyDataManagerState({ + views: [ + { + viewId: component.viewId, + displayedColumnIds: ['col-1'], + }, + ], + }); + + dataManagerService.updateDataState(updatedState, 'columns-length-update'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy).toHaveBeenCalled(); + const [hostState] = spy.calls.mostRecent().args; + expect(hostState.displayedColumnIds).toEqual(['col-1']); + }); + + it('should update adapter when selectedIds changes', async () => { + const initialState = new SkyDataManagerState({ + selectedIds: ['1', '2'], + views: [{ viewId: component.viewId }], + }); + + dataManagerService.updateDataState(initialState, 'initial'); + fixture.detectChanges(); + await fixture.whenStable(); + + const spy = spyOn(adapterService, 'updateDataHost').and.callThrough(); + + const updatedState = new SkyDataManagerState({ + selectedIds: ['1', '3'], + views: [{ viewId: component.viewId }], + }); + + dataManagerService.updateDataState(updatedState, 'selected-update'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy).toHaveBeenCalled(); + const [hostState] = spy.calls.mostRecent().args; + expect(hostState.selectedIds).toEqual(['1', '3']); + }); + + it('should update adapter when selectedIds length changes', async () => { + const initialState = new SkyDataManagerState({ + selectedIds: ['1', '2'], + views: [{ viewId: component.viewId }], + }); + + dataManagerService.updateDataState(initialState, 'initial'); + fixture.detectChanges(); + await fixture.whenStable(); + + const spy = spyOn(adapterService, 'updateDataHost').and.callThrough(); + + const updatedState = new SkyDataManagerState({ + selectedIds: ['1'], + views: [{ viewId: component.viewId }], + }); + + dataManagerService.updateDataState( + updatedState, + 'selected-length-update', + ); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy).toHaveBeenCalled(); + const [hostState] = spy.calls.mostRecent().args; + expect(hostState.selectedIds).toEqual(['1']); + }); + + it('should update adapter when activeSortOption propertyName changes', async () => { + const initialState = new SkyDataManagerState({ + activeSortOption: { + id: '1', + label: 'Name', + propertyName: 'name', + descending: false, + }, + views: [{ viewId: component.viewId }], + }); + + dataManagerService.updateDataState(initialState, 'initial'); + fixture.detectChanges(); + await fixture.whenStable(); + + const spy = spyOn(adapterService, 'updateDataHost').and.callThrough(); + + const updatedState = new SkyDataManagerState({ + activeSortOption: { + id: '2', + label: 'Date', + propertyName: 'date', + descending: false, + }, + views: [{ viewId: component.viewId }], + }); + + dataManagerService.updateDataState(updatedState, 'sort-property-update'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy).toHaveBeenCalled(); + const [hostState] = spy.calls.mostRecent().args; + expect(hostState.activeSortOption?.propertyName).toBe('date'); + }); + + it('should update adapter when activeSortOption descending changes', async () => { + const initialState = new SkyDataManagerState({ + activeSortOption: { + id: '1', + label: 'Name', + propertyName: 'name', + descending: false, + }, + views: [{ viewId: component.viewId }], + }); + + dataManagerService.updateDataState(initialState, 'initial'); + fixture.detectChanges(); + await fixture.whenStable(); + + const spy = spyOn(adapterService, 'updateDataHost').and.callThrough(); + + const updatedState = new SkyDataManagerState({ + activeSortOption: { + id: '1', + label: 'Name', + propertyName: 'name', + descending: true, + }, + views: [{ viewId: component.viewId }], + }); + + dataManagerService.updateDataState( + updatedState, + 'sort-descending-update', + ); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy).toHaveBeenCalled(); + const [hostState] = spy.calls.mostRecent().args; + expect(hostState.activeSortOption?.descending).toBeTrue(); + }); + }); + + describe('distinct changes for data manager state updates', () => { + async function setupInitialState(): Promise { + dataManagerService.updateDataManagerConfig({ + sortOptions: [ + { + id: 'name-sort', + label: 'Name', + propertyName: 'name', + descending: false, + }, + { + id: 'name-sort', + label: 'Name', + propertyName: 'name', + descending: true, + }, + { + id: 'date-sort', + label: 'Date', + propertyName: 'date', + descending: false, + }, + { + id: 'date-sort', + label: 'Date', + propertyName: 'date', + descending: true, + }, + ], + }); + + const initialState = new SkyDataManagerState({ + activeSortOption: { + id: 'name-sort', + label: 'Name', + propertyName: 'name', + descending: false, + }, + searchText: 'initial-search', + views: [ + { + viewId: component.viewId, + displayedColumnIds: ['col-1', 'col-2'], + additionalData: { page: 1 }, + }, + ], + }); + + dataManagerService.updateDataState(initialState, 'initial'); + fixture.detectChanges(); + await fixture.whenStable(); + } + + it('should not echo data manager state changes back to data manager', async () => { + await setupInitialState(); + + const spy = spyOn( + dataManagerService, + 'updateDataState', + ).and.callThrough(); + + // When data manager state changes, the directive converts it and sends to adapter. + // Verify that this converted state doesn't echo back to data manager. + const updatedState = new SkyDataManagerState({ + activeSortOption: { + id: 'name-sort', + label: 'Name', + propertyName: 'name', + descending: false, + }, + searchText: 'initial-search', + views: [ + { + viewId: component.viewId, + displayedColumnIds: ['col-1', 'col-2'], + additionalData: { page: 2 }, + }, + ], + }); + + dataManagerService.updateDataState(updatedState, 'external-source'); + fixture.detectChanges(); + await fixture.whenStable(); + + // The directive should have updated the adapter, but that adapter update + // should not cause an echo back to data manager since the values are equivalent + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), 'external-source'); + }); + + it('should not echo adapter state changes back to adapter', async () => { + await setupInitialState(); + + const adapterSpy = spyOn( + adapterService, + 'updateDataHost', + ).and.callThrough(); + + // Update adapter with new displayedColumnIds + const updatedHostState: SkyDataHost = { + id: component.viewId, + displayedColumnIds: ['col-1', 'col-3'], + page: 1, + searchText: 'initial-search', + activeSortOption: { propertyName: 'name', descending: false }, + selectedIds: undefined, + }; + + adapterService.updateDataHost(updatedHostState, 'external-source'); + fixture.detectChanges(); + await fixture.whenStable(); + + // The adapter was called once directly, but the update to data manager + // should not cause the directive to echo back to adapter + expect(adapterSpy).toHaveBeenCalledTimes(1); + expect(adapterSpy).toHaveBeenCalledWith( + updatedHostState, + 'external-source', + ); + }); + + it('should update data manager when displayedColumnIds changes from adapter', async () => { + await setupInitialState(); + + const spy = spyOn( + dataManagerService, + 'updateDataState', + ).and.callThrough(); + + const updatedHostState: SkyDataHost = { + id: component.viewId, + displayedColumnIds: ['col-1', 'col-3'], + page: 1, + searchText: 'initial-search', + activeSortOption: { propertyName: 'name', descending: false }, + selectedIds: ['1', '2', '3'], + }; + + adapterService.updateDataHost(updatedHostState, 'external-source'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy).toHaveBeenCalled(); + const [updatedState] = spy.calls.mostRecent().args; + expect(updatedState?.selectedIds).toEqual(['1', '2', '3']); + const viewState = updatedState.getViewStateById(component.viewId); + expect(viewState?.displayedColumnIds).toEqual(['col-1', 'col-3']); + + const updatedHostState2: SkyDataHost = { + id: component.viewId, + displayedColumnIds: ['col-1', 'col-3'], + page: 1, + searchText: 'initial-search', + activeSortOption: { propertyName: 'name', descending: false }, + selectedIds: ['3', '4', '5'], + }; + + adapterService.updateDataHost(updatedHostState2, 'external-source'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy).toHaveBeenCalled(); + const [updatedState2] = spy.calls.mostRecent().args; + expect(updatedState2?.selectedIds).toEqual(['3', '4', '5']); + }); + + it('should update data manager when page changes from adapter', async () => { + await setupInitialState(); + + const spy = spyOn( + dataManagerService, + 'updateDataState', + ).and.callThrough(); + + const updatedHostState: SkyDataHost = { + id: component.viewId, + displayedColumnIds: ['col-1', 'col-2'], + page: 5, + searchText: 'initial-search', + activeSortOption: { propertyName: 'name', descending: false }, + selectedIds: undefined, + }; + + adapterService.updateDataHost(updatedHostState, 'external-source'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy).toHaveBeenCalled(); + const [updatedState] = spy.calls.mostRecent().args; + const viewState = updatedState.getViewStateById(component.viewId); + expect(viewState?.additionalData?.page).toBe(5); + }); + + it('should update data manager when activeSortOption propertyName changes from adapter', async () => { + await setupInitialState(); + + const spy = spyOn( + dataManagerService, + 'updateDataState', + ).and.callThrough(); + + const updatedHostState: SkyDataHost = { + id: component.viewId, + displayedColumnIds: ['col-1', 'col-2'], + page: 1, + searchText: 'initial-search', + activeSortOption: { propertyName: 'date', descending: false }, + selectedIds: undefined, + }; + + adapterService.updateDataHost(updatedHostState, 'external-source'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy).toHaveBeenCalled(); + const [updatedState] = spy.calls.mostRecent().args; + expect(updatedState.activeSortOption?.propertyName).toBe('date'); + }); + + it('should update data manager when displayedColumnIds length changes from adapter', async () => { + await setupInitialState(); + + const spy = spyOn( + dataManagerService, + 'updateDataState', + ).and.callThrough(); + + const updatedHostState: SkyDataHost = { + id: component.viewId, + displayedColumnIds: ['col-1'], + page: 1, + searchText: 'initial-search', + activeSortOption: { propertyName: 'name', descending: false }, + selectedIds: undefined, + }; + + adapterService.updateDataHost(updatedHostState, 'external-source'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(spy).toHaveBeenCalled(); + const [updatedState] = spy.calls.mostRecent().args; + const viewState = updatedState.getViewStateById(component.viewId); + expect(viewState?.displayedColumnIds).toEqual(['col-1']); + }); + }); +}); diff --git a/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host-controller.directive.ts b/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host-controller.directive.ts new file mode 100644 index 0000000000..a1074dbef1 --- /dev/null +++ b/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host-controller.directive.ts @@ -0,0 +1,237 @@ +import { + Directive, + computed, + effect, + inject, + input, + linkedSignal, + untracked, +} from '@angular/core'; +import { toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { SkyDataHost, SkyDataHostService } from '@skyux/lists'; + +import { filter, map, switchMap } from 'rxjs'; + +import { SkyDataManagerFilterControllerDirective } from '../data-manager-filters/data-manager-filter-controller.directive'; +import { SkyDataManagerService } from '../data-manager.service'; +import { SkyDataManagerSortOption } from '../models/data-manager-sort-option'; +import { SkyDataManagerState } from '../models/data-manager-state'; +import { SkyDataViewState } from '../models/data-view-state'; + +import { SkyDataManagerHostService } from './data-manager-host.service'; + +/** + * A directive applied to a data-displaying component (like `sky-data-grid`) that enables integration with a data manager. + * This directive synchronizes the component's data state with the data manager's state, including displayed columns, + * sort order, current page, and selected rows. + */ +@Directive({ + selector: '[skyDataManagerHostController]', + providers: [ + SkyDataManagerHostService, + { + provide: SkyDataHostService, + useExisting: SkyDataManagerHostService, + }, + ], + hostDirectives: [SkyDataManagerFilterControllerDirective], +}) +export class SkyDataManagerHostControllerDirective { + /** + * The view ID for the data view in the data manager. + * @required + */ + public readonly viewId = input.required(); + + readonly #sourceId = 'skyDataManagerHostController'; + readonly #dataManagerSourceId = computed( + () => `${this.#sourceId}--${this.viewId()}`, + ); + readonly #dataManagerService = inject(SkyDataManagerService); + readonly #dataManagerSortOptions = toSignal< + SkyDataManagerSortOption[], + SkyDataManagerSortOption[] + >( + this.#dataManagerService + .getDataManagerConfigUpdates() + .pipe(map((options) => options.sortOptions ?? [])), + { initialValue: [] }, + ); + readonly #dataManagerState = toSignal( + toObservable(this.#dataManagerSourceId).pipe( + filter(Boolean), + switchMap((sourceId) => + this.#dataManagerService.getDataStateUpdates(sourceId), + ), + ), + { initialValue: new SkyDataManagerState({}) }, + ); + readonly #adapterService = inject(SkyDataManagerHostService); + readonly #dataHostState = toSignal( + this.#adapterService.getDataHostUpdates(this.#sourceId), + ); + readonly #lastSentDataHost = linkedSignal(() => this.#dataHostState()); + readonly #lastSentDataManagerState = linkedSignal(() => + this.#dataManagerState(), + ); + + constructor() { + effect(() => { + const dataManagerState = this.#dataManagerState(); + const lastSentDataHost = untracked(() => this.#lastSentDataHost()); + const newState = this.#dataHostStateFromDataManagerState( + dataManagerState, + lastSentDataHost, + ); + if ( + newState && + (!lastSentDataHost || + this.#dataHostHasChanges(lastSentDataHost, newState)) + ) { + this.#lastSentDataHost.set(newState); + this.#adapterService.updateDataHost(newState, this.#sourceId); + } + }); + effect(() => { + const lastSentDataManagerState = untracked(() => + this.#lastSentDataManagerState(), + ); + const dataHost = this.#dataHostState(); + if (lastSentDataManagerState && dataHost) { + const newDataManagerState = this.#dataManagerStateFromDataHostState( + lastSentDataManagerState, + dataHost, + ); + if ( + this.#dataManagerStateHasChanges( + lastSentDataManagerState, + newDataManagerState, + ) + ) { + this.#lastSentDataManagerState.set(newDataManagerState); + this.#dataManagerService.updateDataState( + newDataManagerState, + this.#dataManagerSourceId(), + ); + } + } + }); + } + + #dataHostStateFromDataManagerState( + dataState: SkyDataManagerState, + dataHost: SkyDataHost | undefined, + ): SkyDataHost | undefined { + const viewId = this.viewId(); + const viewState = dataState.getViewStateById(viewId); + + if (viewState) { + return { + activeSortOption: dataState.activeSortOption + ? { + propertyName: dataState.activeSortOption.propertyName, + descending: dataState.activeSortOption.descending, + } + : undefined, + displayedColumnIds: + viewState.displayedColumnIds.length > 0 + ? viewState.displayedColumnIds + : (dataHost?.displayedColumnIds ?? []), + id: viewId, + page: Number(viewState.additionalData?.page ?? dataHost?.page ?? 1), + searchText: dataState.searchText, + selectedIds: dataState.selectedIds, + }; + } + return undefined; + } + + #dataManagerStateFromDataHostState( + dataState: SkyDataManagerState, + dataHost: SkyDataHost, + ): SkyDataManagerState { + const id = this.viewId(); + const activeSortOption = this.#dataManagerSortOptions().find( + (option) => + option.propertyName === dataHost.activeSortOption?.propertyName && + option.descending === !!dataHost.activeSortOption?.descending, + ); + const viewState = dataState.getViewStateById(id); + return new SkyDataManagerState({ + ...dataState, + activeSortOption, + searchText: dataHost.searchText, + selectedIds: dataHost.selectedIds, + views: [ + ...dataState.views.filter(({ viewId }) => viewId !== id), + new SkyDataViewState({ + viewId: id, + columnIds: viewState?.columnIds, + columnWidths: viewState?.columnWidths, + displayedColumnIds: dataHost.displayedColumnIds, + additionalData: { + ...(viewState?.additionalData ?? {}), + page: dataHost.page, + }, + }), + ], + }); + } + + #dataHostHasChanges( + oldDataHost: SkyDataHost, + newDataHost: SkyDataHost, + ): boolean { + return ( + String(oldDataHost.activeSortOption?.propertyName) !== + String(newDataHost.activeSortOption?.propertyName) || + !!oldDataHost.activeSortOption?.descending !== + !!newDataHost.activeSortOption?.descending || + oldDataHost.id !== newDataHost.id || + Number(oldDataHost.page) !== Number(newDataHost.page) || + String(oldDataHost.searchText) !== String(newDataHost.searchText) || + oldDataHost.displayedColumnIds.length !== + newDataHost.displayedColumnIds.length || + oldDataHost.displayedColumnIds.some( + (id, idx) => String(id) !== String(newDataHost.displayedColumnIds[idx]), + ) || + Number(oldDataHost.selectedIds?.length) !== + Number(newDataHost.selectedIds?.length) || + !!oldDataHost.selectedIds?.some( + (id, idx) => id !== newDataHost.selectedIds?.[idx], + ) + ); + } + + #dataManagerStateHasChanges( + oldDataManagerState: SkyDataManagerState, + newDataManagerState: SkyDataManagerState, + ): boolean { + const viewId = this.viewId(); + const oldDataViewState = oldDataManagerState.getViewStateById(viewId); + const newDataViewState = newDataManagerState.getViewStateById(viewId); + return ( + String(oldDataManagerState.activeSortOption?.propertyName) !== + String(newDataManagerState.activeSortOption?.propertyName) || + !!oldDataManagerState.activeSortOption?.descending !== + !!newDataManagerState.activeSortOption?.descending || + !!oldDataViewState?.additionalData !== + !!newDataViewState?.additionalData || + Number(oldDataViewState?.additionalData?.page) !== + Number(newDataViewState?.additionalData?.page) || + String(oldDataManagerState.searchText) !== + String(newDataManagerState.searchText) || + Number(oldDataManagerState.selectedIds?.length ?? 0) !== + Number(newDataManagerState.selectedIds?.length ?? 0) || + !!oldDataManagerState.selectedIds?.some( + (id, idx) => + String(id) !== String(newDataManagerState.selectedIds?.[idx]), + ) || + Number(oldDataViewState?.displayedColumnIds?.length) !== + Number(newDataViewState?.displayedColumnIds?.length) || + !!oldDataViewState?.displayedColumnIds?.some( + (id, idx) => id !== newDataViewState?.displayedColumnIds?.[idx], + ) + ); + } +} diff --git a/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host.service.ts b/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host.service.ts new file mode 100644 index 0000000000..2fd3212f5c --- /dev/null +++ b/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host.service.ts @@ -0,0 +1,32 @@ +import { Injectable, signal } from '@angular/core'; +import { SkyDataHost, SkyDataHostService } from '@skyux/lists'; + +import { Observable, ReplaySubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; + +interface SkyDataHostChange { + dataHost: SkyDataHost; + sourceId: string; +} + +@Injectable() +export class SkyDataManagerHostService extends SkyDataHostService { + readonly #dataHostChange = new ReplaySubject(1); + readonly #dataHostId = signal(''); + + public override hostId = this.#dataHostId.asReadonly(); + + public getDataHostUpdates(sourceId: string): Observable { + return this.#dataHostChange.pipe( + filter((c) => c.sourceId !== sourceId), + map((c) => c.dataHost), + ); + } + + public updateDataHost(dataHost: SkyDataHost, sourceId: string): void { + if (dataHost.id) { + this.#dataHostId.set(dataHost.id); + } + this.#dataHostChange.next({ dataHost, sourceId }); + } +} diff --git a/libs/components/data-manager/src/lib/modules/data-manager/data-manager.module.ts b/libs/components/data-manager/src/lib/modules/data-manager/data-manager.module.ts index 025f9b2ce1..cf80e3bdc7 100644 --- a/libs/components/data-manager/src/lib/modules/data-manager/data-manager.module.ts +++ b/libs/components/data-manager/src/lib/modules/data-manager/data-manager.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; import { SKY_DATA_MANAGER_COLUMN_PICKER_PROVIDERS } from './data-manager-column-picker/data-manager-column-picker-providers'; import { SkyDataManagerFilterControllerDirective } from './data-manager-filters/data-manager-filter-controller.directive'; +import { SkyDataManagerHostControllerDirective } from './data-manager-host/data-manager-host-controller.directive'; import { SkyDataManagerToolbarLeftItemComponent } from './data-manager-toolbar/data-manager-toolbar-left-item.component'; import { SkyDataManagerToolbarPrimaryItemComponent } from './data-manager-toolbar/data-manager-toolbar-primary-item.component'; import { SkyDataManagerToolbarRightItemComponent } from './data-manager-toolbar/data-manager-toolbar-right-item.component'; @@ -14,6 +15,7 @@ import { SkyDataViewComponent } from './data-view.component'; imports: [ SkyDataManagerComponent, SkyDataManagerFilterControllerDirective, + SkyDataManagerHostControllerDirective, SkyDataManagerToolbarComponent, SkyDataManagerToolbarLeftItemComponent, SkyDataManagerToolbarPrimaryItemComponent, @@ -24,6 +26,7 @@ import { SkyDataViewComponent } from './data-view.component'; exports: [ SkyDataManagerComponent, SkyDataManagerFilterControllerDirective, + SkyDataManagerHostControllerDirective, SkyDataManagerToolbarComponent, SkyDataManagerToolbarLeftItemComponent, SkyDataManagerToolbarPrimaryItemComponent, diff --git a/libs/components/lists/src/index.ts b/libs/components/lists/src/index.ts index 39dee804b2..1759bc1173 100644 --- a/libs/components/lists/src/index.ts +++ b/libs/components/lists/src/index.ts @@ -10,6 +10,9 @@ export { SkyPagingContentChangeArgs } from './lib/modules/paging/types/paging-co export { SkyRepeaterExpandModeType } from './lib/modules/repeater/repeater-expand-mode-type'; export { SkyRepeaterModule } from './lib/modules/repeater/repeater.module'; +export { SkyDataHost } from './lib/modules/shared/data-host-service/data-host'; +export { SkyDataHostService } from './lib/modules/shared/data-host-service/data-host.service'; + export { SkyFilterStateService } from './lib/modules/shared/filter-state-service/filter-state.service'; export { SkyFilterState } from './lib/modules/shared/filter-state-service/filter-state'; export { SkyFilterStateFilterItem } from './lib/modules/shared/filter-state-service/filter-state-filter-item'; diff --git a/libs/components/lists/src/lib/modules/shared/data-host-service/data-host.service.ts b/libs/components/lists/src/lib/modules/shared/data-host-service/data-host.service.ts new file mode 100644 index 0000000000..d8d6ff552b --- /dev/null +++ b/libs/components/lists/src/lib/modules/shared/data-host-service/data-host.service.ts @@ -0,0 +1,21 @@ +import { Signal } from '@angular/core'; + +import { Observable } from 'rxjs'; + +import { SkyDataHost } from './data-host'; + +export abstract class SkyDataHostService { + public abstract hostId: Signal; + + /** + * Subscribe to data host updates that did not originate from the given source ID. + * This mirrors the data manager service pattern to avoid update loops between participants. + */ + public abstract getDataHostUpdates(sourceId: string): Observable; + + /** + * Updates the data hosts and broadcasts the change to subscribers. + * Implementations should emit a new value to `filterStateChange` with the provided data. + */ + public abstract updateDataHost(host: SkyDataHost, sourceId: string): void; +} diff --git a/libs/components/lists/src/lib/modules/shared/data-host-service/data-host.ts b/libs/components/lists/src/lib/modules/shared/data-host-service/data-host.ts new file mode 100644 index 0000000000..7a3012c4c0 --- /dev/null +++ b/libs/components/lists/src/lib/modules/shared/data-host-service/data-host.ts @@ -0,0 +1,50 @@ +/** + * Represents data state for components that can integrate with a data manager. + * This interface provides a standard way for data-displaying components (like data grids) + * to communicate their state with data management systems. + * @internal + */ +export interface SkyDataHost { + /** + * The data property and direction for sorting. + */ + activeSortOption: + | { + /** + * The data property to sort by. + * @required + */ + propertyName: string; + /** + * Whether to apply the sort in descending order. + * @required + */ + descending: boolean; + } + | undefined; + + /** + * The IDs or fields for columns to display. + */ + displayedColumnIds: string[]; + + /** + * An identifier for the data host. + */ + id: string; + + /** + * The current page number (1-based). + */ + page: number | undefined; + + /** + * The search text to apply. + */ + searchText: string | undefined; + + /** + * The currently selected rows or objects. + */ + selectedIds: string[] | undefined; +} diff --git a/libs/components/manifest/project.json b/libs/components/manifest/project.json index 298b75f82b..ba4e90ead0 100644 --- a/libs/components/manifest/project.json +++ b/libs/components/manifest/project.json @@ -48,6 +48,12 @@ "!{workspaceRoot}/libs/components/core/.storybook/**/*", "!{workspaceRoot}/libs/components/core/jest.config.ts", "!{workspaceRoot}/libs/components/core/src/test-setup.ts", + "{workspaceRoot}/libs/components/data-grid/**/*.ts", + "!{workspaceRoot}/libs/components/data-grid/**/*.@(spec|stories).ts", + "!{workspaceRoot}/libs/components/data-grid/**/fixtures/**/*", + "!{workspaceRoot}/libs/components/data-grid/.storybook/**/*", + "!{workspaceRoot}/libs/components/data-grid/jest.config.ts", + "!{workspaceRoot}/libs/components/data-grid/src/test-setup.ts", "{workspaceRoot}/libs/components/data-manager/**/*.ts", "!{workspaceRoot}/libs/components/data-manager/**/*.@(spec|stories).ts", "!{workspaceRoot}/libs/components/data-manager/**/fixtures/**/*", diff --git a/libs/components/packages/package.json b/libs/components/packages/package.json index d271ef6dc6..f8e39fd751 100644 --- a/libs/components/packages/package.json +++ b/libs/components/packages/package.json @@ -52,6 +52,7 @@ "@skyux/colorpicker": "0.0.0-PLACEHOLDER", "@skyux/config": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", + "@skyux/data-grid": "0.0.0-PLACEHOLDER", "@skyux/data-manager": "0.0.0-PLACEHOLDER", "@skyux/datetime": "0.0.0-PLACEHOLDER", "@skyux/docs-tools": "0.0.0-PLACEHOLDER", diff --git a/tsconfig.base.json b/tsconfig.base.json index 46106c4e79..dafa9e1e59 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -69,6 +69,10 @@ "@skyux/config": ["libs/components/config/src/index.ts"], "@skyux/core": ["libs/components/core/src/index.ts"], "@skyux/core/testing": ["libs/components/core/testing/src/public-api.ts"], + "@skyux/data-grid": ["libs/components/data-grid/src/index.ts"], + "@skyux/data-grid/testing": [ + "libs/components/data-grid/testing/src/public-api.ts" + ], "@skyux/data-manager": ["libs/components/data-manager/src/index.ts"], "@skyux/data-manager/testing": [ "libs/components/data-manager/testing/src/public-api.ts"