🏷️ Rework product list view (#3296)
This commit is contained in:
parent
c0b69e7c31
commit
f8d86510b2
129 changed files with 3225 additions and 1183 deletions
|
@ -18,6 +18,7 @@ import {
|
|||
PERMISSION_GROUP_LIST_SELECTORS,
|
||||
PLUGINS_LIST_SELECTORS,
|
||||
PRODUCT_TYPES_LIST_SELECTORS,
|
||||
PRODUCTS_LIST,
|
||||
SALES_SELECTORS,
|
||||
SHIPPING_ZONES_LIST_SELECTORS,
|
||||
STAFF_MEMBERS_LIST_SELECTORS,
|
||||
|
@ -25,6 +26,7 @@ import {
|
|||
} from "../elements/";
|
||||
import { PERMISSIONS_OPTIONS } from "../fixtures/permissionsUsers";
|
||||
import { urlList } from "../fixtures/urlList";
|
||||
import { ensureCanvasStatic } from "../support/customCommands/sharedElementsOperations/canvas";
|
||||
import {
|
||||
expectConfigurationAvailableSectionsNumber,
|
||||
expectConfigurationSectionsToBeVisible,
|
||||
|
@ -430,6 +432,9 @@ describe("As a staff user I want to navigate through shop using different permis
|
|||
cy.get(MENU_SELECTORS.CATALOG.collections).should("be.visible");
|
||||
expectMainMenuAvailableSections(5);
|
||||
});
|
||||
ensureCanvasStatic(PRODUCTS_LIST.dataGridTable).then(() => {
|
||||
cy.assertCanvasRowsNumber(PRODUCTS_LIST.dataGridTable, 21);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -2,10 +2,9 @@
|
|||
/// <reference types="../../support"/>
|
||||
|
||||
import { PRODUCT_DETAILS } from "../../elements/catalog/products/product-details";
|
||||
import { PRODUCTS_LIST } from "../../elements/catalog/products/products-list";
|
||||
import { SHARED_ELEMENTS } from "../../elements/shared/sharedElements";
|
||||
import { demoProductsNames } from "../../fixtures/products";
|
||||
import { productDetailsUrl, urlList } from "../../fixtures/urlList";
|
||||
import { productDetailsUrl } from "../../fixtures/urlList";
|
||||
import { getFirstProducts } from "../../support/api/requests/Product";
|
||||
import { deleteCollectionsStartsWith } from "../../support/api/utils/catalog/collectionsUtils";
|
||||
import {
|
||||
|
@ -19,39 +18,6 @@ describe("Tests for images", () => {
|
|||
cy.clearSessionData().loginUserViaRequest();
|
||||
});
|
||||
|
||||
it(
|
||||
"Images on product list should be displayed",
|
||||
{ tags: ["@products", "@allEnv", "@stable"] },
|
||||
() => {
|
||||
cy.addAliasToGraphRequest("ProductList")
|
||||
.visit(urlList.products)
|
||||
.wait("@ProductList")
|
||||
.its("response.body.data.products.edges")
|
||||
.then(products => {
|
||||
cy.get(SHARED_ELEMENTS.skeleton).should("not.exist");
|
||||
cy.get(PRODUCTS_LIST.productImage)
|
||||
.each($image => {
|
||||
cy.wrap($image)
|
||||
.invoke("attr", "src")
|
||||
.then(imageUrl => {
|
||||
cy.request(imageUrl);
|
||||
})
|
||||
.then(respImage => {
|
||||
expect(respImage.status).to.eq(200);
|
||||
});
|
||||
})
|
||||
.then(images => {
|
||||
const expectedProductsSvgAvatars =
|
||||
products.length - images.length;
|
||||
cy.get(PRODUCTS_LIST.imageIcon).should(
|
||||
"have.length",
|
||||
expectedProductsSvgAvatars,
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"Should display product image",
|
||||
{ tags: ["@products", "@allEnv", "@stable"] },
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable import/no-duplicates */
|
||||
import * as MENU_SELECTORS from "./left-menu/left-menu-selectors";
|
||||
import * as LOGIN_SELECTORS from "./login-selectors";
|
||||
import * as SET_PASSWORD_SELECTORS from "./login-selectors";
|
||||
|
|
|
@ -3,9 +3,12 @@ import {
|
|||
CATEGORY_DETAILS_SELECTORS,
|
||||
} from "./categories";
|
||||
import { COLLECTION_SELECTORS } from "./collection-selectors";
|
||||
import { PRODUCT_DETAILS, PRODUCTS_LIST } from "./products";
|
||||
|
||||
export {
|
||||
CATEGORIES_LIST_SELECTORS,
|
||||
CATEGORY_DETAILS_SELECTORS,
|
||||
COLLECTION_SELECTORS,
|
||||
PRODUCT_DETAILS,
|
||||
PRODUCTS_LIST,
|
||||
};
|
||||
|
|
4
cypress/elements/catalog/products/index.js
Normal file
4
cypress/elements/catalog/products/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { PRODUCT_DETAILS } from "./product-details";
|
||||
import { PRODUCTS_LIST } from "./products-list";
|
||||
|
||||
export { PRODUCT_DETAILS, PRODUCTS_LIST };
|
|
@ -1,5 +1,6 @@
|
|||
export const PRODUCTS_LIST = {
|
||||
productsList: "[data-test-id^='id-']",
|
||||
dataGridTable: "[data-testid='data-grid-canvas']",
|
||||
productsNames: "[data-test-id='name']",
|
||||
dialogProductTypeInput: "[data-test-id='dialog-product-type']",
|
||||
createProductBtn: "[data-test-id='add-product']",
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
CATEGORIES_LIST_SELECTORS,
|
||||
CATEGORY_DETAILS_SELECTORS,
|
||||
COLLECTION_SELECTORS,
|
||||
PRODUCT_DETAILS,
|
||||
PRODUCTS_LIST,
|
||||
} from "./catalog";
|
||||
import { CHANNELS_SELECTORS } from "./channels";
|
||||
import { CONFIGURATION_SELECTORS } from "./configuration/configuration-selectors";
|
||||
|
@ -67,8 +69,10 @@ export {
|
|||
PERMISSION_GROUP_LIST_SELECTORS,
|
||||
PLUGINS_DETAILS_SELECTORS,
|
||||
PLUGINS_LIST_SELECTORS,
|
||||
PRODUCT_DETAILS,
|
||||
PRODUCT_TYPE_DETAILS_SELECTORS,
|
||||
PRODUCT_TYPES_LIST_SELECTORS,
|
||||
PRODUCTS_LIST,
|
||||
SALES_SELECTORS,
|
||||
SET_PASSWORD_SELECTORS,
|
||||
SHARED_ELEMENTS,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { SHARED_ELEMENTS } from "./sharedElements";
|
||||
import { ADDRESS_SELECTORS } from "./addressForm";
|
||||
import { SHARED_ELEMENTS } from "./sharedElements";
|
||||
|
||||
export { SHARED_ELEMENTS, ADDRESS_SELECTORS };
|
||||
|
|
|
@ -41,3 +41,6 @@ Cypress.Commands.add("checkIfDataAreNotNull", data => {
|
|||
Cypress.Commands.add("checkIfElementIsVisible", element => {
|
||||
cy.get(element).should("be.visible");
|
||||
});
|
||||
Cypress.Commands.add("assertCanvasRowsNumber", (canvas, rowNumber) => {
|
||||
cy.get(canvas).find("tr").should("have.length", rowNumber);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import { recurse } from "cypress-recurse";
|
||||
import pixelmatch from "pixelmatch";
|
||||
|
||||
export function ensureCanvasStatic(selector = "canvas") {
|
||||
cy.log(`ensure the image in **${selector}** is static`);
|
||||
const noLog = { log: false };
|
||||
|
||||
const delay = 300; // ms, when grabbing new image
|
||||
|
||||
// take the current image
|
||||
return cy
|
||||
.get(selector, noLog)
|
||||
.then($canvas => {
|
||||
const ctx1 = $canvas[0].getContext("2d");
|
||||
const width = $canvas[0].width;
|
||||
const height = $canvas[0].height;
|
||||
let img1 = ctx1.getImageData(0, 0, width, height);
|
||||
|
||||
// initial delay to make sure we catch updates
|
||||
cy.wait(delay, noLog);
|
||||
|
||||
return recurse(
|
||||
() =>
|
||||
cy.get(selector, noLog).then($canvas => {
|
||||
const ctx2 = $canvas[0].getContext("2d");
|
||||
const img2 = ctx2.getImageData(0, 0, width, height);
|
||||
|
||||
const diff = ctx2.createImageData(width, height);
|
||||
// number of different pixels
|
||||
const number = pixelmatch(
|
||||
img1.data,
|
||||
img2.data,
|
||||
diff.data,
|
||||
width,
|
||||
height,
|
||||
{
|
||||
threshold: 0.1,
|
||||
},
|
||||
);
|
||||
|
||||
// for next comparison, use the new image
|
||||
// as the base - this way we can get to the end
|
||||
// of any animation
|
||||
img1 = img2;
|
||||
|
||||
return number;
|
||||
}),
|
||||
// predicate function
|
||||
numberOfDifferentPixels => numberOfDifferentPixels < 10,
|
||||
// recurse options
|
||||
{
|
||||
// by default uses the default command timeout
|
||||
log: numberOfDifferentPixels =>
|
||||
cy.log(`**${numberOfDifferentPixels}** diff pixels`),
|
||||
delay,
|
||||
},
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
cy.log(`picture in **${selector}** is static`);
|
||||
});
|
||||
}
|
|
@ -79,6 +79,9 @@
|
|||
"context": "input label",
|
||||
"string": "Stock reservation for anonymous user (in minutes)"
|
||||
},
|
||||
"+U6ozc": {
|
||||
"string": "Type"
|
||||
},
|
||||
"+VEhV8": {
|
||||
"context": "product channels",
|
||||
"string": "Availability"
|
||||
|
@ -1468,6 +1471,10 @@
|
|||
"context": "header",
|
||||
"string": "Translation Product Variant \"{productName}\" - {languageCode}"
|
||||
},
|
||||
"9B2mOB": {
|
||||
"context": "tile view pagination label",
|
||||
"string": "No. of products"
|
||||
},
|
||||
"9C7PZE": {
|
||||
"context": "navigation section name",
|
||||
"string": "Navigation"
|
||||
|
@ -1705,6 +1712,9 @@
|
|||
"context": "section description",
|
||||
"string": "You can set basic checkout rules that will be applied globally to all your channels"
|
||||
},
|
||||
"AyQkmp": {
|
||||
"string": "Sorting by this column is not available"
|
||||
},
|
||||
"AzMSmb": {
|
||||
"context": "caption",
|
||||
"string": "If enabled this attribute can be used as a column in product table."
|
||||
|
@ -3824,6 +3834,9 @@
|
|||
"context": "activate app",
|
||||
"string": "Are you sure you want to activate this app? Activating will start gathering events."
|
||||
},
|
||||
"Q4m1CG": {
|
||||
"string": "Edit Product"
|
||||
},
|
||||
"Q55cTG": {
|
||||
"context": "order refund amount",
|
||||
"string": "Previously refunded"
|
||||
|
@ -4182,10 +4195,6 @@
|
|||
"context": "export filtered items to csv file",
|
||||
"string": "Current search ({number})"
|
||||
},
|
||||
"Sd0Ppm": {
|
||||
"context": "product publication date",
|
||||
"string": "Published on {date}"
|
||||
},
|
||||
"Sjd7wm": {
|
||||
"context": "product filter label",
|
||||
"string": "Product"
|
||||
|
@ -5290,10 +5299,6 @@
|
|||
"context": "gift card history message",
|
||||
"string": "Gift card balance was reset by {resetBy}"
|
||||
},
|
||||
"aFLtLk": {
|
||||
"context": "tab name",
|
||||
"string": "All Products"
|
||||
},
|
||||
"aHc89n": {
|
||||
"context": "select warehouse to restock items",
|
||||
"string": "Select Warehouse"
|
||||
|
@ -7383,10 +7388,6 @@
|
|||
"context": "dialog content",
|
||||
"string": "Select one of customer addresses or add a new address:"
|
||||
},
|
||||
"qpQ0uB": {
|
||||
"context": "product publication date",
|
||||
"string": "Unpublished"
|
||||
},
|
||||
"qrWOxx": {
|
||||
"string": "No members found"
|
||||
},
|
||||
|
@ -7412,10 +7413,6 @@
|
|||
"r+dgiv": {
|
||||
"string": "Taxes"
|
||||
},
|
||||
"r0hgpM": {
|
||||
"context": "product publication date",
|
||||
"string": "Becomes published on {date}"
|
||||
},
|
||||
"r1aQ2f": {
|
||||
"context": "dialog header",
|
||||
"string": "Unassign Attribute from Product Type"
|
||||
|
@ -7773,6 +7770,9 @@
|
|||
"context": "order total amount",
|
||||
"string": "Total"
|
||||
},
|
||||
"tiY7bx": {
|
||||
"string": "Add new product"
|
||||
},
|
||||
"tlGXkh": {
|
||||
"context": "input description",
|
||||
"string": "Unlimited"
|
||||
|
|
78
package-lock.json
generated
78
package-lock.json
generated
|
@ -26,7 +26,7 @@
|
|||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@material-ui/styles": "^4.11.4",
|
||||
"@reach/auto-id": "^0.16.0",
|
||||
"@saleor/macaw-ui": "^0.8.0-pre.49",
|
||||
"@saleor/macaw-ui": "^0.8.0-pre.50",
|
||||
"@saleor/sdk": "^0.4.4",
|
||||
"@sentry/react": "^6.0.0",
|
||||
"@types/faker": "^5.1.6",
|
||||
|
@ -60,6 +60,7 @@
|
|||
"lz-string": "^1.4.4",
|
||||
"marked": "^4.0.17",
|
||||
"moment-timezone": "^0.5.32",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"qs": "^6.9.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -145,6 +146,7 @@
|
|||
"codecov": "^3.7.1",
|
||||
"core-js": "^3.7.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"cypress-recurse": "^1.27.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"env-var": "^7.3.0",
|
||||
"esbuild-loader": "^2.18.0",
|
||||
|
@ -7271,9 +7273,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@saleor/macaw-ui": {
|
||||
"version": "0.8.0-pre.49",
|
||||
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.49.tgz",
|
||||
"integrity": "sha512-SHUfBE1AaDsam/K3ZcotDtUH6/jcyRR9Gi/Zpvw/cTcwoosq09WcewMWZNdHgY5Fkbgve7HTKb+wCyah+sBO8w==",
|
||||
"version": "0.8.0-pre.50",
|
||||
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.50.tgz",
|
||||
"integrity": "sha512-PGDkk1KmabYOQTPPzu+cmBxEyjdiYQdmzAiqtMIKwo8zff8nYPVE7yVKF9rJ7o3twqAiqF7fuAObo8qVjgZUYg==",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom-interactions": "^0.5.0",
|
||||
"@radix-ui/react-radio-group": "^1.1.1",
|
||||
|
@ -14883,6 +14885,15 @@
|
|||
"mocha": ">=3.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cypress-recurse": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress-recurse/-/cypress-recurse-1.27.0.tgz",
|
||||
"integrity": "sha512-BCD83UqaxlD+JiqZn1PvIhHRXasgfCt57vLC1Fcyifvxh4QklELRcYUJV3MdhKamMkmajaErLfnCNbZ8VJ5SIg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"humanize-duration": "^3.27.3"
|
||||
}
|
||||
},
|
||||
"node_modules/cypress-repeat": {
|
||||
"version": "2.3.3",
|
||||
"license": "MIT",
|
||||
|
@ -19911,6 +19922,12 @@
|
|||
"node": ">=8.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/humanize-duration": {
|
||||
"version": "3.28.0",
|
||||
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.28.0.tgz",
|
||||
"integrity": "sha512-jMAxraOOmHuPbffLVDKkEKi/NeG8dMqP8lGRd6Tbf7JgAeG33jjgPWDbXXU7ypCI0o+oNKJFgbSB9FKVdWNI2A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/husky": {
|
||||
"version": "8.0.2",
|
||||
"license": "MIT",
|
||||
|
@ -27389,6 +27406,17 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/pixelmatch": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz",
|
||||
"integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==",
|
||||
"dependencies": {
|
||||
"pngjs": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pixelmatch": "bin/pixelmatch"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-dir": {
|
||||
"version": "3.0.0",
|
||||
"devOptional": true,
|
||||
|
@ -27472,6 +27500,14 @@
|
|||
"semver-compare": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
|
||||
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
|
||||
"engines": {
|
||||
"node": ">=12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pnp-webpack-plugin": {
|
||||
"version": "1.5.0",
|
||||
"license": "MIT",
|
||||
|
@ -41575,9 +41611,9 @@
|
|||
}
|
||||
},
|
||||
"@saleor/macaw-ui": {
|
||||
"version": "0.8.0-pre.49",
|
||||
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.49.tgz",
|
||||
"integrity": "sha512-SHUfBE1AaDsam/K3ZcotDtUH6/jcyRR9Gi/Zpvw/cTcwoosq09WcewMWZNdHgY5Fkbgve7HTKb+wCyah+sBO8w==",
|
||||
"version": "0.8.0-pre.50",
|
||||
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.50.tgz",
|
||||
"integrity": "sha512-PGDkk1KmabYOQTPPzu+cmBxEyjdiYQdmzAiqtMIKwo8zff8nYPVE7yVKF9rJ7o3twqAiqF7fuAObo8qVjgZUYg==",
|
||||
"requires": {
|
||||
"@floating-ui/react-dom-interactions": "^0.5.0",
|
||||
"@radix-ui/react-radio-group": "^1.1.1",
|
||||
|
@ -47190,6 +47226,15 @@
|
|||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"cypress-recurse": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress-recurse/-/cypress-recurse-1.27.0.tgz",
|
||||
"integrity": "sha512-BCD83UqaxlD+JiqZn1PvIhHRXasgfCt57vLC1Fcyifvxh4QklELRcYUJV3MdhKamMkmajaErLfnCNbZ8VJ5SIg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"humanize-duration": "^3.27.3"
|
||||
}
|
||||
},
|
||||
"cypress-repeat": {
|
||||
"version": "2.3.3",
|
||||
"optional": true,
|
||||
|
@ -50376,6 +50421,12 @@
|
|||
"version": "1.1.1",
|
||||
"devOptional": true
|
||||
},
|
||||
"humanize-duration": {
|
||||
"version": "3.28.0",
|
||||
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.28.0.tgz",
|
||||
"integrity": "sha512-jMAxraOOmHuPbffLVDKkEKi/NeG8dMqP8lGRd6Tbf7JgAeG33jjgPWDbXXU7ypCI0o+oNKJFgbSB9FKVdWNI2A==",
|
||||
"dev": true
|
||||
},
|
||||
"husky": {
|
||||
"version": "8.0.2",
|
||||
"optional": true
|
||||
|
@ -55373,6 +55424,14 @@
|
|||
"version": "4.0.5",
|
||||
"devOptional": true
|
||||
},
|
||||
"pixelmatch": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz",
|
||||
"integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==",
|
||||
"requires": {
|
||||
"pngjs": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"pkg-dir": {
|
||||
"version": "3.0.0",
|
||||
"devOptional": true,
|
||||
|
@ -55429,6 +55488,11 @@
|
|||
"semver-compare": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"pngjs": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
|
||||
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="
|
||||
},
|
||||
"pnp-webpack-plugin": {
|
||||
"version": "1.5.0",
|
||||
"optional": true,
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@material-ui/styles": "^4.11.4",
|
||||
"@reach/auto-id": "^0.16.0",
|
||||
"@saleor/macaw-ui": "^0.8.0-pre.49",
|
||||
"@saleor/macaw-ui": "^0.8.0-pre.50",
|
||||
"@saleor/sdk": "^0.4.4",
|
||||
"@sentry/react": "^6.0.0",
|
||||
"@types/faker": "^5.1.6",
|
||||
|
@ -67,6 +67,7 @@
|
|||
"lz-string": "^1.4.4",
|
||||
"marked": "^4.0.17",
|
||||
"moment-timezone": "^0.5.32",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"qs": "^6.9.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -144,6 +145,7 @@
|
|||
"@types/webappsec-credential-management": "^0.5.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.41.0",
|
||||
"@typescript-eslint/parser": "^5.41.0",
|
||||
"@vitejs/plugin-react-swc": "^3.2.0",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^27.5.1",
|
||||
"babel-loader": "^8.0.6",
|
||||
|
@ -151,6 +153,7 @@
|
|||
"codecov": "^3.7.1",
|
||||
"core-js": "^3.7.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"cypress-recurse": "^1.27.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"env-var": "^7.3.0",
|
||||
"esbuild-loader": "^2.18.0",
|
||||
|
@ -172,7 +175,6 @@
|
|||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-pwa": "^0.13.3",
|
||||
"vite-plugin-sentry": "^1.1.6",
|
||||
"@vitejs/plugin-react-swc": "^3.2.0",
|
||||
"workbox-cacheable-response": "^6.1.2",
|
||||
"workbox-expiration": "^6.1.2",
|
||||
"workbox-precaching": "^6.1.2",
|
||||
|
|
126
src/components/AppLayout/FilterBar/Filter.tsx
Normal file
126
src/components/AppLayout/FilterBar/Filter.tsx
Normal file
|
@ -0,0 +1,126 @@
|
|||
import {
|
||||
FilterContent,
|
||||
FilterElement,
|
||||
FilterErrorMessages,
|
||||
IFilter,
|
||||
InvalidFilters,
|
||||
} from "@dashboard/components/Filter";
|
||||
import useFilter from "@dashboard/components/Filter/useFilter";
|
||||
import { extractInvalidFilters } from "@dashboard/components/Filter/utils";
|
||||
import { ClickAwayListener, Grow, Popper } from "@material-ui/core";
|
||||
import { sprinkles } from "@saleor/macaw-ui/next";
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
import { FilterButton } from "./FilterButton";
|
||||
import { getSelectedFilterAmount } from "./utils";
|
||||
|
||||
export interface FilterProps<TFilterKeys extends string = string> {
|
||||
currencySymbol?: string;
|
||||
errorMessages?: FilterErrorMessages<TFilterKeys>;
|
||||
menu: IFilter<TFilterKeys>;
|
||||
onFilterAdd: (filter: Array<FilterElement<string>>) => void;
|
||||
onFilterAttributeFocus?: (id?: string) => void;
|
||||
}
|
||||
|
||||
export const Filter = ({
|
||||
currencySymbol,
|
||||
menu,
|
||||
onFilterAdd,
|
||||
onFilterAttributeFocus,
|
||||
errorMessages,
|
||||
}: FilterProps) => {
|
||||
const anchor = React.useRef<HTMLDivElement>();
|
||||
const [isFilterMenuOpened, setFilterMenuOpened] = useState(false);
|
||||
const [filterErrors, setFilterErrors] = useState<InvalidFilters<string>>({});
|
||||
const [data, dispatch, reset] = useFilter(menu);
|
||||
|
||||
const isFilterActive = menu.some(filterElement => filterElement.active);
|
||||
const selectedFilterAmount = useMemo(
|
||||
() => getSelectedFilterAmount(menu, data),
|
||||
[data, menu],
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const invalidFilters = extractInvalidFilters(data, menu);
|
||||
|
||||
if (Object.keys(invalidFilters).length > 0) {
|
||||
setFilterErrors(invalidFilters);
|
||||
return;
|
||||
}
|
||||
|
||||
setFilterErrors({});
|
||||
onFilterAdd(data);
|
||||
setFilterMenuOpened(false);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
reset();
|
||||
setFilterErrors({});
|
||||
};
|
||||
|
||||
return (
|
||||
<ClickAwayListener
|
||||
onClickAway={event => {
|
||||
if ((event.target as HTMLElement).getAttribute("role") !== "option") {
|
||||
setFilterMenuOpened(false);
|
||||
}
|
||||
}}
|
||||
mouseEvent="onMouseUp"
|
||||
>
|
||||
<div ref={anchor}>
|
||||
<FilterButton
|
||||
isFilterActive={isFilterActive}
|
||||
onClick={() => setFilterMenuOpened(!isFilterMenuOpened)}
|
||||
selectedFilterAmount={selectedFilterAmount}
|
||||
/>
|
||||
<Popper
|
||||
className={sprinkles({
|
||||
backgroundColor: "surfaceNeutralPlain",
|
||||
overflowY: "scroll",
|
||||
boxShadow: "modal",
|
||||
zIndex: "3",
|
||||
})}
|
||||
style={{
|
||||
width: "376px",
|
||||
height: "450px",
|
||||
}}
|
||||
open={isFilterMenuOpened}
|
||||
anchorEl={anchor.current}
|
||||
transition
|
||||
disablePortal
|
||||
placement="bottom-start"
|
||||
modifiers={{
|
||||
flip: {
|
||||
enabled: false,
|
||||
},
|
||||
hide: {
|
||||
enabled: false,
|
||||
},
|
||||
preventOverflow: {
|
||||
boundariesElement: "scrollParent",
|
||||
enabled: false,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{() => (
|
||||
<Grow>
|
||||
<FilterContent
|
||||
errorMessages={errorMessages}
|
||||
errors={filterErrors}
|
||||
dataStructure={menu}
|
||||
currencySymbol={currencySymbol}
|
||||
filters={data}
|
||||
onClear={handleClear}
|
||||
onFilterPropertyChange={dispatch}
|
||||
onFilterAttributeFocus={onFilterAttributeFocus}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
);
|
||||
};
|
||||
|
||||
Filter.displayName = "Filter";
|
62
src/components/AppLayout/FilterBar/FilterBar.tsx
Normal file
62
src/components/AppLayout/FilterBar/FilterBar.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { FilterErrorMessages, IFilter } from "@dashboard/components/Filter";
|
||||
import { FilterProps, SearchPageProps } from "@dashboard/types";
|
||||
import { Box } from "@saleor/macaw-ui/next";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
import { Filter } from "./Filter";
|
||||
import SearchInput from "./SearchInput";
|
||||
|
||||
export interface FilterBarProps<TKeys extends string = string>
|
||||
extends FilterProps<TKeys>,
|
||||
SearchPageProps {
|
||||
searchPlaceholder: string;
|
||||
errorMessages?: FilterErrorMessages<TKeys>;
|
||||
filterStructure: IFilter<TKeys>;
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
export const FilterBar: React.FC<FilterBarProps> = ({
|
||||
currencySymbol,
|
||||
filterStructure,
|
||||
initialSearch,
|
||||
searchPlaceholder,
|
||||
onSearchChange,
|
||||
onFilterChange,
|
||||
onFilterAttributeFocus,
|
||||
errorMessages,
|
||||
actions,
|
||||
}: FilterBarProps) => (
|
||||
<>
|
||||
<Box
|
||||
display="grid"
|
||||
__gridTemplateColumns="1fr 1fr"
|
||||
gap={7}
|
||||
paddingBottom={5}
|
||||
paddingX={9}
|
||||
borderColor="neutralPlain"
|
||||
borderBottomStyle="solid"
|
||||
borderBottomWidth={1}
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={7}>
|
||||
<Filter
|
||||
errorMessages={errorMessages}
|
||||
menu={filterStructure}
|
||||
currencySymbol={currencySymbol}
|
||||
onFilterAdd={onFilterChange}
|
||||
onFilterAttributeFocus={onFilterAttributeFocus}
|
||||
/>
|
||||
<Box __width="320px">
|
||||
<SearchInput
|
||||
initialSearch={initialSearch}
|
||||
placeholder={searchPlaceholder}
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="flex-end">
|
||||
{actions}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
FilterBar.displayName = "FilterBar";
|
35
src/components/AppLayout/FilterBar/FilterButton.tsx
Normal file
35
src/components/AppLayout/FilterBar/FilterButton.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { Box, DropdownButton } from "@saleor/macaw-ui/next";
|
||||
import React, { MouseEventHandler } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
interface FilterButtonProps {
|
||||
isFilterActive: boolean;
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
selectedFilterAmount: number;
|
||||
}
|
||||
|
||||
export const FilterButton = ({
|
||||
isFilterActive,
|
||||
onClick,
|
||||
selectedFilterAmount,
|
||||
}: FilterButtonProps) => (
|
||||
<DropdownButton data-test-id="show-filters-button" onClick={onClick}>
|
||||
<FormattedMessage
|
||||
id="FNpv6K"
|
||||
defaultMessage="Filters"
|
||||
description="button"
|
||||
/>
|
||||
{isFilterActive && (
|
||||
<>
|
||||
<Box
|
||||
as="span"
|
||||
backgroundColor="interactiveNeutralDefault"
|
||||
height={6}
|
||||
width={1}
|
||||
marginX={3}
|
||||
/>
|
||||
{selectedFilterAmount}
|
||||
</>
|
||||
)}
|
||||
</DropdownButton>
|
||||
);
|
41
src/components/AppLayout/FilterBar/SearchInput.tsx
Normal file
41
src/components/AppLayout/FilterBar/SearchInput.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import Debounce from "@dashboard/components/Debounce";
|
||||
import { SearchPageProps } from "@dashboard/types";
|
||||
import { Box, SearchInput as MacawSearchInput } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
export interface SearchInputProps extends SearchPageProps {
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
const SearchInput: React.FC<SearchInputProps> = props => {
|
||||
const { initialSearch, onSearchChange, placeholder } = props;
|
||||
|
||||
const [search, setSearch] = React.useState(initialSearch);
|
||||
React.useEffect(() => setSearch(initialSearch), [initialSearch]);
|
||||
|
||||
return (
|
||||
<Debounce debounceFn={onSearchChange} time={500}>
|
||||
{debounceSearchChange => {
|
||||
const handleSearchChange = (event: React.ChangeEvent<any>) => {
|
||||
const value = event.target.value;
|
||||
setSearch(value);
|
||||
debounceSearchChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="flex" alignItems="center" width="100%">
|
||||
<MacawSearchInput
|
||||
size="small"
|
||||
value={search}
|
||||
onChange={handleSearchChange}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
</Debounce>
|
||||
);
|
||||
};
|
||||
|
||||
SearchInput.displayName = "SearchInput";
|
||||
export default SearchInput;
|
1
src/components/AppLayout/FilterBar/index.ts
Normal file
1
src/components/AppLayout/FilterBar/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./FilterBar";
|
42
src/components/AppLayout/FilterBar/utils.test.ts
Normal file
42
src/components/AppLayout/FilterBar/utils.test.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { FilterElement, IFilter } from "@dashboard/components/Filter";
|
||||
|
||||
import { getSelectedFilterAmount } from "./utils";
|
||||
|
||||
describe("getSelectedFilterAmount", () => {
|
||||
it("should return 0 when no selected filters", () => {
|
||||
// Arrange
|
||||
const menu = [{ name: "test1" }, { name: "test2" }] as IFilter<string>;
|
||||
|
||||
const data = [
|
||||
{ name: "test1", active: false },
|
||||
{ name: "test2", active: false },
|
||||
] as Array<FilterElement<string>>;
|
||||
|
||||
// Act
|
||||
const result = getSelectedFilterAmount(menu, data);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it("should return amount of selected filters", () => {
|
||||
// Arrange
|
||||
const menu = [
|
||||
{ name: "test1" },
|
||||
{ name: "test2" },
|
||||
{ name: "test3" },
|
||||
] as IFilter<string>;
|
||||
|
||||
const data = [
|
||||
{ name: "test1", active: true },
|
||||
{ name: "test2", active: false },
|
||||
{ name: "test3", active: true },
|
||||
] as Array<FilterElement<string>>;
|
||||
|
||||
// Act
|
||||
const result = getSelectedFilterAmount(menu, data);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(2);
|
||||
});
|
||||
});
|
17
src/components/AppLayout/FilterBar/utils.ts
Normal file
17
src/components/AppLayout/FilterBar/utils.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { FilterElement, IFilter } from "@dashboard/components/Filter";
|
||||
|
||||
export const getSelectedFilterAmount = <TFilterKeys extends string = string>(
|
||||
menu: IFilter<TFilterKeys>,
|
||||
data: Array<FilterElement<TFilterKeys>>,
|
||||
) =>
|
||||
menu.reduce((acc, filterElement) => {
|
||||
const dataFilterElement = data.find(
|
||||
({ name }) => name === filterElement.name,
|
||||
);
|
||||
|
||||
if (!dataFilterElement) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc + (dataFilterElement.active ? 1 : 0);
|
||||
}, 0);
|
|
@ -5,6 +5,9 @@ interface LimitsInfoProps {
|
|||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `Text` instead
|
||||
*/
|
||||
export const LimitsInfo: React.FC<LimitsInfoProps> = ({ text }) => (
|
||||
<Box position="absolute" left={10} bottom={3}>
|
||||
{text}
|
||||
|
|
|
@ -9,20 +9,22 @@ import { TopNavWrapper } from "./TopNavWrapper";
|
|||
interface TopNavProps {
|
||||
title: string | React.ReactNode;
|
||||
href?: string;
|
||||
withoutBorder?: boolean;
|
||||
}
|
||||
|
||||
export const TopNav: React.FC<PropsWithChildren<TopNavProps>> = ({
|
||||
title,
|
||||
href,
|
||||
withoutBorder = false,
|
||||
children,
|
||||
}) => {
|
||||
const { availableChannels, channel, isPickerActive, setChannel } =
|
||||
useAppChannel(false);
|
||||
|
||||
return (
|
||||
<TopNavWrapper>
|
||||
<TopNavWrapper withoutBorder={withoutBorder}>
|
||||
{href && <TopNavLink to={href} />}
|
||||
<Box __flex={1} marginLeft={5}>
|
||||
<Box __flex={1}>
|
||||
<Text variant="title">{title}</Text>
|
||||
</Box>
|
||||
<Box display="flex" flexWrap="nowrap">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ArrowLeftIcon, Button } from "@saleor/macaw-ui/next";
|
||||
import { ArrowLeftIcon, Button, sprinkles } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
|
@ -6,7 +6,7 @@ export const TopNavLink: React.FC<{
|
|||
to: string;
|
||||
variant?: "secondary" | "tertiary";
|
||||
}> = ({ to, variant = "secondary" }) => (
|
||||
<Link to={to}>
|
||||
<Link to={to} className={sprinkles({ marginRight: 5 })}>
|
||||
<Button
|
||||
icon={<ArrowLeftIcon />}
|
||||
variant={variant}
|
||||
|
|
|
@ -3,13 +3,16 @@ import React from "react";
|
|||
|
||||
import { topBarHeight } from "../consts";
|
||||
|
||||
export const TopNavWrapper: React.FC = ({ children }) => (
|
||||
export const TopNavWrapper: React.FC<{ withoutBorder?: boolean }> = ({
|
||||
children,
|
||||
withoutBorder,
|
||||
}) => (
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
paddingX={9}
|
||||
paddingY={8}
|
||||
borderBottomWidth={1}
|
||||
borderBottomWidth={withoutBorder ? 0 : 1}
|
||||
borderBottomStyle="solid"
|
||||
borderColor="neutralPlain"
|
||||
position="relative"
|
||||
|
|
55
src/components/ButtonWithDropdown/ButtonWithDropdown.tsx
Normal file
55
src/components/ButtonWithDropdown/ButtonWithDropdown.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
ChervonDownIcon,
|
||||
Dropdown,
|
||||
List,
|
||||
Text,
|
||||
} from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
interface ButtonWithDropdownProps {
|
||||
onClick: () => void;
|
||||
options: Array<{ label: string; testId: string; onSelect: () => void }>;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
export const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
|
||||
onClick,
|
||||
options,
|
||||
children,
|
||||
testId,
|
||||
}) => (
|
||||
<Dropdown>
|
||||
<Dropdown.Trigger>
|
||||
<Button data-test-id={testId} onClick={onClick}>
|
||||
{children}
|
||||
<ChervonDownIcon />
|
||||
</Button>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Content align="end">
|
||||
<Box>
|
||||
<List
|
||||
padding={5}
|
||||
borderRadius={4}
|
||||
boxShadow="overlay"
|
||||
backgroundColor="surfaceNeutralPlain"
|
||||
>
|
||||
{options.map(item => (
|
||||
<Dropdown.Item>
|
||||
<List.Item
|
||||
borderRadius={4}
|
||||
paddingX={4}
|
||||
paddingY={5}
|
||||
onClick={item.onSelect}
|
||||
data-test-id={item.testId}
|
||||
>
|
||||
<Text>{item.label}</Text>
|
||||
</List.Item>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Dropdown.Content>
|
||||
</Dropdown>
|
||||
);
|
1
src/components/ButtonWithDropdown/index.ts
Normal file
1
src/components/ButtonWithDropdown/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./ButtonWithDropdown";
|
|
@ -28,6 +28,9 @@ export interface ButtonWithSelectProps
|
|||
href?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use [`ButtonWithDropdown`](https://github.com/saleor/saleor-dashboard/blob/main/src/components/ButtonWithDropdown/ButtonWithDropdown.tsx) instead
|
||||
*/
|
||||
export const ButtonWithSelect: React.FC<ButtonWithSelectProps> = ({
|
||||
options,
|
||||
children,
|
||||
|
|
|
@ -66,6 +66,9 @@ const useStyles = makeStyles(
|
|||
{ name: "CardMenu" },
|
||||
);
|
||||
|
||||
/**
|
||||
* @deprecated use [`TopNavMenu`](https://github.com/saleor/saleor-dashboard/blob/main/src/components/TopNavMenu/TopNavMenu.tsx) instead
|
||||
*/
|
||||
const CardMenu: React.FC<CardMenuProps> = props => {
|
||||
const {
|
||||
className,
|
||||
|
|
|
@ -4,11 +4,11 @@ import { FetchMoreProps } from "@dashboard/types";
|
|||
import { ClickAwayListener, Grow, Popper } from "@material-ui/core";
|
||||
import {
|
||||
Choice,
|
||||
ColumnsIcon,
|
||||
IconButton,
|
||||
IconButtonProps,
|
||||
makeStyles,
|
||||
} from "@saleor/macaw-ui";
|
||||
import { TableEditIcon } from "@saleor/macaw-ui/next";
|
||||
import { score } from "fuzzaldrin";
|
||||
import sortBy from "lodash/sortBy";
|
||||
import React from "react";
|
||||
|
@ -64,9 +64,8 @@ const ColumnPicker: React.FC<ColumnPickerProps> = props => {
|
|||
|
||||
// Component is uncontrolled but we need to reset it somehow, so we change
|
||||
// initial prop after reset callback to force value refreshing
|
||||
const [initialColumnsChoices, setInitialColumnsChoices] = useStateFromProps(
|
||||
initialColumns,
|
||||
);
|
||||
const [initialColumnsChoices, setInitialColumnsChoices] =
|
||||
useStateFromProps(initialColumns);
|
||||
|
||||
const onChange: FormChange<string[]> = event => {
|
||||
selectedColumns.current = event.target.value;
|
||||
|
@ -112,7 +111,7 @@ const ColumnPicker: React.FC<ColumnPickerProps> = props => {
|
|||
state={isExpanded ? "active" : "default"}
|
||||
onClick={() => setExpansionState(prevState => !prevState)}
|
||||
>
|
||||
<ColumnsIcon />
|
||||
<TableEditIcon />
|
||||
</IconButton>
|
||||
<Popper
|
||||
className={classes.popper}
|
||||
|
|
|
@ -2,42 +2,59 @@ import "@glideapps/glide-data-grid/dist/index.css";
|
|||
|
||||
import { usePreventHistoryBack } from "@dashboard/hooks/usePreventHistoryBack";
|
||||
import DataEditor, {
|
||||
DataEditorProps,
|
||||
DataEditorRef,
|
||||
EditableGridCell,
|
||||
GridCell,
|
||||
GridColumn,
|
||||
GridMouseEventArgs,
|
||||
GridSelection,
|
||||
HeaderClickedEventArgs,
|
||||
Item,
|
||||
} from "@glideapps/glide-data-grid";
|
||||
import { GetRowThemeCallback } from "@glideapps/glide-data-grid/dist/ts/data-grid/data-grid-render";
|
||||
import { Card, CardContent, Typography } from "@material-ui/core";
|
||||
import { useTheme } from "@saleor/macaw-ui";
|
||||
import { Box, useTheme } from "@saleor/macaw-ui/next";
|
||||
import clsx from "clsx";
|
||||
import range from "lodash/range";
|
||||
import throttle from "lodash/throttle";
|
||||
import React from "react";
|
||||
import React, {
|
||||
MutableRefObject,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { contentMaxWidth } from "../AppLayout/consts";
|
||||
import { CardMenuItem } from "../CardMenu";
|
||||
import ColumnPicker from "../ColumnPicker";
|
||||
import { FullScreenContainer } from "./FullScreenContainer";
|
||||
import { Header } from "./Header";
|
||||
import { RowActions } from "./RowActions";
|
||||
import { ColumnPickerProps } from "../ColumnPicker";
|
||||
import { FullScreenContainer } from "./components/FullScreenContainer";
|
||||
import { Header } from "./components/Header";
|
||||
import { RowActions } from "./components/RowActions";
|
||||
import { TooltipContainer } from "./components/TooltipContainer";
|
||||
import useCells from "./customCells/useCells";
|
||||
import { headerIcons } from "./headerIcons";
|
||||
import useDatagridChange, {
|
||||
DatagridChange,
|
||||
OnDatagridChange,
|
||||
} from "./hooks/useDatagridChange";
|
||||
import { useFullScreenMode } from "./hooks/useFullScreenMode";
|
||||
import { usePortalClasses } from "./hooks/usePortalClasses";
|
||||
import { useScrollRight } from "./hooks/useScrollRight";
|
||||
import { useTooltipContainer } from "./hooks/useTooltipContainer";
|
||||
import useStyles, {
|
||||
cellHeight,
|
||||
useDatagridTheme,
|
||||
useFullScreenStyles,
|
||||
} from "./styles";
|
||||
import { AvailableColumn } from "./types";
|
||||
import useCells from "./useCells";
|
||||
import useColumns from "./useColumns";
|
||||
import useDatagridChange, {
|
||||
DatagridChange,
|
||||
OnDatagridChange,
|
||||
} from "./useDatagridChange";
|
||||
import { useFullScreenMode } from "./useFullScreenMode";
|
||||
import { usePortalClasses } from "./usePortalClasses";
|
||||
import { getDefultColumnPickerProps } from "./utils";
|
||||
|
||||
export interface GetCellContentOpts {
|
||||
changes: React.MutableRefObject<DatagridChange[]>;
|
||||
changes: MutableRefObject<DatagridChange[]>;
|
||||
added: number[];
|
||||
removed: number[];
|
||||
getChangeIndex: (column: string, row: number) => number;
|
||||
|
@ -48,20 +65,33 @@ export interface MenuItemsActions {
|
|||
}
|
||||
|
||||
export interface DatagridProps {
|
||||
addButtonLabel: string;
|
||||
addButtonLabel?: string;
|
||||
availableColumns: readonly AvailableColumn[];
|
||||
emptyText: string;
|
||||
getCellError: (item: Item, opts: GetCellContentOpts) => boolean;
|
||||
getCellContent: (item: Item, opts: GetCellContentOpts) => GridCell;
|
||||
getColumnTooltipContent?: (colIndex: number) => string;
|
||||
menuItems: (index: number) => CardMenuItem[];
|
||||
rows: number;
|
||||
title: string;
|
||||
title?: string;
|
||||
fullScreenTitle?: string;
|
||||
selectionActions: (
|
||||
selection: number[],
|
||||
actions: MenuItemsActions,
|
||||
) => React.ReactNode;
|
||||
) => ReactNode;
|
||||
onChange?: OnDatagridChange;
|
||||
onHeaderClicked?: (colIndex: number, event: HeaderClickedEventArgs) => void;
|
||||
renderColumnPicker?: (
|
||||
defaultProps: Partial<ColumnPickerProps>,
|
||||
) => ReactElement;
|
||||
onRowClick?: (item: Item) => void;
|
||||
onColumnMoved?: (startIndex: number, endIndex: number) => void;
|
||||
onColumnResize?: (column: GridColumn, newSize: number) => void;
|
||||
readonly?: boolean;
|
||||
rowMarkers?: DataEditorProps["rowMarkers"];
|
||||
freezeColumns?: DataEditorProps["freezeColumns"];
|
||||
verticalBorder?: DataEditorProps["verticalBorder"];
|
||||
columnSelect?: DataEditorProps["columnSelect"];
|
||||
}
|
||||
|
||||
export const Datagrid: React.FC<DatagridProps> = ({
|
||||
|
@ -75,28 +105,41 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
selectionActions,
|
||||
title,
|
||||
fullScreenTitle,
|
||||
onHeaderClicked,
|
||||
onChange,
|
||||
}): React.ReactElement => {
|
||||
renderColumnPicker,
|
||||
onRowClick,
|
||||
getColumnTooltipContent,
|
||||
readonly = false,
|
||||
rowMarkers = "checkbox",
|
||||
freezeColumns = 1,
|
||||
verticalBorder,
|
||||
columnSelect = "none",
|
||||
onColumnMoved,
|
||||
onColumnResize,
|
||||
...datagridProps
|
||||
}): ReactElement => {
|
||||
const classes = useStyles();
|
||||
const fullScreenClasses = useFullScreenStyles(classes);
|
||||
const datagridTheme = useDatagridTheme();
|
||||
const editor = React.useRef<DataEditorRef>();
|
||||
const { themeValues } = useTheme();
|
||||
const datagridTheme = useDatagridTheme(readonly);
|
||||
const editor = useRef<DataEditorRef>();
|
||||
const cellProps = useCells();
|
||||
const { scrolledToRight, scroller } = useScrollRight();
|
||||
|
||||
const defualtColumnPickerProps = getDefultColumnPickerProps(
|
||||
classes.ghostIcon,
|
||||
);
|
||||
|
||||
const fullScreenClasses = useFullScreenStyles(classes);
|
||||
const { isOpen, isAnimationOpenFinished, toggle } = useFullScreenMode();
|
||||
|
||||
usePortalClasses({ className: classes.portal });
|
||||
const { clearTooltip, tooltip, setTooltip } = useTooltipContainer();
|
||||
|
||||
const {
|
||||
availableColumnsChoices,
|
||||
columns,
|
||||
columnChoices,
|
||||
defaultColumns,
|
||||
displayedColumns,
|
||||
onColumnMoved,
|
||||
onColumnResize,
|
||||
onColumnsChange,
|
||||
picker,
|
||||
} = useColumns(availableColumns);
|
||||
const [selection, setSelection] = useState<GridSelection>();
|
||||
const [hoverRow, setHoverRow] = useState<number | undefined>(undefined);
|
||||
|
||||
usePortalClasses({ className: classes.portal });
|
||||
usePreventHistoryBack(scroller);
|
||||
|
||||
const {
|
||||
added,
|
||||
|
@ -108,83 +151,132 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
onRowAdded,
|
||||
} = useDatagridChange(availableColumns, rows, onChange);
|
||||
|
||||
const theme = useTheme();
|
||||
const rowsTotal = rows - removed.length + added.length;
|
||||
const hasMenuItem = !!menuItems(0).length;
|
||||
const hasColumnGroups = availableColumns.some(col => col.group);
|
||||
const headerTitle = isAnimationOpenFinished
|
||||
? fullScreenTitle ?? title
|
||||
: title;
|
||||
|
||||
const [scrolledToRight, setScrolledToRight] = React.useState(false);
|
||||
const scroller: HTMLDivElement = document.querySelector(".dvn-scroller");
|
||||
const scrollerInner: HTMLDivElement =
|
||||
document.querySelector(".dvn-scroll-inner");
|
||||
|
||||
usePreventHistoryBack(scroller);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!(scroller && scrollerInner)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = throttle(() => {
|
||||
const isScrolledToRight =
|
||||
scroller.scrollWidth - scroller.clientWidth - scroller.scrollLeft < 2;
|
||||
setScrolledToRight(isScrolledToRight);
|
||||
}, 100);
|
||||
scroller.addEventListener("scroll", handler);
|
||||
|
||||
return () => scroller.removeEventListener("scroll", handler);
|
||||
}, [scroller, scrollerInner]);
|
||||
|
||||
const getCellContentEnh = React.useCallback(
|
||||
const handleGetCellContent = useCallback(
|
||||
([column, row]: Item): GridCell => {
|
||||
const item = [
|
||||
availableColumns.findIndex(ac => ac.id === displayedColumns[column]),
|
||||
row,
|
||||
] as const;
|
||||
const item = [column, row] as const;
|
||||
const opts = { changes, added, removed, getChangeIndex };
|
||||
const columnId = availableColumns[column].id;
|
||||
|
||||
const columnId = availableColumns[column]?.id;
|
||||
const changed = !!changes.current[getChangeIndex(columnId, row)]?.data;
|
||||
|
||||
return {
|
||||
...getCellContent(item, opts),
|
||||
...(changed
|
||||
? { themeOverride: { bgCell: theme.palette.saleor.active[5] } }
|
||||
? {
|
||||
themeOverride: {
|
||||
bgCell: themeValues.colors.background.surfaceBrandHighlight,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(getCellError(item, opts)
|
||||
? {
|
||||
themeOverride: {
|
||||
bgCell:
|
||||
theme.palette.saleor.theme === "light"
|
||||
? theme.palette.saleor.fail.light
|
||||
: theme.palette.saleor.errorAction[5],
|
||||
themeValues.colors.background.interactiveCriticalHovering,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
},
|
||||
[getCellContent, availableColumns, displayedColumns, added, removed],
|
||||
[
|
||||
availableColumns,
|
||||
changes,
|
||||
added,
|
||||
removed,
|
||||
getChangeIndex,
|
||||
getCellContent,
|
||||
themeValues,
|
||||
getCellError,
|
||||
],
|
||||
);
|
||||
|
||||
const onCellEditedEnh = React.useCallback(
|
||||
const handleOnCellEdited = useCallback(
|
||||
([column, row]: Item, newValue: EditableGridCell): void => {
|
||||
onCellEdited(
|
||||
[
|
||||
availableColumns.findIndex(ac => ac.id === displayedColumns[column]),
|
||||
row,
|
||||
],
|
||||
newValue,
|
||||
);
|
||||
onCellEdited([column, row], newValue);
|
||||
editor.current.updateCells(
|
||||
range(displayedColumns.length).map(offset => ({
|
||||
range(availableColumns.length).map(offset => ({
|
||||
cell: [column + offset, row],
|
||||
})),
|
||||
);
|
||||
},
|
||||
[onCellEdited, getCellContent, availableColumns, displayedColumns],
|
||||
[onCellEdited, availableColumns],
|
||||
);
|
||||
|
||||
const [selection, setSelection] = React.useState<GridSelection>();
|
||||
const handleCellClick = useCallback(
|
||||
(item: Item) => {
|
||||
if (onRowClick && item[0] !== -1) {
|
||||
onRowClick(item);
|
||||
}
|
||||
},
|
||||
[onRowClick],
|
||||
);
|
||||
|
||||
const props = useCells();
|
||||
const handleRowHover = useCallback(
|
||||
(args: GridMouseEventArgs) => {
|
||||
if (readonly) {
|
||||
setHoverRow(args.kind !== "cell" ? undefined : args.location[1]);
|
||||
}
|
||||
},
|
||||
[readonly],
|
||||
);
|
||||
|
||||
const removeRows = React.useCallback(
|
||||
const handleGridSelectionChange = (gridSelection: GridSelection) => {
|
||||
// In readonly we not allow selecting cells, but we allow selcting column
|
||||
if (readonly && !gridSelection.current) {
|
||||
setSelection(gridSelection);
|
||||
}
|
||||
if (!readonly) {
|
||||
setSelection(gridSelection);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGetThemeOverride = useCallback<GetRowThemeCallback>(
|
||||
(row: number) => {
|
||||
if (row !== hoverRow) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const overrideTheme = {
|
||||
bgCell: themeValues.colors.background.surfaceNeutralHighlight,
|
||||
bgCellMedium: themeValues.colors.background.surfaceNeutralHighlight,
|
||||
accentLight: undefined,
|
||||
};
|
||||
|
||||
if (readonly) {
|
||||
overrideTheme.accentLight =
|
||||
themeValues.colors.background.surfaceNeutralHighlight;
|
||||
}
|
||||
|
||||
return overrideTheme;
|
||||
},
|
||||
[hoverRow, readonly, themeValues],
|
||||
);
|
||||
|
||||
const handleHeaderClicked = useCallback(
|
||||
(colIndex: number, event: HeaderClickedEventArgs) => {
|
||||
if (getColumnTooltipContent) {
|
||||
const content = getColumnTooltipContent(colIndex);
|
||||
|
||||
if (content) {
|
||||
setTooltip(content, event.bounds);
|
||||
}
|
||||
}
|
||||
|
||||
if (onHeaderClicked) {
|
||||
onHeaderClicked(colIndex, event);
|
||||
}
|
||||
},
|
||||
[getColumnTooltipContent, onHeaderClicked, setTooltip],
|
||||
);
|
||||
|
||||
const handleRemoveRows = useCallback(
|
||||
(rows: number[]) => {
|
||||
if (selection?.rows) {
|
||||
onRowsRemoved(rows);
|
||||
|
@ -194,19 +286,36 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
[selection, onRowsRemoved],
|
||||
);
|
||||
|
||||
const selectionActionsComponent = React.useMemo(
|
||||
() =>
|
||||
selection?.rows.length > 0
|
||||
? selectionActions(Array.from(selection.rows), { removeRows })
|
||||
: null,
|
||||
[selection, selectionActions, removeRows],
|
||||
const handleColumnResize = useCallback(
|
||||
(column: GridColumn, newSize: number) => {
|
||||
if (tooltip) {
|
||||
clearTooltip();
|
||||
}
|
||||
|
||||
onColumnResize(column, newSize);
|
||||
},
|
||||
[clearTooltip, onColumnResize, tooltip],
|
||||
);
|
||||
|
||||
const rowsTotal = rows - removed.length + added.length;
|
||||
const hasColumnGroups = columns.some(col => col.group);
|
||||
const headerTitle = isAnimationOpenFinished
|
||||
? fullScreenTitle ?? title
|
||||
: title;
|
||||
const handleColumnMoved = useCallback(
|
||||
(startIndex: number, endIndex: number) => {
|
||||
if (tooltip) {
|
||||
clearTooltip();
|
||||
}
|
||||
onColumnMoved(startIndex, endIndex);
|
||||
},
|
||||
[clearTooltip, onColumnMoved, tooltip],
|
||||
);
|
||||
|
||||
const selectionActionsComponent = useMemo(
|
||||
() =>
|
||||
selection?.rows.length > 0
|
||||
? selectionActions(Array.from(selection.rows), {
|
||||
removeRows: handleRemoveRows,
|
||||
})
|
||||
: null,
|
||||
[selection, selectionActions, handleRemoveRows],
|
||||
);
|
||||
|
||||
return (
|
||||
<FullScreenContainer
|
||||
|
@ -214,26 +323,30 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
className={fullScreenClasses.fullScreenContainer}
|
||||
>
|
||||
<Card className={classes.root}>
|
||||
<Header title={headerTitle}>
|
||||
<Header.ButtonFullScreen isOpen={isOpen} onToggle={toggle}>
|
||||
{isOpen ? (
|
||||
<FormattedMessage
|
||||
id="QjPJ78"
|
||||
defaultMessage="Close"
|
||||
description="close full-screen"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="483Xnh"
|
||||
defaultMessage="Open"
|
||||
description="open full-screen"
|
||||
/>
|
||||
{headerTitle && (
|
||||
<Header title={headerTitle}>
|
||||
<Header.ButtonFullScreen isOpen={isOpen} onToggle={toggle}>
|
||||
{isOpen ? (
|
||||
<FormattedMessage
|
||||
id="QjPJ78"
|
||||
defaultMessage="Close"
|
||||
description="close full-screen"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="483Xnh"
|
||||
defaultMessage="Open"
|
||||
description="open full-screen"
|
||||
/>
|
||||
)}
|
||||
</Header.ButtonFullScreen>
|
||||
{addButtonLabel && (
|
||||
<Header.ButtonAddRow onAddRow={onRowAdded}>
|
||||
{addButtonLabel}
|
||||
</Header.ButtonAddRow>
|
||||
)}
|
||||
</Header.ButtonFullScreen>
|
||||
<Header.ButtonAddRow onAddRow={onRowAdded}>
|
||||
{addButtonLabel}
|
||||
</Header.ButtonAddRow>
|
||||
</Header>
|
||||
</Header>
|
||||
)}
|
||||
<CardContent classes={{ root: classes.cardContentRoot }}>
|
||||
{rowsTotal > 0 ? (
|
||||
<>
|
||||
|
@ -243,28 +356,43 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
</div>
|
||||
)}
|
||||
<div className={classes.editorContainer}>
|
||||
<Box
|
||||
backgroundColor="plain"
|
||||
borderTopWidth={1}
|
||||
borderTopStyle="solid"
|
||||
borderColor="neutralPlain"
|
||||
__maxWidth={contentMaxWidth}
|
||||
margin="auto"
|
||||
/>
|
||||
<DataEditor
|
||||
{...props}
|
||||
{...cellProps}
|
||||
{...datagridProps}
|
||||
verticalBorder={verticalBorder}
|
||||
headerIcons={headerIcons}
|
||||
theme={datagridTheme}
|
||||
className={classes.datagrid}
|
||||
getCellContent={getCellContentEnh}
|
||||
onCellEdited={onCellEditedEnh}
|
||||
columns={columns}
|
||||
getCellContent={handleGetCellContent}
|
||||
onCellEdited={handleOnCellEdited}
|
||||
columns={availableColumns}
|
||||
rows={rowsTotal}
|
||||
freezeColumns={1}
|
||||
freezeColumns={freezeColumns}
|
||||
smoothScrollX
|
||||
rowMarkers="checkbox"
|
||||
rowMarkers={rowMarkers}
|
||||
rowSelect="multi"
|
||||
rowSelectionMode="multi"
|
||||
rangeSelect="multi-rect"
|
||||
columnSelect="none"
|
||||
columnSelect={columnSelect}
|
||||
getCellsForSelection
|
||||
onColumnMoved={onColumnMoved}
|
||||
onColumnResize={onColumnResize}
|
||||
onGridSelectionChange={setSelection}
|
||||
onColumnMoved={handleColumnMoved}
|
||||
onColumnResize={handleColumnResize}
|
||||
onHeaderClicked={handleHeaderClicked}
|
||||
onCellClicked={handleCellClick}
|
||||
onGridSelectionChange={handleGridSelectionChange}
|
||||
onItemHovered={handleRowHover}
|
||||
getRowThemeOverride={handleGetThemeOverride}
|
||||
gridSelection={selection}
|
||||
rowHeight={cellHeight}
|
||||
headerHeight={cellHeight}
|
||||
headerHeight={cellHeight + 16}
|
||||
ref={editor}
|
||||
onPaste
|
||||
rightElementProps={{
|
||||
|
@ -274,50 +402,60 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
<div
|
||||
className={clsx(classes.rowActionBar, {
|
||||
[classes.rowActionBarScrolledToRight]: scrolledToRight,
|
||||
[classes.rowActionvBarWithItems]: hasMenuItem,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={clsx(classes.rowActionBarShadow, {
|
||||
[classes.rowActionBarShadowActive]: !scrolledToRight,
|
||||
[classes.rowActionBarShadowActive]:
|
||||
!scrolledToRight && hasMenuItem,
|
||||
})}
|
||||
/>
|
||||
<div className={classes.columnPicker}>
|
||||
<ColumnPicker
|
||||
IconButtonProps={{
|
||||
className: classes.ghostIcon,
|
||||
variant: "ghost",
|
||||
hoverOutline: false,
|
||||
}}
|
||||
availableColumns={availableColumnsChoices}
|
||||
initialColumns={columnChoices}
|
||||
defaultColumns={defaultColumns}
|
||||
onSave={onColumnsChange}
|
||||
hasMore={false}
|
||||
loading={false}
|
||||
onFetchMore={() => undefined}
|
||||
onQueryChange={picker.setQuery}
|
||||
query={picker.query}
|
||||
/>
|
||||
<div
|
||||
className={clsx(classes.columnPicker, {
|
||||
[classes.columnPickerBackground]: !hasMenuItem,
|
||||
})}
|
||||
>
|
||||
{renderColumnPicker
|
||||
? renderColumnPicker(defualtColumnPickerProps)
|
||||
: null}
|
||||
</div>
|
||||
{hasColumnGroups && (
|
||||
<div
|
||||
className={clsx(classes.rowAction, {
|
||||
[classes.rowActionScrolledToRight]: scrolledToRight,
|
||||
})}
|
||||
className={clsx(
|
||||
classes.rowAction,
|
||||
classes.rowColumnGroup,
|
||||
{
|
||||
[classes.rowActionScrolledToRight]:
|
||||
scrolledToRight,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{Array(rowsTotal)
|
||||
.fill(0)
|
||||
.map((_, index) => (
|
||||
<RowActions
|
||||
menuItems={menuItems(index)}
|
||||
disabled={index >= rowsTotal - added.length}
|
||||
/>
|
||||
))}
|
||||
{hasMenuItem &&
|
||||
Array(rowsTotal)
|
||||
.fill(0)
|
||||
.map((_, index) => (
|
||||
<RowActions
|
||||
menuItems={menuItems(index)}
|
||||
disabled={index >= rowsTotal - added.length}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
rowMarkerWidth={48}
|
||||
/>
|
||||
<Box
|
||||
position="relative"
|
||||
__marginTop="-1px"
|
||||
backgroundColor="plain"
|
||||
borderTopWidth={1}
|
||||
borderTopStyle="solid"
|
||||
borderColor="neutralPlain"
|
||||
__maxWidth={contentMaxWidth}
|
||||
margin="auto"
|
||||
zIndex="2"
|
||||
/>
|
||||
{/* FIXME: https://github.com/glideapps/glide-data-grid/issues/505 */}
|
||||
{hasColumnGroups && (
|
||||
<div className={classes.columnGroupFixer} />
|
||||
|
@ -325,10 +463,22 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
|||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Typography align="center">{emptyText}</Typography>
|
||||
<Box
|
||||
borderTopStyle="solid"
|
||||
borderTopWidth={1}
|
||||
borderColor="neutralHighlight"
|
||||
paddingY={9}
|
||||
>
|
||||
<Typography align="center">{emptyText}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TooltipContainer
|
||||
clearTooltip={clearTooltip}
|
||||
bounds={tooltip?.bounds}
|
||||
title={tooltip?.title}
|
||||
/>
|
||||
</FullScreenContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useTheme } from "@saleor/macaw-ui";
|
|||
import React, { CSSProperties, FC, PropsWithChildren } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import { useDelayedState } from "./useDelayedState";
|
||||
import { useDelayedState } from "../hooks/useDelayedState";
|
||||
|
||||
const modalRoot =
|
||||
document.getElementById("modal-root") || document.createElement("div");
|
|
@ -3,7 +3,7 @@ import { Button, makeStyles, PlusSmallIcon } from "@saleor/macaw-ui";
|
|||
import clsx from "clsx";
|
||||
import React, { FC, PropsWithChildren } from "react";
|
||||
|
||||
import CardTitle from "../CardTitle";
|
||||
import CardTitle from "../../CardTitle";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
|
@ -1,8 +1,8 @@
|
|||
import { IconButton, MoreHorizontalIcon } from "@saleor/macaw-ui";
|
||||
import React from "react";
|
||||
|
||||
import CardMenu, { CardMenuItem } from "../CardMenu";
|
||||
import useStyles from "./styles";
|
||||
import CardMenu, { CardMenuItem } from "../../CardMenu";
|
||||
import useStyles from "../styles";
|
||||
|
||||
interface RowActionsProps {
|
||||
menuItems: CardMenuItem[];
|
58
src/components/Datagrid/components/TooltipContainer.tsx
Normal file
58
src/components/Datagrid/components/TooltipContainer.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { Tooltip } from "@saleor/macaw-ui";
|
||||
import { Box } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
interface TooltipContainerProps {
|
||||
clearTooltip: () => void;
|
||||
title?: string;
|
||||
bounds?: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const TooltipContainer: React.FC<TooltipContainerProps> = ({
|
||||
title,
|
||||
bounds,
|
||||
clearTooltip,
|
||||
}) => {
|
||||
if (!bounds || !title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { x, y, width, height } = bounds;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* This box fill all space base on bound props, serves for container to clear tooltip onMouseLeave */}
|
||||
<Box
|
||||
position="absolute"
|
||||
as="div"
|
||||
__width={width}
|
||||
__height={height}
|
||||
__top={y}
|
||||
__left={x}
|
||||
zIndex="3"
|
||||
onMouseLeave={clearTooltip}
|
||||
/>
|
||||
|
||||
{/* This box is used for positioning tootip in middle */}
|
||||
<Box
|
||||
position="absolute"
|
||||
as="div"
|
||||
__width={1}
|
||||
__height={1}
|
||||
__top={y}
|
||||
__left={x + width / 2}
|
||||
zIndex="2"
|
||||
>
|
||||
{/* TODO: use tooltip from new macaw when it will be ready */}
|
||||
<Tooltip open={true} title={title} placement="top">
|
||||
<span></span>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -10,8 +10,8 @@ import React from "react";
|
|||
|
||||
import SingleAutocompleteSelectField, {
|
||||
SingleAutocompleteSelectFieldProps,
|
||||
} from "../SingleAutocompleteSelectField";
|
||||
import { Choice } from "../SingleSelectField";
|
||||
} from "../../SingleAutocompleteSelectField";
|
||||
import { Choice } from "../../SingleSelectField";
|
||||
|
||||
export type DropdownChoice = Choice<string, string>;
|
||||
export type DropdownCellContentProps = Pick<
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from "@glideapps/glide-data-grid";
|
||||
import React from "react";
|
||||
|
||||
import { usePriceField } from "../PriceField/usePriceField";
|
||||
import { usePriceField } from "../../PriceField/usePriceField";
|
||||
|
||||
interface MoneyCellProps {
|
||||
readonly kind: "money-cell";
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from "@glideapps/glide-data-grid";
|
||||
import React from "react";
|
||||
|
||||
import { Locale } from "../Locale";
|
||||
import { Locale } from "../../Locale";
|
||||
|
||||
export const numberCellEmptyValue = Symbol();
|
||||
interface NumberCellProps {
|
121
src/components/Datagrid/customCells/ThumbnailCell.tsx
Normal file
121
src/components/Datagrid/customCells/ThumbnailCell.tsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
import {
|
||||
CustomCell,
|
||||
CustomCellRenderer,
|
||||
getMiddleCenterBias,
|
||||
TextCellEntry,
|
||||
} from "@glideapps/glide-data-grid";
|
||||
import React from "react";
|
||||
|
||||
export interface ThumbnailCellProps {
|
||||
readonly kind: "thumbnail-cell";
|
||||
readonly image: string;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export type ThumbnailCell = CustomCell<ThumbnailCellProps>;
|
||||
|
||||
export const thumbnailCellRenderer: CustomCellRenderer<ThumbnailCell> = {
|
||||
isMatch: (cell: CustomCell): cell is ThumbnailCell =>
|
||||
(cell.data as any).kind === "thumbnail-cell",
|
||||
draw: (args, cell) => {
|
||||
const { ctx, rect, theme, imageLoader, col, row } = args;
|
||||
const { image, name } = cell.data;
|
||||
|
||||
const xPad = 7;
|
||||
const size = rect.height - xPad * 2;
|
||||
|
||||
const drawX = rect.x + xPad;
|
||||
const drawY = rect.y + xPad;
|
||||
|
||||
const imageResult = imageLoader.loadOrGetImage(image, col, row);
|
||||
|
||||
ctx.save();
|
||||
|
||||
if (imageResult !== undefined && image) {
|
||||
ctx.save();
|
||||
roundedImage(ctx, drawX, drawY, size, size, 4);
|
||||
ctx.strokeStyle = theme.borderColor;
|
||||
ctx.stroke();
|
||||
ctx.clip();
|
||||
ctx.drawImage(imageResult, drawX, drawY, size, size);
|
||||
ctx.restore();
|
||||
} else {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
roundedImage(ctx, drawX, drawY, size, size, 4);
|
||||
ctx.fillStyle = theme.borderColor;
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
if (name !== undefined) {
|
||||
ctx.fillStyle = theme.textDark;
|
||||
ctx.fillText(
|
||||
name,
|
||||
drawX + size + xPad,
|
||||
rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme),
|
||||
);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
provideEditor: () => ({
|
||||
editor: p => {
|
||||
const { isHighlighted, onChange, value } = p;
|
||||
return (
|
||||
<TextCellEntry
|
||||
highlight={isHighlighted}
|
||||
autoFocus={true}
|
||||
readOnly={value.readonly}
|
||||
value={value.data.name ?? ""}
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...value,
|
||||
data: {
|
||||
...value.data,
|
||||
name: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
disablePadding: true,
|
||||
deletedValue: cell => ({
|
||||
...cell,
|
||||
copyData: "",
|
||||
data: {
|
||||
...cell.data,
|
||||
name: "",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
onPaste: (v, d) => ({
|
||||
...d,
|
||||
name: v,
|
||||
}),
|
||||
};
|
||||
|
||||
function roundedImage(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
radius: number,
|
||||
) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.lineTo(x + width - radius, y);
|
||||
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
||||
ctx.lineTo(x + width, y + height - radius);
|
||||
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
||||
ctx.lineTo(x + radius, y + height);
|
||||
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
||||
ctx.lineTo(x, y + radius);
|
||||
ctx.quadraticCurveTo(x, y, x + radius, y);
|
||||
ctx.closePath();
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
NumberCell,
|
||||
numberCellEmptyValue,
|
||||
} from "@dashboard/components/Datagrid/NumberCell";
|
||||
} from "@dashboard/components/Datagrid/customCells/NumberCell";
|
||||
import { GridCell, GridCellKind } from "@glideapps/glide-data-grid";
|
||||
|
||||
import {
|
||||
|
@ -10,6 +10,7 @@ import {
|
|||
DropdownChoice,
|
||||
} from "./DropdownCell";
|
||||
import { MoneyCell } from "./MoneyCell";
|
||||
import { ThumbnailCell } from "./ThumbnailCell";
|
||||
|
||||
const common = {
|
||||
allowOverlay: true,
|
||||
|
@ -25,6 +26,20 @@ export function textCell(value: string): GridCell {
|
|||
};
|
||||
}
|
||||
|
||||
export function readonlyTextCell(
|
||||
value: string,
|
||||
hasCursorPointer: boolean = true,
|
||||
): GridCell {
|
||||
return {
|
||||
cursor: hasCursorPointer ? "pointer" : "default",
|
||||
allowOverlay: false,
|
||||
readonly: true,
|
||||
data: value,
|
||||
displayData: value,
|
||||
kind: GridCellKind.Text,
|
||||
};
|
||||
}
|
||||
|
||||
export function booleanCell(value: boolean): GridCell {
|
||||
return {
|
||||
...common,
|
||||
|
@ -34,6 +49,13 @@ export function booleanCell(value: boolean): GridCell {
|
|||
};
|
||||
}
|
||||
|
||||
export function loadingCell(): GridCell {
|
||||
return {
|
||||
kind: GridCellKind.Loading,
|
||||
allowOverlay: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function numberCell(
|
||||
value: number | typeof numberCellEmptyValue,
|
||||
): NumberCell {
|
||||
|
@ -63,16 +85,18 @@ export function moneyCell(value: number | null, currency: string): MoneyCell {
|
|||
|
||||
export function dropdownCell(
|
||||
value: DropdownChoice,
|
||||
opts: DropdownCellContentProps &
|
||||
dataOpts: DropdownCellContentProps &
|
||||
(
|
||||
| { choices: DropdownChoice[] }
|
||||
| { update: (text: string) => Promise<DropdownChoice[]> }
|
||||
),
|
||||
opts?: Partial<GridCell>,
|
||||
): DropdownCell {
|
||||
return {
|
||||
...common,
|
||||
...opts,
|
||||
data: {
|
||||
...opts,
|
||||
...dataOpts,
|
||||
kind: "dropdown-cell",
|
||||
value,
|
||||
},
|
||||
|
@ -80,3 +104,21 @@ export function dropdownCell(
|
|||
copyData: value.label,
|
||||
};
|
||||
}
|
||||
|
||||
export function thumbnailCell(
|
||||
name: string,
|
||||
image: string,
|
||||
opts?: Partial<GridCell>,
|
||||
): ThumbnailCell {
|
||||
return {
|
||||
...common,
|
||||
...opts,
|
||||
kind: GridCellKind.Custom,
|
||||
copyData: name ?? "",
|
||||
data: {
|
||||
kind: "thumbnail-cell",
|
||||
image,
|
||||
name,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -5,6 +5,7 @@ import { useMemo } from "react";
|
|||
import { dropdownCellRenderer } from "./DropdownCell";
|
||||
import { moneyCellRenderer } from "./MoneyCell";
|
||||
import { numberCellRenderer } from "./NumberCell";
|
||||
import { thumbnailCellRenderer } from "./ThumbnailCell";
|
||||
|
||||
function useCells() {
|
||||
const { locale } = useLocale();
|
||||
|
@ -13,6 +14,7 @@ function useCells() {
|
|||
moneyCellRenderer(),
|
||||
numberCellRenderer(locale),
|
||||
dropdownCellRenderer,
|
||||
thumbnailCellRenderer,
|
||||
],
|
||||
[locale],
|
||||
);
|
18
src/components/Datagrid/headerIcons.ts
Normal file
18
src/components/Datagrid/headerIcons.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { SpriteMap } from "@glideapps/glide-data-grid";
|
||||
|
||||
export const headerIcons: SpriteMap = {
|
||||
arrowUp:
|
||||
p => `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="${p.bgColor}"
|
||||
d="M8.067 14h7.865a.5.5 0 0 0 .385-.82l-3.933-4.72a.5.5 0 0 0-.768 0l-3.933 4.72a.5.5 0 0 0 .384.82Z"
|
||||
/>
|
||||
</svg>`,
|
||||
arrowDown:
|
||||
p => `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="${p.bgColor}"
|
||||
d="M15.932 10H8.067a.5.5 0 0 0-.384.82l3.933 4.72a.5.5 0 0 0 .768 0l3.933-4.72a.5.5 0 0 0-.385-.82Z"
|
||||
/>
|
||||
</svg>`,
|
||||
};
|
|
@ -3,9 +3,11 @@ import { addAtIndex, removeAtIndex } from "@dashboard/utils/lists";
|
|||
import { GridColumn } from "@glideapps/glide-data-grid";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import { AvailableColumn } from "./types";
|
||||
import { AvailableColumn } from "../types";
|
||||
|
||||
function useColumns(availableColumns: readonly AvailableColumn[]) {
|
||||
export function useColumnsDefault(
|
||||
availableColumns: readonly AvailableColumn[],
|
||||
) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [displayedColumns, setDisplayedColumns] = useStateFromProps(
|
||||
availableColumns.map(({ id }) => id),
|
||||
|
@ -18,7 +20,7 @@ function useColumns(availableColumns: readonly AvailableColumn[]) {
|
|||
addAtIndex(old[startIndex], removeAtIndex(old, startIndex), endIndex),
|
||||
);
|
||||
},
|
||||
[],
|
||||
[setDisplayedColumns],
|
||||
);
|
||||
const onColumnResize = useCallback(
|
||||
(column: GridColumn, newSize: number) =>
|
||||
|
@ -29,7 +31,7 @@ function useColumns(availableColumns: readonly AvailableColumn[]) {
|
|||
: prevColumn,
|
||||
),
|
||||
),
|
||||
[],
|
||||
[setColumnState],
|
||||
);
|
||||
const onColumnsChange = useCallback(
|
||||
(picked: string[]) =>
|
||||
|
@ -39,7 +41,7 @@ function useColumns(availableColumns: readonly AvailableColumn[]) {
|
|||
.filter(column => !prevColumns.find(c => c === column))
|
||||
.map(column => availableColumns.find(ac => ac.id === column).id),
|
||||
]),
|
||||
[availableColumns],
|
||||
[availableColumns, setDisplayedColumns],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
|
@ -62,9 +64,10 @@ function useColumns(availableColumns: readonly AvailableColumn[]) {
|
|||
})),
|
||||
[availableColumns],
|
||||
);
|
||||
const defaultColumns = useMemo(() => availableColumns.map(({ id }) => id), [
|
||||
availableColumns,
|
||||
]);
|
||||
const defaultColumns = useMemo(
|
||||
() => availableColumns.map(({ id }) => id),
|
||||
[availableColumns],
|
||||
);
|
||||
|
||||
return {
|
||||
availableColumnsChoices,
|
||||
|
@ -81,5 +84,3 @@ function useColumns(availableColumns: readonly AvailableColumn[]) {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default useColumns;
|
|
@ -1,7 +1,7 @@
|
|||
import { act, renderHook } from "@testing-library/react-hooks";
|
||||
import React from "react";
|
||||
|
||||
import { AvailableColumn } from "./types";
|
||||
import { AvailableColumn } from "../types";
|
||||
import useDatagridChange, {
|
||||
DatagridChangeStateContext,
|
||||
useDatagridChangeState,
|
|
@ -11,7 +11,7 @@ import {
|
|||
useState,
|
||||
} from "react";
|
||||
|
||||
import { AvailableColumn } from "./types";
|
||||
import { AvailableColumn } from "../types";
|
||||
|
||||
export interface DatagridChange {
|
||||
data: any;
|
||||
|
@ -32,13 +32,21 @@ export interface UseDatagridChangeState {
|
|||
removed: number[];
|
||||
setRemoved: Dispatch<SetStateAction<number[]>>;
|
||||
changes: MutableRefObject<DatagridChange[]>;
|
||||
clear: () => void;
|
||||
}
|
||||
export function useDatagridChangeState(): UseDatagridChangeState {
|
||||
const [added, setAdded] = useState<number[]>([]);
|
||||
const [removed, setRemoved] = useState<number[]>([]);
|
||||
const changes = useRef<DatagridChange[]>([]);
|
||||
|
||||
const clear = () => {
|
||||
changes.current = [];
|
||||
setAdded([]);
|
||||
setRemoved([]);
|
||||
};
|
||||
|
||||
return {
|
||||
clear,
|
||||
added,
|
||||
setAdded,
|
||||
removed,
|
||||
|
@ -47,9 +55,8 @@ export function useDatagridChangeState(): UseDatagridChangeState {
|
|||
};
|
||||
}
|
||||
|
||||
export const DatagridChangeStateContext = createContext<UseDatagridChangeState>(
|
||||
undefined,
|
||||
);
|
||||
export const DatagridChangeStateContext =
|
||||
createContext<UseDatagridChangeState>(undefined);
|
||||
export const useDatagridChangeStateContext = () =>
|
||||
useContext(DatagridChangeStateContext);
|
||||
|
||||
|
@ -58,13 +65,8 @@ function useDatagridChange(
|
|||
rows: number,
|
||||
onChange?: OnDatagridChange,
|
||||
) {
|
||||
const {
|
||||
added,
|
||||
setAdded,
|
||||
removed,
|
||||
setRemoved,
|
||||
changes,
|
||||
} = useDatagridChangeStateContext();
|
||||
const { added, setAdded, removed, setRemoved, changes } =
|
||||
useDatagridChangeStateContext();
|
||||
const getChangeIndex = useCallback(
|
||||
(column: string, row: number): number =>
|
||||
changes.current.findIndex(
|
17
src/components/Datagrid/hooks/useEmptyColumn.ts
Normal file
17
src/components/Datagrid/hooks/useEmptyColumn.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { themes, useTheme } from "@saleor/macaw-ui/next";
|
||||
|
||||
export const useEmptyColumn = () => {
|
||||
const { theme: currentTheme } = useTheme();
|
||||
const theme = themes[currentTheme];
|
||||
|
||||
return {
|
||||
id: "empty",
|
||||
title: "",
|
||||
width: 20,
|
||||
themeOverride: {
|
||||
accentColor: theme.colors.background.plain,
|
||||
accentLight: theme.colors.background.plain,
|
||||
bgHeaderHovered: theme.colors.background.plain,
|
||||
},
|
||||
};
|
||||
};
|
27
src/components/Datagrid/hooks/useScrollRight.ts
Normal file
27
src/components/Datagrid/hooks/useScrollRight.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import throttle from "lodash/throttle";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const useScrollRight = () => {
|
||||
const [scrolledToRight, setScrolledToRight] = useState(false);
|
||||
const scroller: HTMLDivElement = document.querySelector(".dvn-scroller");
|
||||
const scrollerInner: HTMLDivElement =
|
||||
document.querySelector(".dvn-scroll-inner");
|
||||
|
||||
useEffect(() => {
|
||||
if (!(scroller && scrollerInner)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = throttle(() => {
|
||||
const isScrolledToRight =
|
||||
scroller.scrollWidth - scroller.clientWidth - scroller.scrollLeft < 2;
|
||||
setScrolledToRight(isScrolledToRight);
|
||||
}, 100);
|
||||
|
||||
scroller.addEventListener("scroll", handler);
|
||||
|
||||
return () => scroller.removeEventListener("scroll", handler);
|
||||
}, [scroller, scrollerInner]);
|
||||
|
||||
return { scrolledToRight, scroller };
|
||||
};
|
32
src/components/Datagrid/hooks/useTooltipContainer.ts
Normal file
32
src/components/Datagrid/hooks/useTooltipContainer.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { useState } from "react";
|
||||
|
||||
interface Bounds {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface UseTooltipContainerState {
|
||||
title: string;
|
||||
bounds: Bounds;
|
||||
}
|
||||
|
||||
export const useTooltipContainer = () => {
|
||||
const [tooltip, setTooltipState] = useState<
|
||||
UseTooltipContainerState | undefined
|
||||
>(undefined);
|
||||
|
||||
const setTooltip = (title: string, bounds: Bounds) => {
|
||||
setTooltipState({
|
||||
title,
|
||||
bounds,
|
||||
});
|
||||
};
|
||||
|
||||
const clearTooltip = () => {
|
||||
setTooltipState(undefined);
|
||||
};
|
||||
|
||||
return { tooltip, setTooltip, clearTooltip };
|
||||
};
|
|
@ -1,25 +1,24 @@
|
|||
import { Theme } from "@glideapps/glide-data-grid";
|
||||
import { makeStyles, useTheme } from "@saleor/macaw-ui";
|
||||
import { themes } from "@saleor/macaw-ui/next";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { useTheme, vars } from "@saleor/macaw-ui/next";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const cellHeight = 36;
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => {
|
||||
() => {
|
||||
const rowActionSelected = {
|
||||
background: theme.palette.background.paper,
|
||||
color: theme.palette.saleor.main[1],
|
||||
background: vars.colors.background.plain,
|
||||
color: vars.colors.border.neutralHighlight,
|
||||
};
|
||||
const activeBorderColor =
|
||||
theme.palette.saleor.theme === "light" ? "#D4D4D4" : "#232323";
|
||||
const activeBorderColor = vars.colors.border.neutralDefault;
|
||||
|
||||
return {
|
||||
actionBtnBar: {
|
||||
position: "absolute",
|
||||
zIndex: 1,
|
||||
background: theme.palette.background.paper,
|
||||
borderRadius: 8,
|
||||
background: vars.colors.background.plain,
|
||||
borderRadius: vars.borderRadius[4],
|
||||
// Right and left toolbars
|
||||
width: `calc(100% - 64px - ${cellHeight} - 1px)`,
|
||||
marginTop: 1,
|
||||
|
@ -28,16 +27,20 @@ const useStyles = makeStyles(
|
|||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
padding: theme.spacing(1),
|
||||
padding: vars.space[4],
|
||||
},
|
||||
columnPicker: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: cellHeight,
|
||||
height: cellHeight + 16,
|
||||
},
|
||||
columnPickerBackground: {
|
||||
background: vars.colors.background.plain,
|
||||
},
|
||||
ghostIcon: {
|
||||
color: theme.palette.saleor.main[3],
|
||||
color: vars.colors.foreground.iconNeutralPlain,
|
||||
padding: vars.space["3"],
|
||||
},
|
||||
portal: {
|
||||
"& input::-webkit-outer-spin-button, input::-webkit-inner-spin-button":
|
||||
|
@ -49,14 +52,14 @@ const useStyles = makeStyles(
|
|||
appearance: "textfield",
|
||||
},
|
||||
"& .clip-region": {
|
||||
border: `1px solid ${theme.palette.saleor.main[1]}`,
|
||||
border: `1px solid ${vars.colors.border.brandSubdued}`,
|
||||
},
|
||||
"& .gdg-growing-entry": {
|
||||
flex: 1,
|
||||
marginTop: 0,
|
||||
},
|
||||
"& .gdg-style": {
|
||||
background: theme.palette.background.paper,
|
||||
background: vars.colors.background.plain,
|
||||
border: "none",
|
||||
// Setting these with !important because we never intend to style
|
||||
// this particular element, like, never ever
|
||||
|
@ -67,11 +70,11 @@ const useStyles = makeStyles(
|
|||
appearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
fontSize: themes.defaultLight.fontSize.bodySmall,
|
||||
letterSpacing: "0.015em",
|
||||
lineHeight: themes.defaultLight.lineHeight.bodySmall,
|
||||
fontWeight: themes.defaultLight.fontWeight.bodySmall,
|
||||
padding: themes.defaultLight.space[3],
|
||||
fontSize: vars.fontSize.bodySmall,
|
||||
letterSpacing: vars.letterSpacing.bodyStrongSmall,
|
||||
lineHeight: vars.lineHeight.bodyEmpMedium,
|
||||
fontWeight: vars.fontWeight.bodySmall,
|
||||
padding: vars.space[3],
|
||||
outline: 0,
|
||||
},
|
||||
'& input[type="number"]': {
|
||||
|
@ -86,26 +89,29 @@ const useStyles = makeStyles(
|
|||
datagrid: {
|
||||
"& .dvn-scroller": {
|
||||
overscrollBehaviorX: "none",
|
||||
scrollbarWidth: "none",
|
||||
},
|
||||
"& .dvn-scroller::-webkit-scrollbar": {
|
||||
display: "none",
|
||||
},
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 0,
|
||||
boxSizing: "content-box",
|
||||
width: "100%",
|
||||
paddingBottom: "1px",
|
||||
color: "red",
|
||||
},
|
||||
root: {
|
||||
position: "relative",
|
||||
},
|
||||
rowActionBar: {
|
||||
height: "100%",
|
||||
background: theme.palette.background.paper,
|
||||
borderLeft: `1px solid ${activeBorderColor}`,
|
||||
width: 36,
|
||||
},
|
||||
rowActionvBarWithItems: {
|
||||
borderLeft: `1px solid ${activeBorderColor}`,
|
||||
background: vars.colors.background.plain,
|
||||
},
|
||||
rowActionBarScrolledToRight: {
|
||||
borderLeftColor: theme.palette.divider,
|
||||
borderLeftColor: vars.colors.border.neutralHighlight,
|
||||
},
|
||||
rowAction: {
|
||||
"&:hover, $rowActionSelected": {
|
||||
|
@ -114,19 +120,22 @@ const useStyles = makeStyles(
|
|||
"&:not(:last-child)": {
|
||||
marginBottom: -1,
|
||||
},
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderLeftColor: activeBorderColor,
|
||||
border: `1px solid ${vars.colors.border.neutralHighlight}`,
|
||||
borderLeft: "none",
|
||||
borderRight: "none",
|
||||
cursor: "pointer",
|
||||
color: theme.palette.saleor.main[5],
|
||||
color: vars.colors.foreground.iconNeutralPlain,
|
||||
marginLeft: -1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: `calc(${cellHeight}px - 1px)`,
|
||||
},
|
||||
rowColumnGroup: {
|
||||
height: cellHeight + 15,
|
||||
},
|
||||
rowActionScrolledToRight: {
|
||||
borderLeftColor: theme.palette.divider,
|
||||
borderLeftColor: vars.colors.border.neutralHighlight,
|
||||
},
|
||||
columnGroupFixer: {
|
||||
position: "absolute",
|
||||
|
@ -135,7 +144,7 @@ const useStyles = makeStyles(
|
|||
height: cellHeight,
|
||||
width: 10,
|
||||
borderLeft: 0,
|
||||
background: theme.palette.background.paper,
|
||||
background: vars.colors.background.plain,
|
||||
},
|
||||
editorContainer: {
|
||||
position: "relative",
|
||||
|
@ -146,9 +155,7 @@ const useStyles = makeStyles(
|
|||
width: 1,
|
||||
position: "absolute",
|
||||
zIndex: -1,
|
||||
transition: theme.transitions.create("box-shadow", {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
transition: "box-shadow .2s ease-in-out",
|
||||
boxShadow: "-1px 0px 12px transparent",
|
||||
},
|
||||
rowActionBarShadowActive: {
|
||||
|
@ -156,8 +163,12 @@ const useStyles = makeStyles(
|
|||
},
|
||||
rowActionSelected,
|
||||
cardContentRoot: {
|
||||
padding: "0 0 2.4rem 0",
|
||||
padding: "0",
|
||||
flex: 1,
|
||||
|
||||
"&:last-child": {
|
||||
padding: "0",
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -180,38 +191,53 @@ export const useFullScreenStyles = makeStyles<ReturnType<typeof useStyles>>(
|
|||
{ name: "Datagrid-fullscreen" },
|
||||
);
|
||||
|
||||
export function useDatagridTheme() {
|
||||
const theme = useTheme();
|
||||
export function useDatagridTheme(
|
||||
readonly?: boolean,
|
||||
hasHeaderClickable?: boolean,
|
||||
) {
|
||||
const { themeValues } = useTheme();
|
||||
|
||||
const datagridTheme = useMemo(
|
||||
(): Partial<Theme> => ({
|
||||
accentColor: theme.palette.secondary.main,
|
||||
accentLight: theme.palette.background.default,
|
||||
accentColor: themeValues.colors.background.interactiveBrandDefault,
|
||||
accentLight:
|
||||
themeValues.colors.background.interactiveBrandSecondaryPressing,
|
||||
accentFg: "transparent",
|
||||
bgCell: theme.palette.background.paper,
|
||||
bgHeader: theme.palette.background.paper,
|
||||
bgHeaderHasFocus: theme.palette.background.paper,
|
||||
bgHeaderHovered: theme.palette.background.paper,
|
||||
bgBubbleSelected: theme.palette.background.paper,
|
||||
textHeader: theme.palette.text.secondary,
|
||||
borderColor: theme.palette.divider,
|
||||
bgCell: themeValues.colors.background.plain,
|
||||
bgHeader: themeValues.colors.background.plain,
|
||||
bgHeaderHasFocus: themeValues.colors.background.plain,
|
||||
bgHeaderHovered: hasHeaderClickable
|
||||
? themeValues.colors.background.surfaceNeutralHighlight
|
||||
: themeValues.colors.background.plain,
|
||||
bgBubbleSelected: themeValues.colors.background.plain,
|
||||
textHeader: themeValues.colors.foreground.iconNeutralPlain,
|
||||
borderColor: themeValues.colors.border.neutralHighlight,
|
||||
fontFamily: "'Inter var', sans-serif",
|
||||
baseFontStyle: themes.defaultLight.fontSize.bodySmall,
|
||||
headerFontStyle: themes.defaultLight.fontSize.bodySmall,
|
||||
editorFontSize: themes.defaultLight.fontSize.bodySmall,
|
||||
textMedium: theme.palette.text.primary,
|
||||
textGroupHeader: theme.palette.text.secondary,
|
||||
textBubble: theme.palette.text.primary,
|
||||
textDark: theme.palette.text.primary,
|
||||
textLight: theme.palette.text.primary,
|
||||
baseFontStyle: themeValues.fontSize.bodySmall,
|
||||
headerFontStyle: themeValues.fontSize.captionSmall,
|
||||
editorFontSize: themeValues.fontSize.bodySmall,
|
||||
textMedium: themeValues.colors.background.interactiveNeutralDefault,
|
||||
textGroupHeader: themeValues.colors.foreground.iconNeutralPlain,
|
||||
textBubble: themeValues.colors.background.interactiveNeutralDefault,
|
||||
textDark: themeValues.colors.background.interactiveNeutralDefault,
|
||||
textLight: themeValues.colors.background.interactiveNeutralDefault,
|
||||
textHeaderSelected: themeValues.colors.foreground.textBrandDefault,
|
||||
cellHorizontalPadding: 8,
|
||||
cellVerticalPadding: 8,
|
||||
lineHeight: 20,
|
||||
}),
|
||||
[theme],
|
||||
[themeValues, hasHeaderClickable],
|
||||
);
|
||||
|
||||
return datagridTheme;
|
||||
const readonylDatagridTheme = useMemo(
|
||||
() => ({
|
||||
...datagridTheme,
|
||||
accentColor: themeValues.colors.background.surfaceBrandDepressed,
|
||||
accentLight: themeValues.colors.background.plain,
|
||||
}),
|
||||
[themeValues, datagridTheme],
|
||||
);
|
||||
return readonly ? readonylDatagridTheme : datagridTheme;
|
||||
}
|
||||
|
||||
export default useStyles;
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { Theme } from "@glideapps/glide-data-grid";
|
||||
|
||||
export interface AvailableColumn {
|
||||
id: string;
|
||||
title: string;
|
||||
width: number;
|
||||
group?: string;
|
||||
hasMenu?: boolean;
|
||||
icon?: string;
|
||||
themeOverride?: Partial<Theme>;
|
||||
}
|
||||
|
|
11
src/components/Datagrid/utils.ts
Normal file
11
src/components/Datagrid/utils.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { ColumnPickerProps } from "../ColumnPicker";
|
||||
|
||||
export const getDefultColumnPickerProps = (
|
||||
className: string,
|
||||
): Partial<ColumnPickerProps> => ({
|
||||
IconButtonProps: {
|
||||
className,
|
||||
variant: "ghost",
|
||||
hoverOutline: false,
|
||||
},
|
||||
});
|
|
@ -15,12 +15,14 @@ export interface FilterBarProps<TKeys extends string = string>
|
|||
SearchBarProps {
|
||||
errorMessages?: FilterErrorMessages<TKeys>;
|
||||
filterStructure: IFilter<TKeys>;
|
||||
withoutBorder?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
const useStyles = makeStyles<{ withoutBorder?: boolean }>(
|
||||
theme => ({
|
||||
root: {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
borderBottom: props =>
|
||||
props.withoutBorder ? "none" : `1px solid ${theme.palette.divider}`,
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
padding: theme.spacing(1, 4),
|
||||
|
|
|
@ -21,11 +21,8 @@ export const useExitFormDialog = (
|
|||
const id = useRef(formId || Symbol()).current;
|
||||
|
||||
const exitDialogProps = useContext(ExitFormDialogContext);
|
||||
const {
|
||||
setIsDirty,
|
||||
setIsSubmitDisabled,
|
||||
setExitDialogSubmitRef,
|
||||
} = exitDialogProps;
|
||||
const { setIsDirty, setIsSubmitDisabled, setExitDialogSubmitRef } =
|
||||
exitDialogProps;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isDisabled !== undefined) {
|
||||
|
|
|
@ -50,10 +50,10 @@ export const formatMoneyRange = (
|
|||
locale: string,
|
||||
) => {
|
||||
try {
|
||||
const formattedMoneyRange = (Intl.NumberFormat(locale, {
|
||||
const formattedMoneyRange = Intl.NumberFormat(locale, {
|
||||
style: "currency",
|
||||
currency: moneyFrom.currency,
|
||||
}) as any).formatRange(moneyFrom.amount, moneyTo.amount);
|
||||
}).formatRange(moneyFrom.amount, moneyTo.amount);
|
||||
// TODO: remove casting from formatRange when typescript
|
||||
// is updated to 4.7 or higher
|
||||
return formattedMoneyRange;
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from "react";
|
|||
import { useIntl } from "react-intl";
|
||||
|
||||
import { LocaleConsumer } from "../Locale";
|
||||
import { formatMoney, formatMoneyRange } from "../Money";
|
||||
import { getMoneyRange } from "./utils";
|
||||
|
||||
export interface MoneyRangeProps {
|
||||
from?: IMoney;
|
||||
|
@ -15,38 +15,7 @@ export const MoneyRange: React.FC<MoneyRangeProps> = ({ from, to }) => {
|
|||
|
||||
return (
|
||||
<LocaleConsumer>
|
||||
{({ locale }) => {
|
||||
if (from && to) {
|
||||
return from.amount === to.amount
|
||||
? formatMoney(from, locale)
|
||||
: formatMoneyRange(from, to, locale);
|
||||
}
|
||||
if (from && !to) {
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: "lW5uJO",
|
||||
defaultMessage: "from {money}",
|
||||
description: "money",
|
||||
},
|
||||
{
|
||||
money: formatMoney(from, locale),
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!from && to) {
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: "hptDxW",
|
||||
defaultMessage: "to {money}",
|
||||
description: "money",
|
||||
},
|
||||
{
|
||||
money: formatMoney(to, locale),
|
||||
},
|
||||
);
|
||||
}
|
||||
return "-";
|
||||
}}
|
||||
{({ locale }) => getMoneyRange(locale, intl, from, to)}
|
||||
</LocaleConsumer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export { default } from "./MoneyRange";
|
||||
export * from "./MoneyRange";
|
||||
export { getMoneyRange } from "./utils";
|
||||
|
|
85
src/components/MoneyRange/utils.test.ts
Normal file
85
src/components/MoneyRange/utils.test.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { IMoney } from "@dashboard/utils/intl";
|
||||
import { IntlShape } from "react-intl";
|
||||
|
||||
import { Locale } from "../Locale";
|
||||
import { getMoneyRange } from "./utils";
|
||||
|
||||
const intl = {
|
||||
formatMessage: ({ defaultMessage }, params) => {
|
||||
if (defaultMessage.includes("{money}")) {
|
||||
return defaultMessage.replace("{money}", params.money);
|
||||
}
|
||||
|
||||
return defaultMessage;
|
||||
},
|
||||
} as IntlShape;
|
||||
|
||||
describe("getMoneyRange", () => {
|
||||
it("should return - when no from and to values", () => {
|
||||
// Arrange & Act
|
||||
const result = getMoneyRange(Locale.PL, intl);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe("-");
|
||||
});
|
||||
|
||||
it("should return formated money with currency when from and to have same amount", () => {
|
||||
// Arrange
|
||||
const fromMoney: IMoney = {
|
||||
amount: 10,
|
||||
currency: "eur",
|
||||
};
|
||||
|
||||
const toMoney: IMoney = {
|
||||
amount: 10,
|
||||
currency: "eur",
|
||||
};
|
||||
// Act & Assert
|
||||
|
||||
expect(getMoneyRange(Locale.EN, intl, fromMoney, toMoney)).toBe("€10.00");
|
||||
});
|
||||
|
||||
it("should return formated money range when from and to have different amount", () => {
|
||||
// Arrange
|
||||
const fromMoney: IMoney = {
|
||||
amount: 10,
|
||||
currency: "eur",
|
||||
};
|
||||
|
||||
const toMoney: IMoney = {
|
||||
amount: 20,
|
||||
currency: "eur",
|
||||
};
|
||||
// Act & Assert
|
||||
|
||||
expect(getMoneyRange(Locale.EN, intl, fromMoney, toMoney)).toBe(
|
||||
"€10.00 - €20.00",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return formated money when only from is provided", () => {
|
||||
// Arrange
|
||||
const fromMoney: IMoney = {
|
||||
amount: 10,
|
||||
currency: "eur",
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
|
||||
expect(getMoneyRange(Locale.EN, intl, fromMoney)).toBe("from €10.00");
|
||||
});
|
||||
|
||||
it("should return formated money when only to is provided", () => {
|
||||
// Arrange
|
||||
const toMoney: IMoney = {
|
||||
amount: 10,
|
||||
currency: "eur",
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
|
||||
expect(getMoneyRange(Locale.EN, intl, undefined, toMoney)).toBe(
|
||||
"to €10.00",
|
||||
);
|
||||
});
|
||||
});
|
43
src/components/MoneyRange/utils.ts
Normal file
43
src/components/MoneyRange/utils.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { IMoney } from "@dashboard/utils/intl";
|
||||
import { IntlShape } from "react-intl";
|
||||
|
||||
import { Locale } from "../Locale";
|
||||
import { formatMoney, formatMoneyRange } from "../Money";
|
||||
|
||||
export const getMoneyRange = (
|
||||
locale: Locale,
|
||||
intl: IntlShape,
|
||||
from?: IMoney,
|
||||
to?: IMoney,
|
||||
): string => {
|
||||
if (from && to) {
|
||||
return from.amount === to.amount
|
||||
? formatMoney(from, locale)
|
||||
: formatMoneyRange(from, to, locale);
|
||||
}
|
||||
if (from && !to) {
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: "lW5uJO",
|
||||
defaultMessage: "from {money}",
|
||||
description: "money",
|
||||
},
|
||||
{
|
||||
money: formatMoney(from, locale),
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!from && to) {
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: "hptDxW",
|
||||
defaultMessage: "to {money}",
|
||||
description: "money",
|
||||
},
|
||||
{
|
||||
money: formatMoney(to, locale),
|
||||
},
|
||||
);
|
||||
}
|
||||
return "-";
|
||||
};
|
26
src/components/StatusDot/StatusDot.tsx
Normal file
26
src/components/StatusDot/StatusDot.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Box, Sprinkles } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
export interface StatusDotProps {
|
||||
status: "default" | "error";
|
||||
}
|
||||
|
||||
const getStatusColor = (
|
||||
status: StatusDotProps["status"],
|
||||
): Sprinkles["backgroundColor"] => {
|
||||
switch (status) {
|
||||
case "error":
|
||||
return "interactiveCriticalDefault";
|
||||
default:
|
||||
return "decorativeSurfacePlain2";
|
||||
}
|
||||
};
|
||||
|
||||
export const StatusDot: React.FC<StatusDotProps> = ({ status }) => (
|
||||
<Box
|
||||
width={4}
|
||||
height={4}
|
||||
borderRadius="50%"
|
||||
backgroundColor={getStatusColor(status)}
|
||||
/>
|
||||
);
|
|
@ -3,6 +3,7 @@ import { TableCell } from "@material-ui/core";
|
|||
import {
|
||||
Pagination,
|
||||
PaginationProps as MacawPaginationProps,
|
||||
PaginationRowNumberSelectLabels,
|
||||
} from "@saleor/macaw-ui";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
@ -27,6 +28,7 @@ export interface PaginationProps
|
|||
prevHref?: string;
|
||||
nextHref?: string;
|
||||
disabled?: boolean;
|
||||
labels?: PaginationRowNumberSelectLabels;
|
||||
}
|
||||
export const TablePagination: React.FC<PaginationProps> = ({
|
||||
component,
|
||||
|
@ -38,6 +40,7 @@ export const TablePagination: React.FC<PaginationProps> = ({
|
|||
hasNextPage,
|
||||
hasPreviousPage,
|
||||
disabled,
|
||||
labels,
|
||||
...rest
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
@ -50,7 +53,8 @@ export const TablePagination: React.FC<PaginationProps> = ({
|
|||
hasNextPage={hasNextPage && !disabled}
|
||||
hasPreviousPage={hasPreviousPage && !disabled}
|
||||
labels={{
|
||||
noOfRows: intl.formatMessage(commonMessages.noOfRows),
|
||||
noOfRows:
|
||||
labels?.noOfRows ?? intl.formatMessage(commonMessages.noOfRows),
|
||||
}}
|
||||
rowNumber={settings?.rowNumber}
|
||||
onRowNumberUpdate={
|
||||
|
|
|
@ -6,4 +6,8 @@ export const commonTooltipMessages = defineMessages({
|
|||
defaultMessage:
|
||||
"Sorting by this column requires active filter: {filterName}",
|
||||
},
|
||||
noSortable: {
|
||||
defaultMessage: "Sorting by this column is not available",
|
||||
id: "AyQkmp",
|
||||
},
|
||||
});
|
||||
|
|
59
src/components/TopNavMenu/TopNavMenu.tsx
Normal file
59
src/components/TopNavMenu/TopNavMenu.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
ConfigurationIcon,
|
||||
Dropdown,
|
||||
List,
|
||||
Text,
|
||||
} from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
interface TopNavMenuItem {
|
||||
label: string;
|
||||
testId?: string;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
interface TopNavMenuProps {
|
||||
items: TopNavMenuItem[];
|
||||
dataTestId?: string;
|
||||
}
|
||||
|
||||
export const TopNavMenu: React.FC<TopNavMenuProps> = ({
|
||||
items,
|
||||
dataTestId,
|
||||
}) => (
|
||||
<Dropdown data-test-id={dataTestId}>
|
||||
<Dropdown.Trigger>
|
||||
<Button
|
||||
icon={<ConfigurationIcon />}
|
||||
variant="secondary"
|
||||
data-test-id="show-more-button"
|
||||
/>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Content align="end">
|
||||
<Box>
|
||||
<List
|
||||
padding={5}
|
||||
borderRadius={4}
|
||||
boxShadow="overlay"
|
||||
backgroundColor="surfaceNeutralPlain"
|
||||
>
|
||||
{items.map(item => (
|
||||
<Dropdown.Item key={item.label}>
|
||||
<List.Item
|
||||
borderRadius={4}
|
||||
paddingX={4}
|
||||
paddingY={5}
|
||||
onClick={item.onSelect}
|
||||
data-test-id={item.testId}
|
||||
>
|
||||
<Text>{item.label}</Text>
|
||||
</List.Item>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Dropdown.Content>
|
||||
</Dropdown>
|
||||
);
|
1
src/components/TopNavMenu/index.ts
Normal file
1
src/components/TopNavMenu/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./TopNavMenu";
|
|
@ -33,6 +33,7 @@ export const VALUES_PAGINATE_BY = 10;
|
|||
|
||||
export type ProductListColumns =
|
||||
| "productType"
|
||||
| "description"
|
||||
| "availability"
|
||||
| "price"
|
||||
| "date";
|
||||
|
@ -92,7 +93,7 @@ export const defaultListSettings: AppListViewSettings = {
|
|||
rowNumber: PAGINATE_BY,
|
||||
},
|
||||
[ListViews.PRODUCT_LIST]: {
|
||||
columns: ["availability", "price", "productType", "date"],
|
||||
columns: ["availability", "description", "price", "productType", "date"],
|
||||
rowNumber: PAGINATE_BY,
|
||||
},
|
||||
[ListViews.SALES_LIST]: {
|
||||
|
|
|
@ -19,11 +19,11 @@ const attachVariablesLink = new ApolloLink((operation, forward) =>
|
|||
);
|
||||
|
||||
export const link = attachVariablesLink.concat(
|
||||
(createUploadLink({
|
||||
createUploadLink({
|
||||
credentials: "include",
|
||||
uri: getApiUrl(),
|
||||
fetch: createFetch(),
|
||||
}) as unknown) as ApolloLink, // type mismatch between apollo-upload-client and @apollo/cient
|
||||
}) as unknown as ApolloLink, // type mismatch between apollo-upload-client and @apollo/cient
|
||||
);
|
||||
|
||||
export const apolloClient = new ApolloClient({
|
||||
|
|
|
@ -12684,6 +12684,7 @@ export const ProductListDocument = gql`
|
|||
node {
|
||||
...ProductWithChannelListings
|
||||
updatedAt
|
||||
description
|
||||
attributes @include(if: $hasSelectedAttributes) {
|
||||
...ProductListAttribute
|
||||
}
|
||||
|
|
|
@ -9085,7 +9085,7 @@ export type ProductListQueryVariables = Exact<{
|
|||
}>;
|
||||
|
||||
|
||||
export type ProductListQuery = { __typename: 'Query', products: { __typename: 'ProductCountableConnection', totalCount: number | null, edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', updatedAt: any, id: string, name: string, attributes?: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string }, values: Array<{ __typename: 'AttributeValue', id: string, name: string | null, slug: string | null, reference: string | null, boolean: boolean | null, date: any | null, dateTime: any | null, value: string | null, file: { __typename: 'File', url: string, contentType: string | null } | null }> }>, thumbnail: { __typename: 'Image', url: string } | null, productType: { __typename: 'ProductType', id: string, name: string, hasVariants: boolean }, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, pricing?: { __typename: 'ProductPricingInfo', priceRange: { __typename: 'TaxedMoneyRange', start: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string } } | null, stop: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string } } | null } | null } | null, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } }>, pageInfo: { __typename: 'PageInfo', hasPreviousPage: boolean, hasNextPage: boolean, startCursor: string | null, endCursor: string | null } } | null };
|
||||
export type ProductListQuery = { __typename: 'Query', products: { __typename: 'ProductCountableConnection', totalCount: number | null, edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', updatedAt: any, description: any | null, id: string, name: string, attributes?: Array<{ __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string }, values: Array<{ __typename: 'AttributeValue', id: string, name: string | null, slug: string | null, reference: string | null, boolean: boolean | null, date: any | null, dateTime: any | null, value: string | null, file: { __typename: 'File', url: string, contentType: string | null } | null }> }>, thumbnail: { __typename: 'Image', url: string } | null, productType: { __typename: 'ProductType', id: string, name: string, hasVariants: boolean }, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, pricing?: { __typename: 'ProductPricingInfo', priceRange: { __typename: 'TaxedMoneyRange', start: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string } } | null, stop: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string } } | null } | null } | null, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } }>, pageInfo: { __typename: 'PageInfo', hasPreviousPage: boolean, hasNextPage: boolean, startCursor: string | null, endCursor: string | null } } | null };
|
||||
|
||||
export type ProductCountQueryVariables = Exact<{
|
||||
filter?: InputMaybe<ProductFilterInput>;
|
||||
|
|
|
@ -151,6 +151,10 @@ export const commonMessages = defineMessages({
|
|||
id: "RrCui3",
|
||||
defaultMessage: "Summary",
|
||||
},
|
||||
type: {
|
||||
defaultMessage: "Type",
|
||||
id: "+U6ozc",
|
||||
},
|
||||
translationAttributes: {
|
||||
id: "+xTpT1",
|
||||
defaultMessage: "Attributes",
|
||||
|
@ -203,6 +207,10 @@ export const commonMessages = defineMessages({
|
|||
defaultMessage: "No. of rows",
|
||||
description: "pagination",
|
||||
},
|
||||
product: {
|
||||
defaultMessage: "Product",
|
||||
id: "x/ZVlU",
|
||||
},
|
||||
});
|
||||
|
||||
export const errorMessages = defineMessages({
|
||||
|
|
|
@ -105,8 +105,7 @@ const props: OrderGrantRefundPageProps = {
|
|||
id: "RnVsZmlsbG1lbnRMaW5lOjIw",
|
||||
quantity: 1,
|
||||
orderLine: {
|
||||
id:
|
||||
"T3JkZXJMaW5lOjIxOTM0YTBjLWQ2NGQtNDdlMS05ZTc4LTJmMjIyYzU5NjQ5ZA==",
|
||||
id: "T3JkZXJMaW5lOjIxOTM0YTBjLWQ2NGQtNDdlMS05ZTc4LTJmMjIyYzU5NjQ5ZA==",
|
||||
thumbnail: {
|
||||
url: placeholderImage,
|
||||
__typename: "Image",
|
||||
|
@ -139,8 +138,7 @@ const props: OrderGrantRefundPageProps = {
|
|||
id: "RnVsZmlsbG1lbnRMaW5lOjIx",
|
||||
quantity: 1,
|
||||
orderLine: {
|
||||
id:
|
||||
"T3JkZXJMaW5lOjIxOTM0YTBjLWQ2NGQtNDdlMS05ZTc4LTJmMjIyYzU5NjQ5ZA==",
|
||||
id: "T3JkZXJMaW5lOjIxOTM0YTBjLWQ2NGQtNDdlMS05ZTc4LTJmMjIyYzU5NjQ5ZA==",
|
||||
thumbnail: {
|
||||
url: placeholderImage,
|
||||
__typename: "Image",
|
||||
|
@ -173,8 +171,7 @@ const props: OrderGrantRefundPageProps = {
|
|||
id: "RnVsZmlsbG1lbnRMaW5lOjIy",
|
||||
quantity: 2,
|
||||
orderLine: {
|
||||
id:
|
||||
"T3JkZXJMaW5lOmU0N2UyN2IyLTQzZWYtNGY5OS05ZTU0LTkyZmZkZWE5MmY4OA==",
|
||||
id: "T3JkZXJMaW5lOmU0N2UyN2IyLTQzZWYtNGY5OS05ZTU0LTkyZmZkZWE5MmY4OA==",
|
||||
thumbnail: {
|
||||
url: placeholderImage,
|
||||
__typename: "Image",
|
||||
|
@ -199,8 +196,7 @@ const props: OrderGrantRefundPageProps = {
|
|||
id: "RnVsZmlsbG1lbnRMaW5lOjIz",
|
||||
quantity: 1,
|
||||
orderLine: {
|
||||
id:
|
||||
"T3JkZXJMaW5lOmZjOWU1YzFlLWFiMDItNDU5MS05YTRmLTA5YmJiN2U0NmZlMg==",
|
||||
id: "T3JkZXJMaW5lOmZjOWU1YzFlLWFiMDItNDU5MS05YTRmLTA5YmJiN2U0NmZlMg==",
|
||||
thumbnail: {
|
||||
url: placeholderImage,
|
||||
__typename: "Image",
|
||||
|
|
|
@ -37,18 +37,18 @@ export const ProductsCard: React.FC<ProductsCardProps> = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
const getHandleAmountChange = (line: OrderLineGrantRefundFragment) => (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const parsedValue = parseInt(e.target.value, 10);
|
||||
const value = Number.isNaN(parsedValue) ? 0 : parsedValue;
|
||||
const getHandleAmountChange =
|
||||
(line: OrderLineGrantRefundFragment) =>
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const parsedValue = parseInt(e.target.value, 10);
|
||||
const value = Number.isNaN(parsedValue) ? 0 : parsedValue;
|
||||
|
||||
dispatch({
|
||||
type: "setQuantity",
|
||||
lineId: line.id,
|
||||
amount: value,
|
||||
});
|
||||
};
|
||||
dispatch({
|
||||
type: "setQuantity",
|
||||
lineId: line.id,
|
||||
amount: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSetMaxQuanity = () => {
|
||||
dispatch({
|
||||
|
|
|
@ -36,10 +36,9 @@ type OrderManualTransactionDialogProps = {
|
|||
dialogProps: DialogProps;
|
||||
} & OrderManualTransactionFormProps;
|
||||
|
||||
export const OrderManualTransactionDialog: React.FC<OrderManualTransactionDialogProps> = ({
|
||||
dialogProps,
|
||||
...props
|
||||
}) => {
|
||||
export const OrderManualTransactionDialog: React.FC<
|
||||
OrderManualTransactionDialogProps
|
||||
> = ({ dialogProps, ...props }) => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles();
|
||||
|
||||
|
|
|
@ -18,10 +18,9 @@ export interface OrderManualTransactionFormProps {
|
|||
initialData?: Partial<OrderManualTransactionSubmitVariables>;
|
||||
}
|
||||
|
||||
export const OrderManualTransactionForm: React.FC<OrderManualTransactionFormProps> = ({
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
export const OrderManualTransactionForm: React.FC<
|
||||
OrderManualTransactionFormProps
|
||||
> = ({ children, ...props }) => {
|
||||
const { submitState, initialData } = props;
|
||||
const hookData = useManualRefund({ submitState, initialData });
|
||||
|
||||
|
|
|
@ -3,15 +3,11 @@ import React from "react";
|
|||
|
||||
import { useManualTransactionContext } from "../context";
|
||||
|
||||
export const DescriptionField: React.FC<Omit<
|
||||
TextFieldProps,
|
||||
"onChange" | "value"
|
||||
>> = ({ disabled, ...props }) => {
|
||||
const {
|
||||
submitState,
|
||||
handleChangeDescription,
|
||||
description,
|
||||
} = useManualTransactionContext();
|
||||
export const DescriptionField: React.FC<
|
||||
Omit<TextFieldProps, "onChange" | "value">
|
||||
> = ({ disabled, ...props }) => {
|
||||
const { submitState, handleChangeDescription, description } =
|
||||
useManualTransactionContext();
|
||||
|
||||
return (
|
||||
<TextField
|
||||
|
|
|
@ -6,12 +6,8 @@ export const Form: React.FC<React.HTMLProps<HTMLFormElement>> = ({
|
|||
children,
|
||||
...props
|
||||
}) => {
|
||||
const {
|
||||
amount,
|
||||
description,
|
||||
pspReference,
|
||||
onAddTransaction,
|
||||
} = useManualTransactionContext();
|
||||
const { amount, description, pspReference, onAddTransaction } =
|
||||
useManualTransactionContext();
|
||||
|
||||
return (
|
||||
<form
|
||||
|
|
|
@ -3,16 +3,11 @@ import React from "react";
|
|||
|
||||
import { useManualTransactionContext } from "../context";
|
||||
|
||||
export const PriceInputField: React.FC<Omit<
|
||||
PriceFieldProps,
|
||||
"currencySymbol" | "onChange" | "value"
|
||||
>> = ({ disabled, ...props }) => {
|
||||
const {
|
||||
currency,
|
||||
submitState,
|
||||
handleChangeAmount,
|
||||
amount,
|
||||
} = useManualTransactionContext();
|
||||
export const PriceInputField: React.FC<
|
||||
Omit<PriceFieldProps, "currencySymbol" | "onChange" | "value">
|
||||
> = ({ disabled, ...props }) => {
|
||||
const { currency, submitState, handleChangeAmount, amount } =
|
||||
useManualTransactionContext();
|
||||
|
||||
return (
|
||||
<PriceField
|
||||
|
|
|
@ -3,15 +3,11 @@ import React from "react";
|
|||
|
||||
import { useManualTransactionContext } from "../context";
|
||||
|
||||
export const PspReferenceField: React.FC<Omit<
|
||||
TextFieldProps,
|
||||
"onChange" | "value"
|
||||
>> = ({ disabled, variant = "outlined", ...props }) => {
|
||||
const {
|
||||
submitState,
|
||||
pspReference,
|
||||
handleChangePspReference,
|
||||
} = useManualTransactionContext();
|
||||
export const PspReferenceField: React.FC<
|
||||
Omit<TextFieldProps, "onChange" | "value">
|
||||
> = ({ disabled, variant = "outlined", ...props }) => {
|
||||
const { submitState, pspReference, handleChangePspReference } =
|
||||
useManualTransactionContext();
|
||||
|
||||
return (
|
||||
<TextField
|
||||
|
|
|
@ -5,10 +5,9 @@ import React from "react";
|
|||
|
||||
import { useManualTransactionContext } from "../context";
|
||||
|
||||
export const SubmitButton: React.FC<Omit<
|
||||
ConfirmButtonProps,
|
||||
"type" | "transitionState"
|
||||
>> = ({ disabled, ...props }) => {
|
||||
export const SubmitButton: React.FC<
|
||||
Omit<ConfirmButtonProps, "type" | "transitionState">
|
||||
> = ({ disabled, ...props }) => {
|
||||
const { submitState, amount } = useManualTransactionContext();
|
||||
|
||||
return (
|
||||
|
|
|
@ -35,7 +35,9 @@ export const useManualRefund = ({
|
|||
}
|
||||
}, [submitState]);
|
||||
|
||||
const handleChangeDescription: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
const handleChangeDescription: React.ChangeEventHandler<
|
||||
HTMLInputElement
|
||||
> = e => {
|
||||
setDescription(e.target.value);
|
||||
};
|
||||
|
||||
|
@ -48,7 +50,9 @@ export const useManualRefund = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleChangePspReference: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
const handleChangePspReference: React.ChangeEventHandler<
|
||||
HTMLInputElement
|
||||
> = e => {
|
||||
setPspReference(e.target.value);
|
||||
};
|
||||
|
||||
|
|
|
@ -23,7 +23,8 @@ interface OrderManualTransactionCombinedComponent {
|
|||
PspReferenceField: typeof PspReferenceField;
|
||||
}
|
||||
|
||||
export const OrderManualTransactionForm = MainComponent as OrderManualTransactionCombinedComponent;
|
||||
export const OrderManualTransactionForm =
|
||||
MainComponent as OrderManualTransactionCombinedComponent;
|
||||
OrderManualTransactionForm.DescriptionField = DescriptionField;
|
||||
OrderManualTransactionForm.ErrorText = ErrorText;
|
||||
OrderManualTransactionForm.Form = Form;
|
||||
|
|
|
@ -59,9 +59,9 @@ export const ItemsCard: React.FC<OrderReturnRefundLinesCardProps> = ({
|
|||
const classes = useItemCardStyles({});
|
||||
const intl = useIntl();
|
||||
|
||||
const handleChangeQuantity = (id: string) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => onChangeQuantity(id, parseInt(event.target.value, 10));
|
||||
const handleChangeQuantity =
|
||||
(id: string) => (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChangeQuantity(id, parseInt(event.target.value, 10));
|
||||
|
||||
const fulfillment = order?.fulfillments.find(getById(fulfilmentId));
|
||||
|
||||
|
|
|
@ -11,7 +11,9 @@ import {
|
|||
import { manualRefundMessages, refundPageMessages } from "../messages";
|
||||
import { useManualRefundCardStyles } from "../styles";
|
||||
|
||||
export const ManualRefundCard: React.FC<OrderManualTransactionFormProps> = props => {
|
||||
export const ManualRefundCard: React.FC<
|
||||
OrderManualTransactionFormProps
|
||||
> = props => {
|
||||
const classes = useManualRefundCardStyles();
|
||||
const intl = useIntl();
|
||||
|
||||
|
|
|
@ -22,13 +22,9 @@ export interface OrderTransactionActionDialogProps extends DialogProps {
|
|||
action: TransactionActionEnum;
|
||||
}
|
||||
|
||||
export const OrderTransactionActionDialog: React.FC<OrderTransactionActionDialogProps> = ({
|
||||
confirmButtonState,
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
action,
|
||||
}) => {
|
||||
export const OrderTransactionActionDialog: React.FC<
|
||||
OrderTransactionActionDialogProps
|
||||
> = ({ confirmButtonState, open, onClose, onSubmit, action }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const actionIntl = action
|
||||
|
|
|
@ -64,17 +64,15 @@ const OrderTransactionPayment: React.FC<OrderTransactionPaymentProps> = ({
|
|||
__typename: "FakeTransaction",
|
||||
};
|
||||
|
||||
const handleTransactionAction: OrderTransactionProps["onTransactionAction"] = (
|
||||
_,
|
||||
action,
|
||||
) => {
|
||||
if (action === TransactionActionEnum.CHARGE) {
|
||||
onCapture();
|
||||
}
|
||||
if (action === TransactionActionEnum.VOID) {
|
||||
onVoid();
|
||||
}
|
||||
};
|
||||
const handleTransactionAction: OrderTransactionProps["onTransactionAction"] =
|
||||
(_, action) => {
|
||||
if (action === TransactionActionEnum.CHARGE) {
|
||||
onCapture();
|
||||
}
|
||||
if (action === TransactionActionEnum.VOID) {
|
||||
onVoid();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<OrderTransaction
|
||||
|
|
|
@ -1,463 +0,0 @@
|
|||
import { ChannelsAvailabilityDropdown } from "@dashboard/components/ChannelsAvailabilityDropdown";
|
||||
import {
|
||||
getChannelAvailabilityColor,
|
||||
getChannelAvailabilityLabel,
|
||||
} from "@dashboard/components/ChannelsAvailabilityDropdown/utils";
|
||||
import Checkbox from "@dashboard/components/Checkbox";
|
||||
import Date from "@dashboard/components/Date";
|
||||
import MoneyRange from "@dashboard/components/MoneyRange";
|
||||
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
|
||||
import Skeleton from "@dashboard/components/Skeleton";
|
||||
import TableCellAvatar from "@dashboard/components/TableCellAvatar";
|
||||
import TableCellHeader from "@dashboard/components/TableCellHeader";
|
||||
import TableHead from "@dashboard/components/TableHead";
|
||||
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
|
||||
import TableRowLink from "@dashboard/components/TableRowLink";
|
||||
import TooltipTableCellHeader from "@dashboard/components/TooltipTableCellHeader";
|
||||
import { commonTooltipMessages } from "@dashboard/components/TooltipTableCellHeader/messages";
|
||||
import { ProductListColumns } from "@dashboard/config";
|
||||
import { GridAttributesQuery, ProductListQuery } from "@dashboard/graphql";
|
||||
import { maybe, renderCollection } from "@dashboard/misc";
|
||||
import {
|
||||
getAttributeIdFromColumnValue,
|
||||
isAttributeColumnValue,
|
||||
} from "@dashboard/products/components/ProductListPage/utils";
|
||||
import { ProductListUrlSortField, productUrl } from "@dashboard/products/urls";
|
||||
import { canBeSorted } from "@dashboard/products/views/ProductList/sort";
|
||||
import {
|
||||
ChannelProps,
|
||||
ListActions,
|
||||
ListProps,
|
||||
RelayToFlat,
|
||||
SortPage,
|
||||
} from "@dashboard/types";
|
||||
import TDisplayColumn, {
|
||||
DisplayColumnProps,
|
||||
} from "@dashboard/utils/columns/DisplayColumn";
|
||||
import { getArrowDirection } from "@dashboard/utils/sort";
|
||||
import { TableBody, TableCell, TableFooter } from "@material-ui/core";
|
||||
import { makeStyles, Pill } from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import { columnsMessages } from "./messages";
|
||||
import ProductListAttribute from "./ProductListAttribute";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
[theme.breakpoints.up("md")]: {
|
||||
colName: {
|
||||
minWidth: 300,
|
||||
},
|
||||
colPrice: {
|
||||
width: 300,
|
||||
},
|
||||
colPublished: {
|
||||
width: 200,
|
||||
},
|
||||
colType: {
|
||||
width: 300,
|
||||
},
|
||||
colDate: {
|
||||
width: 200,
|
||||
},
|
||||
},
|
||||
colAttribute: {
|
||||
width: 200,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
colFill: {
|
||||
padding: 0,
|
||||
width: "100%",
|
||||
},
|
||||
colName: {
|
||||
wordBreak: "break-all",
|
||||
paddingLeft: 0,
|
||||
"&$colNameFixed": {
|
||||
width: 300,
|
||||
},
|
||||
},
|
||||
colAvatar: {
|
||||
wordBreak: "break-all",
|
||||
},
|
||||
colNameFixed: {},
|
||||
colNameWrapper: {
|
||||
display: "block",
|
||||
},
|
||||
colPrice: {
|
||||
textAlign: "right",
|
||||
},
|
||||
colPublished: {},
|
||||
colType: {
|
||||
wordBreak: "break-all",
|
||||
},
|
||||
link: {
|
||||
cursor: "pointer",
|
||||
},
|
||||
table: {
|
||||
tableLayout: "fixed",
|
||||
},
|
||||
tableContainer: {
|
||||
overflowX: "scroll",
|
||||
},
|
||||
textLeft: {
|
||||
textAlign: "left",
|
||||
},
|
||||
textRight: {
|
||||
textAlign: "right",
|
||||
},
|
||||
}),
|
||||
{ name: "ProductList" },
|
||||
);
|
||||
|
||||
const DisplayColumn = TDisplayColumn as React.FunctionComponent<
|
||||
DisplayColumnProps<ProductListColumns>
|
||||
>;
|
||||
|
||||
interface ProductListProps
|
||||
extends ListProps<ProductListColumns>,
|
||||
ListActions,
|
||||
SortPage<ProductListUrlSortField>,
|
||||
ChannelProps {
|
||||
activeAttributeSortId: string;
|
||||
gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>;
|
||||
products: RelayToFlat<ProductListQuery["products"]>;
|
||||
}
|
||||
|
||||
export const ProductList: React.FC<ProductListProps> = props => {
|
||||
const {
|
||||
activeAttributeSortId,
|
||||
settings,
|
||||
disabled,
|
||||
isChecked,
|
||||
gridAttributes,
|
||||
products,
|
||||
selected,
|
||||
sort,
|
||||
toggle,
|
||||
toggleAll,
|
||||
toolbar,
|
||||
onUpdateListSettings,
|
||||
onSort,
|
||||
selectedChannelId,
|
||||
filterDependency,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
const gridAttributesFromSettings = settings.columns.filter(
|
||||
isAttributeColumnValue,
|
||||
);
|
||||
const numberOfColumns =
|
||||
(products?.length === 0 ? 1 : 2) + settings.columns.length;
|
||||
|
||||
return (
|
||||
<div className={classes.tableContainer}>
|
||||
<ResponsiveTable className={classes.table}>
|
||||
<colgroup>
|
||||
{products?.length !== 0 && <col />}
|
||||
<col className={classes.colName} />
|
||||
<DisplayColumn column="productType" displayColumns={settings.columns}>
|
||||
<col className={classes.colType} />
|
||||
</DisplayColumn>
|
||||
<DisplayColumn
|
||||
column="availability"
|
||||
displayColumns={settings.columns}
|
||||
>
|
||||
<col className={classes.colPublished} />
|
||||
</DisplayColumn>
|
||||
{gridAttributesFromSettings.map(gridAttribute => (
|
||||
<col className={classes.colAttribute} key={gridAttribute} />
|
||||
))}
|
||||
<DisplayColumn column="date" displayColumns={settings.columns}>
|
||||
<col className={classes.colDate} />
|
||||
</DisplayColumn>
|
||||
<DisplayColumn column="price" displayColumns={settings.columns}>
|
||||
<col className={classes.colPrice} />
|
||||
</DisplayColumn>
|
||||
</colgroup>
|
||||
<TableHead
|
||||
colSpan={numberOfColumns}
|
||||
selected={selected}
|
||||
disabled={disabled}
|
||||
items={products}
|
||||
toggleAll={toggleAll}
|
||||
toolbar={toolbar}
|
||||
>
|
||||
<TableCellHeader
|
||||
data-test-id="col-name-header"
|
||||
arrowPosition="right"
|
||||
className={clsx(classes.colName, {
|
||||
[classes.colNameFixed]: settings.columns.length > 4,
|
||||
})}
|
||||
direction={
|
||||
sort.sort === ProductListUrlSortField.name
|
||||
? getArrowDirection(sort.asc)
|
||||
: undefined
|
||||
}
|
||||
onClick={() => onSort(ProductListUrlSortField.name)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="VQLIXd"
|
||||
defaultMessage="Name"
|
||||
description="product"
|
||||
/>
|
||||
</TableCellHeader>
|
||||
<DisplayColumn column="productType" displayColumns={settings.columns}>
|
||||
<TableCellHeader
|
||||
data-test-id="col-type-header"
|
||||
className={classes.colType}
|
||||
direction={
|
||||
sort.sort === ProductListUrlSortField.productType
|
||||
? getArrowDirection(sort.asc)
|
||||
: undefined
|
||||
}
|
||||
onClick={() => onSort(ProductListUrlSortField.productType)}
|
||||
>
|
||||
<FormattedMessage {...columnsMessages.type} />
|
||||
</TableCellHeader>
|
||||
</DisplayColumn>
|
||||
<DisplayColumn
|
||||
column="availability"
|
||||
displayColumns={settings.columns}
|
||||
>
|
||||
<TooltipTableCellHeader
|
||||
data-test-id="col-availability-header"
|
||||
className={classes.colPublished}
|
||||
direction={
|
||||
sort.sort === ProductListUrlSortField.status
|
||||
? getArrowDirection(sort.asc)
|
||||
: undefined
|
||||
}
|
||||
onClick={() => onSort(ProductListUrlSortField.status)}
|
||||
disabled={
|
||||
!canBeSorted(
|
||||
ProductListUrlSortField.status,
|
||||
!!selectedChannelId,
|
||||
)
|
||||
}
|
||||
tooltip={intl.formatMessage(
|
||||
commonTooltipMessages.noFilterSelected,
|
||||
{ filterName: filterDependency.label },
|
||||
)}
|
||||
>
|
||||
<FormattedMessage {...columnsMessages.availability} />
|
||||
</TooltipTableCellHeader>
|
||||
</DisplayColumn>
|
||||
{gridAttributesFromSettings.map(gridAttributeFromSettings => {
|
||||
const attributeId = getAttributeIdFromColumnValue(
|
||||
gridAttributeFromSettings,
|
||||
);
|
||||
|
||||
return (
|
||||
<TableCellHeader
|
||||
className={classes.colAttribute}
|
||||
direction={
|
||||
sort.sort === ProductListUrlSortField.attribute &&
|
||||
attributeId === activeAttributeSortId
|
||||
? getArrowDirection(sort.asc)
|
||||
: undefined
|
||||
}
|
||||
onClick={() =>
|
||||
onSort(ProductListUrlSortField.attribute, attributeId)
|
||||
}
|
||||
key={gridAttributeFromSettings}
|
||||
>
|
||||
{maybe<React.ReactNode>(
|
||||
() =>
|
||||
gridAttributes.find(
|
||||
gridAttribute => attributeId === gridAttribute.id,
|
||||
).name,
|
||||
<Skeleton />,
|
||||
)}
|
||||
</TableCellHeader>
|
||||
);
|
||||
})}
|
||||
<DisplayColumn column="date" displayColumns={settings.columns}>
|
||||
<TableCellHeader
|
||||
data-test-id="col-date-header"
|
||||
className={classes.colDate}
|
||||
direction={
|
||||
sort.sort === ProductListUrlSortField.date
|
||||
? getArrowDirection(sort.asc)
|
||||
: undefined
|
||||
}
|
||||
onClick={() => onSort(ProductListUrlSortField.date)}
|
||||
>
|
||||
<FormattedMessage {...columnsMessages.updatedAt} />
|
||||
</TableCellHeader>
|
||||
</DisplayColumn>
|
||||
<DisplayColumn column="price" displayColumns={settings.columns}>
|
||||
<TooltipTableCellHeader
|
||||
data-test-id="col-price-header"
|
||||
className={classes.colPrice}
|
||||
direction={
|
||||
sort.sort === ProductListUrlSortField.price
|
||||
? getArrowDirection(sort.asc)
|
||||
: undefined
|
||||
}
|
||||
textAlign="right"
|
||||
onClick={() => onSort(ProductListUrlSortField.price)}
|
||||
disabled={
|
||||
!canBeSorted(ProductListUrlSortField.price, !!selectedChannelId)
|
||||
}
|
||||
tooltip={intl.formatMessage(
|
||||
commonTooltipMessages.noFilterSelected,
|
||||
{ filterName: filterDependency.label },
|
||||
)}
|
||||
>
|
||||
<FormattedMessage {...columnsMessages.price} />
|
||||
</TooltipTableCellHeader>
|
||||
</DisplayColumn>
|
||||
</TableHead>
|
||||
<TableFooter>
|
||||
<TableRowLink>
|
||||
<TablePaginationWithContext
|
||||
colSpan={numberOfColumns}
|
||||
settings={settings}
|
||||
onUpdateListSettings={onUpdateListSettings}
|
||||
/>
|
||||
</TableRowLink>
|
||||
</TableFooter>
|
||||
<TableBody>
|
||||
{renderCollection(
|
||||
products,
|
||||
product => {
|
||||
const isSelected = product ? isChecked(product.id) : false;
|
||||
const channel = product?.channelListings.find(
|
||||
listing => listing.channel.id === selectedChannelId,
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRowLink
|
||||
selected={isSelected}
|
||||
hover={!!product}
|
||||
key={product ? product.id : "skeleton"}
|
||||
href={product && productUrl(product.id)}
|
||||
className={classes.link}
|
||||
data-test-id={"id-" + (product ? product?.id : "skeleton")}
|
||||
>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={disabled}
|
||||
disableClickPropagation
|
||||
onChange={() => toggle(product.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCellAvatar
|
||||
className={classes.colAvatar}
|
||||
thumbnail={maybe(() => product.thumbnail.url)}
|
||||
>
|
||||
{product?.name ? (
|
||||
<span data-test-id="name">{product.name}</span>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)}
|
||||
</TableCellAvatar>
|
||||
<DisplayColumn
|
||||
column="productType"
|
||||
displayColumns={settings.columns}
|
||||
>
|
||||
<TableCell
|
||||
className={classes.colType}
|
||||
data-test-id="product-type"
|
||||
>
|
||||
{product?.productType?.name || <Skeleton />}
|
||||
</TableCell>
|
||||
</DisplayColumn>
|
||||
<DisplayColumn
|
||||
column="availability"
|
||||
displayColumns={settings.columns}
|
||||
>
|
||||
<TableCell
|
||||
className={classes.colPublished}
|
||||
data-test-id="availability"
|
||||
data-test-availability={
|
||||
!!product?.channelListings?.length
|
||||
}
|
||||
>
|
||||
{(product &&
|
||||
(channel ? (
|
||||
<Pill
|
||||
label={intl.formatMessage(
|
||||
getChannelAvailabilityLabel(channel),
|
||||
)}
|
||||
color={getChannelAvailabilityColor(channel)}
|
||||
/>
|
||||
) : (
|
||||
<ChannelsAvailabilityDropdown
|
||||
channels={product?.channelListings}
|
||||
/>
|
||||
))) ?? <Skeleton />}
|
||||
</TableCell>
|
||||
</DisplayColumn>
|
||||
{gridAttributesFromSettings.map(gridAttribute => (
|
||||
<TableCell
|
||||
className={classes.colAttribute}
|
||||
key={gridAttribute}
|
||||
data-test-id="attribute"
|
||||
data-test-attribute={getAttributeIdFromColumnValue(
|
||||
gridAttribute,
|
||||
)}
|
||||
>
|
||||
<ProductListAttribute
|
||||
attribute={gridAttribute}
|
||||
productAttributes={product?.attributes}
|
||||
/>
|
||||
</TableCell>
|
||||
))}
|
||||
<DisplayColumn
|
||||
column="date"
|
||||
displayColumns={settings.columns}
|
||||
>
|
||||
<TableCell className={classes.colDate} data-test-id="date">
|
||||
{product?.updatedAt ? (
|
||||
<Date date={product.updatedAt} plain />
|
||||
) : (
|
||||
<Skeleton />
|
||||
)}
|
||||
</TableCell>
|
||||
</DisplayColumn>
|
||||
<DisplayColumn
|
||||
column="price"
|
||||
displayColumns={settings.columns}
|
||||
>
|
||||
<TableCell
|
||||
className={classes.colPrice}
|
||||
data-test-id="price"
|
||||
>
|
||||
{product?.channelListings ? (
|
||||
<MoneyRange
|
||||
from={channel?.pricing?.priceRange?.start?.net}
|
||||
to={channel?.pricing?.priceRange?.stop?.net}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)}
|
||||
</TableCell>
|
||||
</DisplayColumn>
|
||||
</TableRowLink>
|
||||
);
|
||||
},
|
||||
() => (
|
||||
<TableRowLink>
|
||||
<TableCell colSpan={numberOfColumns}>
|
||||
<FormattedMessage
|
||||
id="Q1Uzbb"
|
||||
defaultMessage="No products found"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
),
|
||||
)}
|
||||
</TableBody>
|
||||
</ResponsiveTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
ProductList.displayName = "ProductList";
|
||||
export default ProductList;
|
|
@ -1,44 +0,0 @@
|
|||
import Date, { DateTime } from "@dashboard/components/Date";
|
||||
import Skeleton from "@dashboard/components/Skeleton";
|
||||
import { ProductListAttributeFragment } from "@dashboard/graphql";
|
||||
import React from "react";
|
||||
|
||||
import { getAttributeIdFromColumnValue } from "../ProductListPage/utils";
|
||||
|
||||
export interface ProductListAttributeProps {
|
||||
attribute: string;
|
||||
productAttributes: ProductListAttributeFragment[];
|
||||
}
|
||||
|
||||
const ProductListAttribute: React.FC<ProductListAttributeProps> = ({
|
||||
attribute: gridAttribute,
|
||||
productAttributes,
|
||||
}) => {
|
||||
if (!productAttributes) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const productAttribute = productAttributes.find(
|
||||
attribute =>
|
||||
attribute.attribute.id === getAttributeIdFromColumnValue(gridAttribute),
|
||||
);
|
||||
if (productAttribute) {
|
||||
if (productAttribute.values.length) {
|
||||
if (productAttribute.values[0].date) {
|
||||
return <Date date={productAttribute.values[0].date} />;
|
||||
}
|
||||
if (productAttribute.values[0].dateTime) {
|
||||
return <DateTime date={productAttribute.values[0].dateTime} />;
|
||||
}
|
||||
}
|
||||
|
||||
const textValue = productAttribute.values
|
||||
.map(value => value.name)
|
||||
.join(", ");
|
||||
|
||||
return <span title={textValue}>{textValue}</span>;
|
||||
}
|
||||
return <span>-</span>;
|
||||
};
|
||||
|
||||
export default ProductListAttribute;
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./ProductList";
|
|
@ -0,0 +1,277 @@
|
|||
import ColumnPicker from "@dashboard/components/ColumnPicker";
|
||||
import Datagrid from "@dashboard/components/Datagrid/Datagrid";
|
||||
import {
|
||||
DatagridChangeStateContext,
|
||||
useDatagridChangeState,
|
||||
} from "@dashboard/components/Datagrid/hooks/useDatagridChange";
|
||||
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
|
||||
import { commonTooltipMessages } from "@dashboard/components/TooltipTableCellHeader/messages";
|
||||
import { ProductListColumns } from "@dashboard/config";
|
||||
import {
|
||||
GridAttributesQuery,
|
||||
ProductListQuery,
|
||||
SearchAvailableInGridAttributesQuery,
|
||||
} from "@dashboard/graphql";
|
||||
import useLocale from "@dashboard/hooks/useLocale";
|
||||
import { buttonMessages } from "@dashboard/intl";
|
||||
import { ProductListUrlSortField } from "@dashboard/products/urls";
|
||||
import { canBeSorted } from "@dashboard/products/views/ProductList/sort";
|
||||
import { useSearchProductTypes } from "@dashboard/searches/useProductTypeSearch";
|
||||
import {
|
||||
ChannelProps,
|
||||
FetchMoreProps,
|
||||
ListProps,
|
||||
PageListProps,
|
||||
RelayToFlat,
|
||||
SortPage,
|
||||
} from "@dashboard/types";
|
||||
import { addAtIndex, removeAtIndex } from "@dashboard/utils/lists";
|
||||
import { GridColumn, Item } from "@glideapps/glide-data-grid";
|
||||
import { Button } from "@saleor/macaw-ui";
|
||||
import { Box } from "@saleor/macaw-ui/next";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import { isAttributeColumnValue } from "../ProductListPage/utils";
|
||||
import { useColumnPickerColumns } from "./hooks/useColumnPickerColumns";
|
||||
import { useDatagridColumns } from "./hooks/useDatagridColumns";
|
||||
import { messages } from "./messages";
|
||||
import {
|
||||
createGetCellContent,
|
||||
getColumnMetadata,
|
||||
getProductRowsLength,
|
||||
} from "./utils";
|
||||
|
||||
interface ProductListDatagridProps
|
||||
extends ListProps<ProductListColumns>,
|
||||
PageListProps<ProductListColumns>,
|
||||
SortPage<ProductListUrlSortField>,
|
||||
FetchMoreProps,
|
||||
ChannelProps {
|
||||
activeAttributeSortId: string;
|
||||
gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>;
|
||||
products: RelayToFlat<ProductListQuery["products"]>;
|
||||
onRowClick: (id: string) => void;
|
||||
columnQuery: string;
|
||||
availableInGridAttributes: RelayToFlat<
|
||||
SearchAvailableInGridAttributesQuery["availableInGrid"]
|
||||
>;
|
||||
onColumnQueryChange: (query: string) => void;
|
||||
isAttributeLoading?: boolean;
|
||||
}
|
||||
|
||||
export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
|
||||
products,
|
||||
onRowClick,
|
||||
disabled,
|
||||
settings,
|
||||
onUpdateListSettings,
|
||||
selectedChannelId,
|
||||
onSort,
|
||||
sort,
|
||||
loading,
|
||||
gridAttributes,
|
||||
hasMore,
|
||||
isAttributeLoading,
|
||||
onFetchMore,
|
||||
columnQuery,
|
||||
defaultSettings,
|
||||
availableInGridAttributes,
|
||||
onColumnQueryChange,
|
||||
activeAttributeSortId,
|
||||
filterDependency,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const searchProductType = useSearchProductTypes();
|
||||
const datagrid = useDatagridChangeState();
|
||||
const { locale } = useLocale();
|
||||
const gridAttributesFromSettings = useMemo(
|
||||
() => settings.columns.filter(isAttributeColumnValue),
|
||||
[settings.columns],
|
||||
);
|
||||
|
||||
const { columns, setColumns } = useDatagridColumns({
|
||||
activeAttributeSortId,
|
||||
gridAttributes,
|
||||
gridAttributesFromSettings,
|
||||
settings,
|
||||
sort,
|
||||
});
|
||||
|
||||
const handleColumnMoved = useCallback(
|
||||
(startIndex: number, endIndex: number): void => {
|
||||
// Keep empty column always at beginning
|
||||
if (startIndex === 0) {
|
||||
return setColumns(prevColumns => [...prevColumns]);
|
||||
}
|
||||
|
||||
// Keep empty column always at beginning
|
||||
if (endIndex === 0) {
|
||||
return setColumns(old =>
|
||||
addAtIndex(
|
||||
old[startIndex],
|
||||
removeAtIndex(old, startIndex),
|
||||
endIndex + 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setColumns(old =>
|
||||
addAtIndex(old[startIndex], removeAtIndex(old, startIndex), endIndex),
|
||||
);
|
||||
},
|
||||
[setColumns],
|
||||
);
|
||||
|
||||
const handleColumnResize = useCallback(
|
||||
(column: GridColumn, newSize: number) => {
|
||||
if (column.id === "empty") {
|
||||
return;
|
||||
}
|
||||
|
||||
setColumns(prevColumns =>
|
||||
prevColumns.map(prevColumn =>
|
||||
prevColumn.id === column.id
|
||||
? { ...prevColumn, width: newSize }
|
||||
: prevColumn,
|
||||
),
|
||||
);
|
||||
},
|
||||
[setColumns],
|
||||
);
|
||||
|
||||
const columnPickerColumns = useColumnPickerColumns(
|
||||
gridAttributes,
|
||||
availableInGridAttributes,
|
||||
settings,
|
||||
defaultSettings.columns,
|
||||
);
|
||||
|
||||
const getCellContent = useMemo(
|
||||
() =>
|
||||
createGetCellContent({
|
||||
columns,
|
||||
products,
|
||||
intl,
|
||||
getProductTypes: searchProductType,
|
||||
locale,
|
||||
gridAttributes,
|
||||
gridAttributesFromSettings,
|
||||
selectedChannelId,
|
||||
loading,
|
||||
}),
|
||||
[
|
||||
columns,
|
||||
gridAttributes,
|
||||
gridAttributesFromSettings,
|
||||
intl,
|
||||
loading,
|
||||
locale,
|
||||
products,
|
||||
searchProductType,
|
||||
selectedChannelId,
|
||||
],
|
||||
);
|
||||
|
||||
const handleHeaderClicked = useCallback(
|
||||
(col: number) => {
|
||||
const { columnName, columnId } = getColumnMetadata(columns[col].id);
|
||||
|
||||
if (canBeSorted(columnName, !!selectedChannelId)) {
|
||||
onSort(columnName, columnId);
|
||||
}
|
||||
},
|
||||
[columns, onSort, selectedChannelId],
|
||||
);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
([_, row]: Item) => {
|
||||
const rowData = products[row];
|
||||
onRowClick(rowData.id);
|
||||
},
|
||||
[onRowClick, products],
|
||||
);
|
||||
|
||||
const handleGetColumnTooltipContent = useCallback(
|
||||
(colIndex: number): string => {
|
||||
const { columnName } = getColumnMetadata(columns[colIndex].id);
|
||||
// Sortable column or empty
|
||||
if (
|
||||
canBeSorted(columnName, !!selectedChannelId) ||
|
||||
columns[colIndex].id === "empty"
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
// No sortable column
|
||||
if (!Object.keys(ProductListUrlSortField).includes(columnName)) {
|
||||
return intl.formatMessage(commonTooltipMessages.noSortable);
|
||||
}
|
||||
|
||||
// Sortable but requrie selected channel
|
||||
return intl.formatMessage(commonTooltipMessages.noFilterSelected, {
|
||||
filterName: filterDependency.label,
|
||||
});
|
||||
},
|
||||
[columns, filterDependency.label, intl, selectedChannelId],
|
||||
);
|
||||
|
||||
const handleColumnChange = useCallback(
|
||||
(picked: ProductListColumns[]) => {
|
||||
onUpdateListSettings("columns", picked);
|
||||
},
|
||||
[onUpdateListSettings],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box __marginTop={-1}>
|
||||
<DatagridChangeStateContext.Provider value={datagrid}>
|
||||
<Datagrid
|
||||
readonly
|
||||
rowMarkers="none"
|
||||
columnSelect="single"
|
||||
freezeColumns={2}
|
||||
onColumnMoved={handleColumnMoved}
|
||||
onColumnResize={handleColumnResize}
|
||||
verticalBorder={col => (col > 1 ? true : false)}
|
||||
getColumnTooltipContent={handleGetColumnTooltipContent}
|
||||
availableColumns={columns}
|
||||
onHeaderClicked={handleHeaderClicked}
|
||||
emptyText={intl.formatMessage(messages.emptyText)}
|
||||
getCellContent={getCellContent}
|
||||
getCellError={() => false}
|
||||
menuItems={() => []}
|
||||
rows={getProductRowsLength(disabled, products)}
|
||||
selectionActions={(indexes, { removeRows }) => (
|
||||
<Button variant="tertiary" onClick={() => removeRows(indexes)}>
|
||||
<FormattedMessage {...buttonMessages.delete} />
|
||||
</Button>
|
||||
)}
|
||||
fullScreenTitle={intl.formatMessage(messages.products)}
|
||||
onRowClick={handleRowClick}
|
||||
renderColumnPicker={defaultProps => (
|
||||
<ColumnPicker
|
||||
{...defaultProps}
|
||||
{...columnPickerColumns}
|
||||
hasMore={hasMore}
|
||||
loading={isAttributeLoading}
|
||||
onFetchMore={onFetchMore}
|
||||
query={columnQuery}
|
||||
onQueryChange={onColumnQueryChange}
|
||||
onSave={handleColumnChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Box paddingX={9}>
|
||||
<TablePaginationWithContext
|
||||
component="div"
|
||||
colSpan={(products?.length === 0 ? 1 : 2) + settings.columns.length}
|
||||
settings={settings}
|
||||
disabled={disabled}
|
||||
onUpdateListSettings={onUpdateListSettings}
|
||||
/>
|
||||
</Box>
|
||||
</DatagridChangeStateContext.Provider>
|
||||
</Box>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,170 @@
|
|||
import { ProductListColumns } from "@dashboard/config";
|
||||
import {
|
||||
GridAttributesQuery,
|
||||
SearchAvailableInGridAttributesQuery,
|
||||
} from "@dashboard/graphql";
|
||||
import { ListSettings, RelayToFlat } from "@dashboard/types";
|
||||
import { renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import { useColumnPickerColumns } from "./useColumnPickerColumns";
|
||||
|
||||
jest.mock("react-intl", () => ({
|
||||
useIntl: jest.fn(() => ({
|
||||
formatMessage: jest.fn(x => x.defaultMessage),
|
||||
})),
|
||||
defineMessages: jest.fn(x => x),
|
||||
}));
|
||||
|
||||
describe("useColumnPickerColumns", () => {
|
||||
const staticColumns = [
|
||||
{
|
||||
label: "Availability",
|
||||
value: "availability" as ProductListColumns,
|
||||
},
|
||||
{
|
||||
label: "Price",
|
||||
value: "price" as ProductListColumns,
|
||||
},
|
||||
{
|
||||
label: "Description",
|
||||
value: "description" as ProductListColumns,
|
||||
},
|
||||
{
|
||||
label: "Type",
|
||||
value: "productType" as ProductListColumns,
|
||||
},
|
||||
{
|
||||
label: "Last updated",
|
||||
value: "date" as ProductListColumns,
|
||||
},
|
||||
];
|
||||
|
||||
it("should return static columns when attributes are empty and settings contains all columns", () => {
|
||||
// Arrange
|
||||
const settings = {
|
||||
columns: [
|
||||
"availability",
|
||||
"description",
|
||||
"price",
|
||||
"productType",
|
||||
"date",
|
||||
] as ListSettings<ProductListColumns>["columns"],
|
||||
rowNumber: 20,
|
||||
};
|
||||
|
||||
const defaultSettings = settings.columns;
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() =>
|
||||
useColumnPickerColumns([], [], settings, defaultSettings),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({
|
||||
initialColumns: [...staticColumns],
|
||||
availableColumns: [...staticColumns],
|
||||
defaultColumns: [...defaultSettings],
|
||||
});
|
||||
});
|
||||
|
||||
it("should return columns selected in settings", () => {
|
||||
// Arrange
|
||||
const settings = {
|
||||
columns: [
|
||||
"availability",
|
||||
"date",
|
||||
] as ListSettings<ProductListColumns>["columns"],
|
||||
rowNumber: 20,
|
||||
};
|
||||
|
||||
const defaultSettings = settings.columns;
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() =>
|
||||
useColumnPickerColumns([], [], settings, defaultSettings),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({
|
||||
initialColumns: [staticColumns[0], staticColumns[4]],
|
||||
availableColumns: [...staticColumns],
|
||||
defaultColumns: [...defaultSettings],
|
||||
});
|
||||
});
|
||||
|
||||
it("should return selected in setting with attributes", () => {
|
||||
// Arrange
|
||||
const settings = {
|
||||
columns: [
|
||||
"availability",
|
||||
"date",
|
||||
] as ListSettings<ProductListColumns>["columns"],
|
||||
rowNumber: 20,
|
||||
};
|
||||
const selectedAttibutes = [
|
||||
{
|
||||
__typename: "Attribute",
|
||||
id: "1",
|
||||
name: "Attr1",
|
||||
},
|
||||
{
|
||||
__typename: "Attribute",
|
||||
id: "2",
|
||||
name: "Attr2",
|
||||
},
|
||||
] as RelayToFlat<GridAttributesQuery["grid"]>;
|
||||
|
||||
const availableAttributesToSelect = [
|
||||
{
|
||||
__typename: "Attribute",
|
||||
id: "op1",
|
||||
name: "AttrOption1",
|
||||
},
|
||||
{
|
||||
__typename: "Attribute",
|
||||
id: "op2",
|
||||
name: "AttrOption2",
|
||||
},
|
||||
{
|
||||
__typename: "Attribute",
|
||||
id: "op3",
|
||||
name: "AttrOption3",
|
||||
},
|
||||
] as RelayToFlat<SearchAvailableInGridAttributesQuery["availableInGrid"]>;
|
||||
|
||||
const defaultSettings = settings.columns;
|
||||
|
||||
// Act
|
||||
const { result } = renderHook(() =>
|
||||
useColumnPickerColumns(
|
||||
selectedAttibutes,
|
||||
availableAttributesToSelect,
|
||||
settings,
|
||||
defaultSettings,
|
||||
),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({
|
||||
initialColumns: [
|
||||
staticColumns[0],
|
||||
staticColumns[4],
|
||||
{
|
||||
label: "Attr1",
|
||||
value: "attribute:1",
|
||||
},
|
||||
{ label: "Attr2", value: "attribute:2" },
|
||||
],
|
||||
availableColumns: [
|
||||
...staticColumns,
|
||||
{
|
||||
label: "AttrOption1",
|
||||
value: "attribute:op1",
|
||||
},
|
||||
{ label: "AttrOption2", value: "attribute:op2" },
|
||||
{ label: "AttrOption3", value: "attribute:op3" },
|
||||
],
|
||||
defaultColumns: [...defaultSettings],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
import { MultiAutocompleteChoiceType } from "@dashboard/components/MultiAutocompleteSelectField";
|
||||
import { ProductListColumns } from "@dashboard/config";
|
||||
import {
|
||||
GridAttributesQuery,
|
||||
SearchAvailableInGridAttributesQuery,
|
||||
} from "@dashboard/graphql";
|
||||
import { commonMessages } from "@dashboard/intl";
|
||||
import { ListSettings, RelayToFlat } from "@dashboard/types";
|
||||
import { useMemo } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { getAttributeColumnValue } from "../../ProductListPage/utils";
|
||||
import { columnsMessages } from "../messages";
|
||||
|
||||
export const useColumnPickerColumns = (
|
||||
gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>,
|
||||
availableInGridAttributes: RelayToFlat<
|
||||
SearchAvailableInGridAttributesQuery["availableInGrid"]
|
||||
>,
|
||||
settings: ListSettings<ProductListColumns>,
|
||||
defaultColumns: ProductListColumns[],
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const staticColumns = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: intl.formatMessage(columnsMessages.availability),
|
||||
value: "availability" as ProductListColumns,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(columnsMessages.price),
|
||||
value: "price" as ProductListColumns,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(commonMessages.description),
|
||||
value: "description" as ProductListColumns,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(columnsMessages.type),
|
||||
value: "productType" as ProductListColumns,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(columnsMessages.updatedAt),
|
||||
value: "date" as ProductListColumns,
|
||||
},
|
||||
],
|
||||
[intl],
|
||||
);
|
||||
|
||||
const initialColumns = useMemo(() => {
|
||||
const selectedStaticColumns = staticColumns.filter(column =>
|
||||
(settings.columns || []).includes(column.value),
|
||||
);
|
||||
const selectedAttributeColumns = gridAttributes.map(attribute => ({
|
||||
label: attribute.name,
|
||||
value: getAttributeColumnValue(attribute.id),
|
||||
}));
|
||||
|
||||
return [...selectedStaticColumns, ...selectedAttributeColumns];
|
||||
}, [gridAttributes, settings.columns, staticColumns]);
|
||||
|
||||
const availableColumns: MultiAutocompleteChoiceType[] = [
|
||||
...staticColumns,
|
||||
...availableInGridAttributes.map(
|
||||
attribute =>
|
||||
({
|
||||
label: attribute.name,
|
||||
value: getAttributeColumnValue(attribute.id),
|
||||
} as MultiAutocompleteChoiceType),
|
||||
),
|
||||
];
|
||||
|
||||
return {
|
||||
availableColumns,
|
||||
initialColumns,
|
||||
defaultColumns,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
import { useEmptyColumn } from "@dashboard/components/Datagrid/hooks/useEmptyColumn";
|
||||
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
|
||||
import { ProductListColumns } from "@dashboard/config";
|
||||
import { GridAttributesQuery } from "@dashboard/graphql";
|
||||
import { ProductListUrlSortField } from "@dashboard/products/urls";
|
||||
import { ListSettings, RelayToFlat, Sort } from "@dashboard/types";
|
||||
import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { getColumns, toAttributeColumnData } from "../utils";
|
||||
|
||||
interface UseDatagridColumnsProps {
|
||||
activeAttributeSortId: string;
|
||||
gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>;
|
||||
gridAttributesFromSettings: ProductListColumns[];
|
||||
sort: Sort<ProductListUrlSortField>;
|
||||
settings: ListSettings<ProductListColumns>;
|
||||
}
|
||||
|
||||
export const useDatagridColumns = ({
|
||||
sort,
|
||||
gridAttributes,
|
||||
gridAttributesFromSettings,
|
||||
activeAttributeSortId,
|
||||
settings,
|
||||
}: UseDatagridColumnsProps) => {
|
||||
const intl = useIntl();
|
||||
const emptyColumn = useEmptyColumn();
|
||||
|
||||
const initialColumns = useRef(
|
||||
getColumns({
|
||||
intl,
|
||||
sort,
|
||||
gridAttributes,
|
||||
gridAttributesFromSettings,
|
||||
activeAttributeSortId,
|
||||
emptyColumn,
|
||||
}),
|
||||
);
|
||||
|
||||
const [columns, setColumns] = useState<AvailableColumn[]>([
|
||||
initialColumns.current[0],
|
||||
initialColumns.current[1],
|
||||
...initialColumns.current.filter(col =>
|
||||
settings.columns.includes(col.id as ProductListColumns),
|
||||
),
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const attributeColumns = gridAttributesFromSettings.map(
|
||||
toAttributeColumnData(gridAttributes, activeAttributeSortId, sort),
|
||||
);
|
||||
|
||||
setColumns(prevColumns => [
|
||||
...prevColumns
|
||||
.filter(byColumnsInSettingsOrStaticColumns(settings))
|
||||
.map(toCurrentColumnData(sort, attributeColumns)),
|
||||
...settings.columns
|
||||
.filter(byNewAddedColumns(prevColumns))
|
||||
.map(
|
||||
toNewAddedColumData(
|
||||
[...initialColumns.current, ...attributeColumns],
|
||||
sort,
|
||||
),
|
||||
),
|
||||
]);
|
||||
}, [
|
||||
activeAttributeSortId,
|
||||
gridAttributes,
|
||||
gridAttributesFromSettings,
|
||||
settings,
|
||||
sort,
|
||||
]);
|
||||
|
||||
return { columns, setColumns };
|
||||
};
|
||||
|
||||
function byNewAddedColumns(currentColumns: AvailableColumn[]) {
|
||||
return (column: ProductListColumns) =>
|
||||
!currentColumns.find(c => c.id === column);
|
||||
}
|
||||
|
||||
function byColumnsInSettingsOrStaticColumns(
|
||||
settings: ListSettings<ProductListColumns>,
|
||||
) {
|
||||
return (column: AvailableColumn) =>
|
||||
settings.columns.includes(column.id as ProductListColumns) ||
|
||||
["empty", "name"].includes(column.id);
|
||||
}
|
||||
|
||||
function toCurrentColumnData(
|
||||
sort: Sort<ProductListUrlSortField>,
|
||||
attributeColumns: AvailableColumn[],
|
||||
) {
|
||||
return (column: AvailableColumn) => {
|
||||
// Take newest attibutes data from attributeColumns
|
||||
if (column.id.startsWith("attribute")) {
|
||||
return attributeColumns.find(ac => ac.id === column.id);
|
||||
}
|
||||
|
||||
return {
|
||||
...column,
|
||||
icon: getColumnSortDirectionIcon(sort, column.id),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function toNewAddedColumData(
|
||||
columnSource: AvailableColumn[],
|
||||
sort: Sort<ProductListUrlSortField>,
|
||||
) {
|
||||
return (column: ProductListColumns) => ({
|
||||
...columnSource.find(ac => ac.id === column),
|
||||
icon: getColumnSortDirectionIcon(sort, column as ProductListUrlSortField),
|
||||
});
|
||||
}
|
1
src/products/components/ProductListDatagrid/index.ts
Normal file
1
src/products/components/ProductListDatagrid/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./ProductListDatagrid";
|
|
@ -1,20 +1,21 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const messages = defineMessages({
|
||||
published: {
|
||||
id: "Sd0Ppm",
|
||||
defaultMessage: "Published on {date}",
|
||||
description: "product publication date",
|
||||
emptyText: {
|
||||
id: "Q1Uzbb",
|
||||
defaultMessage: "No products found",
|
||||
},
|
||||
unpublished: {
|
||||
id: "qpQ0uB",
|
||||
defaultMessage: "Unpublished",
|
||||
description: "product publication date",
|
||||
addProduct: {
|
||||
id: "tiY7bx",
|
||||
defaultMessage: "Add new product",
|
||||
},
|
||||
willBePublished: {
|
||||
id: "r0hgpM",
|
||||
defaultMessage: "Becomes published on {date}",
|
||||
description: "product publication date",
|
||||
editProduct: {
|
||||
defaultMessage: "Edit Product",
|
||||
id: "Q4m1CG",
|
||||
},
|
||||
products: {
|
||||
defaultMessage: "Products",
|
||||
id: "7NFfmz",
|
||||
},
|
||||
});
|
||||
|
377
src/products/components/ProductListDatagrid/utils.ts
Normal file
377
src/products/components/ProductListDatagrid/utils.ts
Normal file
|
@ -0,0 +1,377 @@
|
|||
import { messages } from "@dashboard/components/ChannelsAvailabilityDropdown/messages";
|
||||
import { getChannelAvailabilityLabel } from "@dashboard/components/ChannelsAvailabilityDropdown/utils";
|
||||
import {
|
||||
dropdownCell,
|
||||
loadingCell,
|
||||
readonlyTextCell,
|
||||
thumbnailCell,
|
||||
} from "@dashboard/components/Datagrid/customCells/cells";
|
||||
import {
|
||||
DropdownChoice,
|
||||
emptyDropdownCellValue,
|
||||
} from "@dashboard/components/Datagrid/customCells/DropdownCell";
|
||||
import { ThumbnailCellProps } from "@dashboard/components/Datagrid/customCells/ThumbnailCell";
|
||||
import { GetCellContentOpts } from "@dashboard/components/Datagrid/Datagrid";
|
||||
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
|
||||
import { Locale } from "@dashboard/components/Locale";
|
||||
import { getMoneyRange } from "@dashboard/components/MoneyRange";
|
||||
import { ProductListColumns } from "@dashboard/config";
|
||||
import { GridAttributesQuery, ProductListQuery } from "@dashboard/graphql";
|
||||
import { commonMessages } from "@dashboard/intl";
|
||||
import { ProductListUrlSortField } from "@dashboard/products/urls";
|
||||
import { RelayToFlat, Sort } from "@dashboard/types";
|
||||
import { Item } from "@glideapps/glide-data-grid";
|
||||
import moment from "moment-timezone";
|
||||
import { IntlShape } from "react-intl";
|
||||
|
||||
import { getAttributeIdFromColumnValue } from "../ProductListPage/utils";
|
||||
import { columnsMessages } from "./messages";
|
||||
|
||||
interface GetColumnsProps {
|
||||
intl: IntlShape;
|
||||
sort: Sort<ProductListUrlSortField>;
|
||||
gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>;
|
||||
gridAttributesFromSettings: ProductListColumns[];
|
||||
activeAttributeSortId: string;
|
||||
emptyColumn: AvailableColumn;
|
||||
}
|
||||
|
||||
export function getColumns({
|
||||
intl,
|
||||
sort,
|
||||
gridAttributes,
|
||||
gridAttributesFromSettings,
|
||||
activeAttributeSortId,
|
||||
emptyColumn,
|
||||
}: GetColumnsProps): AvailableColumn[] {
|
||||
return [
|
||||
emptyColumn,
|
||||
{
|
||||
id: "name",
|
||||
title: intl.formatMessage(commonMessages.product),
|
||||
width: 300,
|
||||
icon: getColumnSortIconName(sort, ProductListUrlSortField.name),
|
||||
},
|
||||
{
|
||||
id: "productType",
|
||||
title: intl.formatMessage(columnsMessages.type),
|
||||
width: 200,
|
||||
icon: getColumnSortIconName(sort, ProductListUrlSortField.productType),
|
||||
},
|
||||
{
|
||||
id: "description",
|
||||
title: intl.formatMessage(commonMessages.description),
|
||||
width: 400,
|
||||
},
|
||||
{
|
||||
id: "availability",
|
||||
title: intl.formatMessage(columnsMessages.availability),
|
||||
width: 250,
|
||||
icon: getColumnSortIconName(sort, ProductListUrlSortField.availability),
|
||||
},
|
||||
{
|
||||
id: "date",
|
||||
title: intl.formatMessage(columnsMessages.updatedAt),
|
||||
width: 250,
|
||||
icon: getColumnSortIconName(sort, ProductListUrlSortField.date),
|
||||
},
|
||||
{
|
||||
id: "price",
|
||||
title: intl.formatMessage(columnsMessages.price),
|
||||
width: 250,
|
||||
icon: getColumnSortIconName(sort, ProductListUrlSortField.price),
|
||||
},
|
||||
...gridAttributesFromSettings.map(
|
||||
toAttributeColumnData(gridAttributes, activeAttributeSortId, sort),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export function toAttributeColumnData(
|
||||
gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>,
|
||||
activeAttributeSortId: string,
|
||||
sort: Sort<ProductListUrlSortField>,
|
||||
) {
|
||||
return (attribute: ProductListColumns) => {
|
||||
const attributeId = getAttributeIdFromColumnValue(attribute);
|
||||
|
||||
const title =
|
||||
gridAttributes.find(gridAttribute => attributeId === gridAttribute.id)
|
||||
?.name ?? "";
|
||||
|
||||
return {
|
||||
id: attribute,
|
||||
title,
|
||||
width: 200,
|
||||
icon:
|
||||
attributeId === activeAttributeSortId &&
|
||||
getColumnSortIconName(sort, ProductListUrlSortField.attribute),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function getColumnSortIconName(
|
||||
{ sort, asc }: Sort<ProductListUrlSortField>,
|
||||
columnName: ProductListUrlSortField,
|
||||
) {
|
||||
if (columnName === sort) {
|
||||
if (asc) {
|
||||
return "arrowUp";
|
||||
} else {
|
||||
return "arrowDown";
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
interface GetCellContentProps {
|
||||
columns: AvailableColumn[];
|
||||
products: RelayToFlat<ProductListQuery["products"]>;
|
||||
intl: IntlShape;
|
||||
getProductTypes: (query: string) => Promise<DropdownChoice[]>;
|
||||
locale: Locale;
|
||||
gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>;
|
||||
gridAttributesFromSettings: ProductListColumns[];
|
||||
selectedChannelId?: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function createGetCellContent({
|
||||
columns,
|
||||
getProductTypes,
|
||||
intl,
|
||||
locale,
|
||||
products,
|
||||
selectedChannelId,
|
||||
loading,
|
||||
}: GetCellContentProps) {
|
||||
return (
|
||||
[column, row]: Item,
|
||||
{ changes, getChangeIndex, added, removed }: GetCellContentOpts,
|
||||
) => {
|
||||
if (column === -1) {
|
||||
return readonlyTextCell("");
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return loadingCell();
|
||||
}
|
||||
|
||||
const columnId = columns[column]?.id;
|
||||
|
||||
if (!columnId) {
|
||||
return readonlyTextCell("");
|
||||
}
|
||||
|
||||
const change = changes.current[getChangeIndex(columnId, row)]?.data;
|
||||
const rowData = added.includes(row)
|
||||
? undefined
|
||||
: products[row + removed.filter(r => r <= row).length];
|
||||
|
||||
const channel = rowData?.channelListings?.find(
|
||||
listing => listing.channel.id === selectedChannelId,
|
||||
);
|
||||
|
||||
switch (columnId) {
|
||||
case "productType":
|
||||
return getProductTypeCellContent(change, rowData, getProductTypes);
|
||||
case "availability":
|
||||
return getAvailabilityCellContent(rowData, intl, channel);
|
||||
|
||||
case "description":
|
||||
return getDescriptionCellContent(columnId, change, rowData);
|
||||
case "name":
|
||||
return getNameCellContent(change, rowData);
|
||||
case "price":
|
||||
return getPriceCellContent(intl, locale, channel);
|
||||
case "date":
|
||||
return getUpdatedAtrCellContent(rowData, locale);
|
||||
}
|
||||
|
||||
if (columnId.startsWith("attribute")) {
|
||||
return getAttributeCellContent(columnId, rowData);
|
||||
}
|
||||
|
||||
const value = change ?? rowData?.[columnId] ?? "";
|
||||
return readonlyTextCell(value || "");
|
||||
};
|
||||
}
|
||||
|
||||
function getProductTypeCellContent(
|
||||
change: { value: DropdownChoice },
|
||||
rowData: RelayToFlat<ProductListQuery["products"]>[number],
|
||||
getProductTypes: (query: string) => Promise<DropdownChoice[]>,
|
||||
) {
|
||||
const value = change?.value ?? getRowDataValue(rowData, change?.value);
|
||||
|
||||
return dropdownCell(
|
||||
value,
|
||||
{
|
||||
allowCustomValues: false,
|
||||
emptyOption: false,
|
||||
update: (text: string) =>
|
||||
getProductTypes(value.label !== text ? text : ""),
|
||||
},
|
||||
{
|
||||
cursor: "pointer",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function getRowDataValue(
|
||||
rowData?: RelayToFlat<ProductListQuery["products"]>[number],
|
||||
changeValue?: DropdownChoice,
|
||||
): DropdownChoice {
|
||||
if (changeValue === null || !rowData) {
|
||||
return emptyDropdownCellValue;
|
||||
}
|
||||
|
||||
return {
|
||||
label: rowData.productType?.name,
|
||||
value: rowData.productType?.id,
|
||||
};
|
||||
}
|
||||
|
||||
function getAvailabilityCellContent(
|
||||
rowData: RelayToFlat<ProductListQuery["products"]>[number],
|
||||
intl: IntlShape,
|
||||
selectedChannnel?: RelayToFlat<
|
||||
ProductListQuery["products"]
|
||||
>[number]["channelListings"][number],
|
||||
) {
|
||||
if (!!selectedChannnel) {
|
||||
return readonlyTextCell(
|
||||
intl.formatMessage(getChannelAvailabilityLabel(selectedChannnel)),
|
||||
);
|
||||
}
|
||||
|
||||
return readonlyTextCell(
|
||||
rowData?.channelListings?.length
|
||||
? intl.formatMessage(messages.dropdownLabel, {
|
||||
channelCount: rowData?.channelListings?.length,
|
||||
})
|
||||
: intl.formatMessage(messages.noChannels),
|
||||
);
|
||||
}
|
||||
|
||||
function getDescriptionCellContent(
|
||||
columnId: string,
|
||||
change: boolean,
|
||||
rowData: RelayToFlat<ProductListQuery["products"]>[number],
|
||||
) {
|
||||
const value = change ?? rowData?.[columnId] ?? "";
|
||||
|
||||
if (!value) {
|
||||
return readonlyTextCell("");
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(value);
|
||||
|
||||
if (parsed) {
|
||||
const descriptionFirstParagraph = parsed.blocks.find(
|
||||
block => block.type === "paragraph",
|
||||
);
|
||||
|
||||
if (descriptionFirstParagraph) {
|
||||
return readonlyTextCell(descriptionFirstParagraph.data.text);
|
||||
}
|
||||
}
|
||||
|
||||
return readonlyTextCell(value || "");
|
||||
}
|
||||
|
||||
function getNameCellContent(
|
||||
change: ThumbnailCellProps,
|
||||
rowData: RelayToFlat<ProductListQuery["products"]>[number],
|
||||
) {
|
||||
const name = change?.name ?? rowData?.name ?? "";
|
||||
return thumbnailCell(name, rowData?.thumbnail?.url ?? "", {
|
||||
cursor: "pointer",
|
||||
});
|
||||
}
|
||||
|
||||
function getPriceCellContent(
|
||||
intl: IntlShape,
|
||||
locale: Locale,
|
||||
selectedChannnel?: RelayToFlat<
|
||||
ProductListQuery["products"]
|
||||
>[number]["channelListings"][number],
|
||||
) {
|
||||
const from = selectedChannnel?.pricing?.priceRange?.start?.net;
|
||||
const to = selectedChannnel?.pricing?.priceRange?.stop?.net;
|
||||
|
||||
return readonlyTextCell(getMoneyRange(locale, intl, from, to));
|
||||
}
|
||||
|
||||
function getUpdatedAtrCellContent(
|
||||
rowData: RelayToFlat<ProductListQuery["products"]>[number],
|
||||
locale: Locale,
|
||||
) {
|
||||
if (!rowData) {
|
||||
return readonlyTextCell("");
|
||||
}
|
||||
|
||||
return readonlyTextCell(
|
||||
moment(rowData.updatedAt).locale(locale).format("lll"),
|
||||
);
|
||||
}
|
||||
|
||||
function getAttributeCellContent(
|
||||
columnId: string,
|
||||
rowData: RelayToFlat<ProductListQuery["products"]>[number],
|
||||
) {
|
||||
const attributeId = getAttributeIdFromColumnValue(columnId);
|
||||
const productAttribute = rowData?.attributes.find(
|
||||
attribute => attribute.attribute.id === attributeId,
|
||||
);
|
||||
|
||||
if (productAttribute) {
|
||||
if (productAttribute.values.length) {
|
||||
if (productAttribute.values[0].date) {
|
||||
return readonlyTextCell(productAttribute.values[0].date);
|
||||
}
|
||||
if (productAttribute.values[0].dateTime) {
|
||||
return readonlyTextCell(productAttribute.values[0].dateTime);
|
||||
}
|
||||
}
|
||||
|
||||
const textValue = productAttribute.values
|
||||
.map(value => value.name)
|
||||
.join(", ");
|
||||
|
||||
return readonlyTextCell(textValue);
|
||||
}
|
||||
|
||||
return readonlyTextCell("");
|
||||
}
|
||||
|
||||
export function getColumnMetadata(column: string) {
|
||||
if (column.includes(":")) {
|
||||
const [columnName, columnId] = column.split(":");
|
||||
|
||||
return {
|
||||
columnName: columnName as ProductListUrlSortField,
|
||||
columnId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
columnName: column as ProductListUrlSortField,
|
||||
};
|
||||
}
|
||||
|
||||
export function getProductRowsLength(
|
||||
disabled: boolean,
|
||||
product?: RelayToFlat<ProductListQuery["products"]>,
|
||||
) {
|
||||
if (product?.length) {
|
||||
return product.length;
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -34,11 +34,12 @@ const props: ProductListPageProps = {
|
|||
...sortPageProps.sort,
|
||||
sort: ProductListUrlSortField.name,
|
||||
},
|
||||
channels: [],
|
||||
columnQuery: "",
|
||||
availableInGridAttributes: [],
|
||||
onColumnQueryChange: () => undefined,
|
||||
},
|
||||
activeAttributeSortId: undefined,
|
||||
availableInGridAttributes: attributes,
|
||||
columnQuery: "",
|
||||
onColumnQueryChange: () => undefined,
|
||||
currencySymbol: "USD",
|
||||
defaultSettings: defaultListSettings[ListViews.PRODUCT_LIST],
|
||||
filterOpts: productListFilterOpts,
|
||||
|
|
|
@ -4,16 +4,13 @@ import {
|
|||
mapToMenuItemsForProductOverviewActions,
|
||||
useExtensions,
|
||||
} from "@dashboard/apps/hooks/useExtensions";
|
||||
import { LimitsInfo } from "@dashboard/components/AppLayout/LimitsInfo";
|
||||
import { FilterBar } from "@dashboard/components/AppLayout/FilterBar";
|
||||
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
||||
import { ButtonWithSelect } from "@dashboard/components/ButtonWithSelect";
|
||||
import CardMenu from "@dashboard/components/CardMenu";
|
||||
import ColumnPicker from "@dashboard/components/ColumnPicker";
|
||||
import { ButtonWithDropdown } from "@dashboard/components/ButtonWithDropdown";
|
||||
import { getByName } from "@dashboard/components/Filter/utils";
|
||||
import FilterBar from "@dashboard/components/FilterBar";
|
||||
import { ListPageLayout } from "@dashboard/components/Layouts";
|
||||
import LimitReachedAlert from "@dashboard/components/LimitReachedAlert";
|
||||
import { MultiAutocompleteChoiceType } from "@dashboard/components/MultiAutocompleteSelectField";
|
||||
import { TopNavMenu } from "@dashboard/components/TopNavMenu";
|
||||
import { ProductListColumns } from "@dashboard/config";
|
||||
import {
|
||||
GridAttributesQuery,
|
||||
|
@ -21,6 +18,8 @@ import {
|
|||
RefreshLimitsQuery,
|
||||
SearchAvailableInGridAttributesQuery,
|
||||
} from "@dashboard/graphql";
|
||||
import useLocalStorage from "@dashboard/hooks/useLocalStorage";
|
||||
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||
import { sectionNames } from "@dashboard/intl";
|
||||
import {
|
||||
ChannelProps,
|
||||
|
@ -33,19 +32,19 @@ import {
|
|||
} from "@dashboard/types";
|
||||
import { hasLimits, isLimitReached } from "@dashboard/utils/limits";
|
||||
import { Card } from "@material-ui/core";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import { ProductListUrlSortField } from "../../urls";
|
||||
import ProductList from "../ProductList";
|
||||
import { columnsMessages } from "../ProductList/messages";
|
||||
import { ProductListUrlSortField, productUrl } from "../../urls";
|
||||
import { ProductListDatagrid } from "../ProductListDatagrid";
|
||||
import { ProductListTiles } from "../ProductListTiles/ProductListTiles";
|
||||
import { ProductListViewSwitch } from "../ProductListViewSwitch";
|
||||
import {
|
||||
createFilterStructure,
|
||||
ProductFilterKeys,
|
||||
ProductListFilterOpts,
|
||||
} from "./filters";
|
||||
import { getAttributeColumnValue } from "./utils";
|
||||
|
||||
export interface ProductListPageProps
|
||||
extends PageListProps<ProductListColumns>,
|
||||
|
@ -69,30 +68,13 @@ export interface ProductListPageProps
|
|||
onColumnQueryChange: (query: string) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
columnPicker: {
|
||||
marginRight: theme.spacing(3),
|
||||
[theme.breakpoints.down("xs")]: {
|
||||
"& > button": {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
marginRight: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ name: "ProductListPage" },
|
||||
);
|
||||
export type ProductListViewType = "datagrid" | "tile";
|
||||
const DEFAULT_PRODUCT_LIST_VIEW_TYPE: ProductListViewType = "datagrid";
|
||||
|
||||
export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||
const {
|
||||
columnQuery,
|
||||
currencySymbol,
|
||||
currentTab,
|
||||
defaultSettings,
|
||||
gridAttributes,
|
||||
limits,
|
||||
|
@ -102,75 +84,25 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
initialSearch,
|
||||
loading,
|
||||
settings,
|
||||
tabs,
|
||||
onAdd,
|
||||
onAll,
|
||||
onColumnQueryChange,
|
||||
onExport,
|
||||
onFetchMore,
|
||||
onFilterChange,
|
||||
onFilterAttributeFocus,
|
||||
onSearchChange,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
onTabSave,
|
||||
onUpdateListSettings,
|
||||
selectedChannelId,
|
||||
selectedProductIds,
|
||||
activeAttributeSortId,
|
||||
...listProps
|
||||
} = props;
|
||||
const intl = useIntl();
|
||||
const classes = useStyles(props);
|
||||
|
||||
const staticColumns = [
|
||||
{
|
||||
label: intl.formatMessage(columnsMessages.availability),
|
||||
value: "availability" as ProductListColumns,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(columnsMessages.price),
|
||||
value: "price" as ProductListColumns,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(columnsMessages.type),
|
||||
value: "productType" as ProductListColumns,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(columnsMessages.updatedAt),
|
||||
value: "date" as ProductListColumns,
|
||||
},
|
||||
];
|
||||
|
||||
const initialColumnsChoices = React.useMemo(() => {
|
||||
const selectedStaticColumns = staticColumns.filter(column =>
|
||||
(settings.columns || []).includes(column.value),
|
||||
);
|
||||
const selectedAttributeColumns = gridAttributes.map(attribute => ({
|
||||
label: attribute.name,
|
||||
value: getAttributeColumnValue(attribute.id),
|
||||
}));
|
||||
|
||||
return [...selectedStaticColumns, ...selectedAttributeColumns];
|
||||
}, [gridAttributes, settings.columns]);
|
||||
|
||||
const handleSave = (columns: ProductListColumns[]) =>
|
||||
onUpdateListSettings("columns", columns);
|
||||
|
||||
const navigate = useNavigator();
|
||||
const filterStructure = createFilterStructure(intl, filterOpts);
|
||||
|
||||
const filterDependency = filterStructure.find(getByName("channel"));
|
||||
|
||||
const availableColumns: MultiAutocompleteChoiceType[] = [
|
||||
...staticColumns,
|
||||
...availableInGridAttributes.map(
|
||||
attribute =>
|
||||
({
|
||||
label: attribute.name,
|
||||
value: getAttributeColumnValue(attribute.id),
|
||||
} as MultiAutocompleteChoiceType),
|
||||
),
|
||||
];
|
||||
|
||||
const limitReached = isLimitReached(limits, "productVariants");
|
||||
const { PRODUCT_OVERVIEW_CREATE, PRODUCT_OVERVIEW_MORE_ACTIONS } =
|
||||
useExtensions(extensionMountPoints.PRODUCT_LIST);
|
||||
|
@ -181,65 +113,69 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
);
|
||||
const extensionCreateButtonItems = mapToMenuItems(PRODUCT_OVERVIEW_CREATE);
|
||||
|
||||
const [storedProductListViewType, setProductListViewType] =
|
||||
useLocalStorage<ProductListViewType>(
|
||||
"productListViewType",
|
||||
DEFAULT_PRODUCT_LIST_VIEW_TYPE,
|
||||
);
|
||||
const isDatagridView = storedProductListViewType === "datagrid";
|
||||
|
||||
return (
|
||||
<ListPageLayout>
|
||||
<TopNav title={intl.formatMessage(sectionNames.products)}>
|
||||
<CardMenu
|
||||
className={classes.settings}
|
||||
menuItems={[
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: "7FL+WZ",
|
||||
defaultMessage: "Export Products",
|
||||
description: "export products to csv file, button",
|
||||
}),
|
||||
onSelect: onExport,
|
||||
testId: "export",
|
||||
},
|
||||
...extensionMenuItems,
|
||||
]}
|
||||
data-test-id="menu"
|
||||
/>
|
||||
<ColumnPicker
|
||||
className={classes.columnPicker}
|
||||
availableColumns={availableColumns}
|
||||
initialColumns={initialColumnsChoices}
|
||||
defaultColumns={defaultSettings.columns}
|
||||
hasMore={hasMore}
|
||||
loading={loading}
|
||||
query={columnQuery}
|
||||
onQueryChange={onColumnQueryChange}
|
||||
onFetchMore={onFetchMore}
|
||||
onSave={handleSave}
|
||||
IconButtonProps={{ variant: "secondary" }}
|
||||
/>
|
||||
<ButtonWithSelect
|
||||
options={extensionCreateButtonItems}
|
||||
data-test-id="add-product"
|
||||
disabled={limitReached}
|
||||
onClick={onAdd}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="JFmOfi"
|
||||
defaultMessage="Create Product"
|
||||
description="button"
|
||||
/>
|
||||
</ButtonWithSelect>
|
||||
{hasLimits(limits, "productVariants") && (
|
||||
<LimitsInfo
|
||||
text={intl.formatMessage(
|
||||
<TopNav withoutBorder title={intl.formatMessage(sectionNames.products)}>
|
||||
<Box display="flex" alignItems="center" gap={5}>
|
||||
{hasLimits(limits, "productVariants") && (
|
||||
<Text variant="caption">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Kw0jHS",
|
||||
defaultMessage: "{count}/{max} SKUs used",
|
||||
description: "created products counter",
|
||||
},
|
||||
{
|
||||
count: limits.currentUsage.productVariants,
|
||||
max: limits.allowedUsage.productVariants,
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
<TopNavMenu
|
||||
dataTestId="menu"
|
||||
items={[
|
||||
{
|
||||
id: "Kw0jHS",
|
||||
defaultMessage: "{count}/{max} SKUs used",
|
||||
description: "created products counter",
|
||||
label: intl.formatMessage({
|
||||
id: "7FL+WZ",
|
||||
defaultMessage: "Export Products",
|
||||
description: "export products to csv file, button",
|
||||
}),
|
||||
onSelect: onExport,
|
||||
testId: "export",
|
||||
},
|
||||
{
|
||||
count: limits.currentUsage.productVariants,
|
||||
max: limits.allowedUsage.productVariants,
|
||||
},
|
||||
)}
|
||||
...extensionMenuItems,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{extensionCreateButtonItems.length > 0 ? (
|
||||
<ButtonWithDropdown
|
||||
onClick={onAdd}
|
||||
testId={"add-product"}
|
||||
options={extensionCreateButtonItems}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="JFmOfi"
|
||||
defaultMessage="Create Product"
|
||||
description="button"
|
||||
/>
|
||||
</ButtonWithDropdown>
|
||||
) : (
|
||||
<Button data-test-id="add-product" onClick={onAdd}>
|
||||
<FormattedMessage
|
||||
id="JFmOfi"
|
||||
defaultMessage="Create Product"
|
||||
description="button"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</TopNav>
|
||||
{limitReached && (
|
||||
<LimitReachedAlert
|
||||
|
@ -256,37 +192,66 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
</LimitReachedAlert>
|
||||
)}
|
||||
<Card>
|
||||
<FilterBar
|
||||
currencySymbol={currencySymbol}
|
||||
currentTab={currentTab}
|
||||
initialSearch={initialSearch}
|
||||
onAll={onAll}
|
||||
onFilterChange={onFilterChange}
|
||||
onFilterAttributeFocus={onFilterAttributeFocus}
|
||||
onSearchChange={onSearchChange}
|
||||
onTabChange={onTabChange}
|
||||
onTabDelete={onTabDelete}
|
||||
onTabSave={onTabSave}
|
||||
tabs={tabs}
|
||||
allTabLabel={intl.formatMessage({
|
||||
id: "aFLtLk",
|
||||
defaultMessage: "All Products",
|
||||
description: "tab name",
|
||||
})}
|
||||
filterStructure={filterStructure}
|
||||
searchPlaceholder={intl.formatMessage({
|
||||
id: "kIvvax",
|
||||
defaultMessage: "Search Products...",
|
||||
})}
|
||||
/>
|
||||
<ProductList
|
||||
{...listProps}
|
||||
gridAttributes={gridAttributes}
|
||||
settings={settings}
|
||||
selectedChannelId={selectedChannelId}
|
||||
onUpdateListSettings={onUpdateListSettings}
|
||||
filterDependency={filterDependency}
|
||||
/>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
alignItems="stretch"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<FilterBar
|
||||
currencySymbol={currencySymbol}
|
||||
initialSearch={initialSearch}
|
||||
onFilterChange={onFilterChange}
|
||||
onFilterAttributeFocus={onFilterAttributeFocus}
|
||||
onSearchChange={onSearchChange}
|
||||
filterStructure={filterStructure}
|
||||
searchPlaceholder={intl.formatMessage({
|
||||
id: "kIvvax",
|
||||
defaultMessage: "Search Products...",
|
||||
})}
|
||||
actions={
|
||||
<ProductListViewSwitch
|
||||
defaultValue={storedProductListViewType}
|
||||
setProductListViewType={setProductListViewType}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{isDatagridView ? (
|
||||
<ProductListDatagrid
|
||||
{...listProps}
|
||||
filterDependency={filterDependency}
|
||||
activeAttributeSortId={activeAttributeSortId}
|
||||
columnQuery={columnQuery}
|
||||
defaultSettings={defaultSettings}
|
||||
availableInGridAttributes={availableInGridAttributes}
|
||||
isAttributeLoading={loading}
|
||||
loading={listProps.disabled}
|
||||
hasMore={hasMore}
|
||||
gridAttributes={gridAttributes}
|
||||
onColumnQueryChange={onColumnQueryChange}
|
||||
onFetchMore={onFetchMore}
|
||||
products={listProps.products}
|
||||
settings={settings}
|
||||
selectedChannelId={selectedChannelId}
|
||||
onUpdateListSettings={onUpdateListSettings}
|
||||
onRowClick={id => {
|
||||
navigate(productUrl(id));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ProductListTiles
|
||||
{...listProps}
|
||||
settings={settings}
|
||||
loading={listProps.disabled}
|
||||
onUpdateListSettings={onUpdateListSettings}
|
||||
products={listProps.products}
|
||||
onTileClick={id => {
|
||||
navigate(productUrl(id));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</ListPageLayout>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
|
||||
import { ProductListColumns } from "@dashboard/config";
|
||||
import { ProductListQuery } from "@dashboard/graphql";
|
||||
import { ListProps, RelayToFlat } from "@dashboard/types";
|
||||
import { CircularProgress } from "@material-ui/core";
|
||||
import { Box, Text } from "@saleor/macaw-ui/next";
|
||||
import React, { useCallback } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { messages } from "../ProductListDatagrid/messages";
|
||||
import { ProductTile } from "../ProductTile/ProductTile";
|
||||
|
||||
export interface ProductListTilesProps extends ListProps<ProductListColumns> {
|
||||
products: RelayToFlat<ProductListQuery["products"]> | undefined;
|
||||
loading?: boolean;
|
||||
onTileClick: (id: string) => void;
|
||||
}
|
||||
|
||||
export const ProductListTiles: React.FC<ProductListTilesProps> = ({
|
||||
products,
|
||||
onTileClick,
|
||||
settings,
|
||||
disabled,
|
||||
loading,
|
||||
onUpdateListSettings,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
if (loading) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" height="100%" marginY={12}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (products?.length > 0) {
|
||||
return (
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateColumns={{ mobile: 3, tablet: 5, desktop: 6 }}
|
||||
gap={9}
|
||||
padding={9}
|
||||
>
|
||||
{products.map(product => (
|
||||
<ProductTile
|
||||
key={product.id}
|
||||
product={product}
|
||||
onClick={() => onTileClick(product.id)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box padding={9} textAlign="center">
|
||||
<Text size="small">{intl.formatMessage(messages.emptyText)}</Text>
|
||||
</Box>
|
||||
);
|
||||
}, [intl, loading, onTileClick, products]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderContent()}
|
||||
<Box paddingX={9}>
|
||||
<TablePaginationWithContext
|
||||
component="div"
|
||||
settings={settings}
|
||||
disabled={disabled}
|
||||
labels={{
|
||||
noOfRows: intl.formatMessage({
|
||||
id: "9B2mOB",
|
||||
defaultMessage: "No. of products",
|
||||
description: "tile view pagination label",
|
||||
}),
|
||||
}}
|
||||
onUpdateListSettings={onUpdateListSettings}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue