🏷️ 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,
|
PERMISSION_GROUP_LIST_SELECTORS,
|
||||||
PLUGINS_LIST_SELECTORS,
|
PLUGINS_LIST_SELECTORS,
|
||||||
PRODUCT_TYPES_LIST_SELECTORS,
|
PRODUCT_TYPES_LIST_SELECTORS,
|
||||||
|
PRODUCTS_LIST,
|
||||||
SALES_SELECTORS,
|
SALES_SELECTORS,
|
||||||
SHIPPING_ZONES_LIST_SELECTORS,
|
SHIPPING_ZONES_LIST_SELECTORS,
|
||||||
STAFF_MEMBERS_LIST_SELECTORS,
|
STAFF_MEMBERS_LIST_SELECTORS,
|
||||||
|
@ -25,6 +26,7 @@ import {
|
||||||
} from "../elements/";
|
} from "../elements/";
|
||||||
import { PERMISSIONS_OPTIONS } from "../fixtures/permissionsUsers";
|
import { PERMISSIONS_OPTIONS } from "../fixtures/permissionsUsers";
|
||||||
import { urlList } from "../fixtures/urlList";
|
import { urlList } from "../fixtures/urlList";
|
||||||
|
import { ensureCanvasStatic } from "../support/customCommands/sharedElementsOperations/canvas";
|
||||||
import {
|
import {
|
||||||
expectConfigurationAvailableSectionsNumber,
|
expectConfigurationAvailableSectionsNumber,
|
||||||
expectConfigurationSectionsToBeVisible,
|
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");
|
cy.get(MENU_SELECTORS.CATALOG.collections).should("be.visible");
|
||||||
expectMainMenuAvailableSections(5);
|
expectMainMenuAvailableSections(5);
|
||||||
});
|
});
|
||||||
|
ensureCanvasStatic(PRODUCTS_LIST.dataGridTable).then(() => {
|
||||||
|
cy.assertCanvasRowsNumber(PRODUCTS_LIST.dataGridTable, 21);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,9 @@
|
||||||
/// <reference types="../../support"/>
|
/// <reference types="../../support"/>
|
||||||
|
|
||||||
import { PRODUCT_DETAILS } from "../../elements/catalog/products/product-details";
|
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 { SHARED_ELEMENTS } from "../../elements/shared/sharedElements";
|
||||||
import { demoProductsNames } from "../../fixtures/products";
|
import { demoProductsNames } from "../../fixtures/products";
|
||||||
import { productDetailsUrl, urlList } from "../../fixtures/urlList";
|
import { productDetailsUrl } from "../../fixtures/urlList";
|
||||||
import { getFirstProducts } from "../../support/api/requests/Product";
|
import { getFirstProducts } from "../../support/api/requests/Product";
|
||||||
import { deleteCollectionsStartsWith } from "../../support/api/utils/catalog/collectionsUtils";
|
import { deleteCollectionsStartsWith } from "../../support/api/utils/catalog/collectionsUtils";
|
||||||
import {
|
import {
|
||||||
|
@ -19,39 +18,6 @@ describe("Tests for images", () => {
|
||||||
cy.clearSessionData().loginUserViaRequest();
|
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(
|
it(
|
||||||
"Should display product image",
|
"Should display product image",
|
||||||
{ tags: ["@products", "@allEnv", "@stable"] },
|
{ 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 MENU_SELECTORS from "./left-menu/left-menu-selectors";
|
||||||
import * as LOGIN_SELECTORS from "./login-selectors";
|
import * as LOGIN_SELECTORS from "./login-selectors";
|
||||||
import * as SET_PASSWORD_SELECTORS from "./login-selectors";
|
import * as SET_PASSWORD_SELECTORS from "./login-selectors";
|
||||||
|
|
|
@ -3,9 +3,12 @@ import {
|
||||||
CATEGORY_DETAILS_SELECTORS,
|
CATEGORY_DETAILS_SELECTORS,
|
||||||
} from "./categories";
|
} from "./categories";
|
||||||
import { COLLECTION_SELECTORS } from "./collection-selectors";
|
import { COLLECTION_SELECTORS } from "./collection-selectors";
|
||||||
|
import { PRODUCT_DETAILS, PRODUCTS_LIST } from "./products";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
CATEGORIES_LIST_SELECTORS,
|
CATEGORIES_LIST_SELECTORS,
|
||||||
CATEGORY_DETAILS_SELECTORS,
|
CATEGORY_DETAILS_SELECTORS,
|
||||||
COLLECTION_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 = {
|
export const PRODUCTS_LIST = {
|
||||||
productsList: "[data-test-id^='id-']",
|
productsList: "[data-test-id^='id-']",
|
||||||
|
dataGridTable: "[data-testid='data-grid-canvas']",
|
||||||
productsNames: "[data-test-id='name']",
|
productsNames: "[data-test-id='name']",
|
||||||
dialogProductTypeInput: "[data-test-id='dialog-product-type']",
|
dialogProductTypeInput: "[data-test-id='dialog-product-type']",
|
||||||
createProductBtn: "[data-test-id='add-product']",
|
createProductBtn: "[data-test-id='add-product']",
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {
|
||||||
CATEGORIES_LIST_SELECTORS,
|
CATEGORIES_LIST_SELECTORS,
|
||||||
CATEGORY_DETAILS_SELECTORS,
|
CATEGORY_DETAILS_SELECTORS,
|
||||||
COLLECTION_SELECTORS,
|
COLLECTION_SELECTORS,
|
||||||
|
PRODUCT_DETAILS,
|
||||||
|
PRODUCTS_LIST,
|
||||||
} from "./catalog";
|
} from "./catalog";
|
||||||
import { CHANNELS_SELECTORS } from "./channels";
|
import { CHANNELS_SELECTORS } from "./channels";
|
||||||
import { CONFIGURATION_SELECTORS } from "./configuration/configuration-selectors";
|
import { CONFIGURATION_SELECTORS } from "./configuration/configuration-selectors";
|
||||||
|
@ -67,8 +69,10 @@ export {
|
||||||
PERMISSION_GROUP_LIST_SELECTORS,
|
PERMISSION_GROUP_LIST_SELECTORS,
|
||||||
PLUGINS_DETAILS_SELECTORS,
|
PLUGINS_DETAILS_SELECTORS,
|
||||||
PLUGINS_LIST_SELECTORS,
|
PLUGINS_LIST_SELECTORS,
|
||||||
|
PRODUCT_DETAILS,
|
||||||
PRODUCT_TYPE_DETAILS_SELECTORS,
|
PRODUCT_TYPE_DETAILS_SELECTORS,
|
||||||
PRODUCT_TYPES_LIST_SELECTORS,
|
PRODUCT_TYPES_LIST_SELECTORS,
|
||||||
|
PRODUCTS_LIST,
|
||||||
SALES_SELECTORS,
|
SALES_SELECTORS,
|
||||||
SET_PASSWORD_SELECTORS,
|
SET_PASSWORD_SELECTORS,
|
||||||
SHARED_ELEMENTS,
|
SHARED_ELEMENTS,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SHARED_ELEMENTS } from "./sharedElements";
|
|
||||||
import { ADDRESS_SELECTORS } from "./addressForm";
|
import { ADDRESS_SELECTORS } from "./addressForm";
|
||||||
|
import { SHARED_ELEMENTS } from "./sharedElements";
|
||||||
|
|
||||||
export { SHARED_ELEMENTS, ADDRESS_SELECTORS };
|
export { SHARED_ELEMENTS, ADDRESS_SELECTORS };
|
||||||
|
|
|
@ -41,3 +41,6 @@ Cypress.Commands.add("checkIfDataAreNotNull", data => {
|
||||||
Cypress.Commands.add("checkIfElementIsVisible", element => {
|
Cypress.Commands.add("checkIfElementIsVisible", element => {
|
||||||
cy.get(element).should("be.visible");
|
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",
|
"context": "input label",
|
||||||
"string": "Stock reservation for anonymous user (in minutes)"
|
"string": "Stock reservation for anonymous user (in minutes)"
|
||||||
},
|
},
|
||||||
|
"+U6ozc": {
|
||||||
|
"string": "Type"
|
||||||
|
},
|
||||||
"+VEhV8": {
|
"+VEhV8": {
|
||||||
"context": "product channels",
|
"context": "product channels",
|
||||||
"string": "Availability"
|
"string": "Availability"
|
||||||
|
@ -1468,6 +1471,10 @@
|
||||||
"context": "header",
|
"context": "header",
|
||||||
"string": "Translation Product Variant \"{productName}\" - {languageCode}"
|
"string": "Translation Product Variant \"{productName}\" - {languageCode}"
|
||||||
},
|
},
|
||||||
|
"9B2mOB": {
|
||||||
|
"context": "tile view pagination label",
|
||||||
|
"string": "No. of products"
|
||||||
|
},
|
||||||
"9C7PZE": {
|
"9C7PZE": {
|
||||||
"context": "navigation section name",
|
"context": "navigation section name",
|
||||||
"string": "Navigation"
|
"string": "Navigation"
|
||||||
|
@ -1705,6 +1712,9 @@
|
||||||
"context": "section description",
|
"context": "section description",
|
||||||
"string": "You can set basic checkout rules that will be applied globally to all your channels"
|
"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": {
|
"AzMSmb": {
|
||||||
"context": "caption",
|
"context": "caption",
|
||||||
"string": "If enabled this attribute can be used as a column in product table."
|
"string": "If enabled this attribute can be used as a column in product table."
|
||||||
|
@ -3824,6 +3834,9 @@
|
||||||
"context": "activate app",
|
"context": "activate app",
|
||||||
"string": "Are you sure you want to activate this app? Activating will start gathering events."
|
"string": "Are you sure you want to activate this app? Activating will start gathering events."
|
||||||
},
|
},
|
||||||
|
"Q4m1CG": {
|
||||||
|
"string": "Edit Product"
|
||||||
|
},
|
||||||
"Q55cTG": {
|
"Q55cTG": {
|
||||||
"context": "order refund amount",
|
"context": "order refund amount",
|
||||||
"string": "Previously refunded"
|
"string": "Previously refunded"
|
||||||
|
@ -4182,10 +4195,6 @@
|
||||||
"context": "export filtered items to csv file",
|
"context": "export filtered items to csv file",
|
||||||
"string": "Current search ({number})"
|
"string": "Current search ({number})"
|
||||||
},
|
},
|
||||||
"Sd0Ppm": {
|
|
||||||
"context": "product publication date",
|
|
||||||
"string": "Published on {date}"
|
|
||||||
},
|
|
||||||
"Sjd7wm": {
|
"Sjd7wm": {
|
||||||
"context": "product filter label",
|
"context": "product filter label",
|
||||||
"string": "Product"
|
"string": "Product"
|
||||||
|
@ -5290,10 +5299,6 @@
|
||||||
"context": "gift card history message",
|
"context": "gift card history message",
|
||||||
"string": "Gift card balance was reset by {resetBy}"
|
"string": "Gift card balance was reset by {resetBy}"
|
||||||
},
|
},
|
||||||
"aFLtLk": {
|
|
||||||
"context": "tab name",
|
|
||||||
"string": "All Products"
|
|
||||||
},
|
|
||||||
"aHc89n": {
|
"aHc89n": {
|
||||||
"context": "select warehouse to restock items",
|
"context": "select warehouse to restock items",
|
||||||
"string": "Select Warehouse"
|
"string": "Select Warehouse"
|
||||||
|
@ -7383,10 +7388,6 @@
|
||||||
"context": "dialog content",
|
"context": "dialog content",
|
||||||
"string": "Select one of customer addresses or add a new address:"
|
"string": "Select one of customer addresses or add a new address:"
|
||||||
},
|
},
|
||||||
"qpQ0uB": {
|
|
||||||
"context": "product publication date",
|
|
||||||
"string": "Unpublished"
|
|
||||||
},
|
|
||||||
"qrWOxx": {
|
"qrWOxx": {
|
||||||
"string": "No members found"
|
"string": "No members found"
|
||||||
},
|
},
|
||||||
|
@ -7412,10 +7413,6 @@
|
||||||
"r+dgiv": {
|
"r+dgiv": {
|
||||||
"string": "Taxes"
|
"string": "Taxes"
|
||||||
},
|
},
|
||||||
"r0hgpM": {
|
|
||||||
"context": "product publication date",
|
|
||||||
"string": "Becomes published on {date}"
|
|
||||||
},
|
|
||||||
"r1aQ2f": {
|
"r1aQ2f": {
|
||||||
"context": "dialog header",
|
"context": "dialog header",
|
||||||
"string": "Unassign Attribute from Product Type"
|
"string": "Unassign Attribute from Product Type"
|
||||||
|
@ -7773,6 +7770,9 @@
|
||||||
"context": "order total amount",
|
"context": "order total amount",
|
||||||
"string": "Total"
|
"string": "Total"
|
||||||
},
|
},
|
||||||
|
"tiY7bx": {
|
||||||
|
"string": "Add new product"
|
||||||
|
},
|
||||||
"tlGXkh": {
|
"tlGXkh": {
|
||||||
"context": "input description",
|
"context": "input description",
|
||||||
"string": "Unlimited"
|
"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/lab": "^4.0.0-alpha.61",
|
||||||
"@material-ui/styles": "^4.11.4",
|
"@material-ui/styles": "^4.11.4",
|
||||||
"@reach/auto-id": "^0.16.0",
|
"@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",
|
"@saleor/sdk": "^0.4.4",
|
||||||
"@sentry/react": "^6.0.0",
|
"@sentry/react": "^6.0.0",
|
||||||
"@types/faker": "^5.1.6",
|
"@types/faker": "^5.1.6",
|
||||||
|
@ -60,6 +60,7 @@
|
||||||
"lz-string": "^1.4.4",
|
"lz-string": "^1.4.4",
|
||||||
"marked": "^4.0.17",
|
"marked": "^4.0.17",
|
||||||
"moment-timezone": "^0.5.32",
|
"moment-timezone": "^0.5.32",
|
||||||
|
"pixelmatch": "^5.3.0",
|
||||||
"qs": "^6.9.0",
|
"qs": "^6.9.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
@ -145,6 +146,7 @@
|
||||||
"codecov": "^3.7.1",
|
"codecov": "^3.7.1",
|
||||||
"core-js": "^3.7.0",
|
"core-js": "^3.7.0",
|
||||||
"cross-env": "^6.0.3",
|
"cross-env": "^6.0.3",
|
||||||
|
"cypress-recurse": "^1.27.0",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"env-var": "^7.3.0",
|
"env-var": "^7.3.0",
|
||||||
"esbuild-loader": "^2.18.0",
|
"esbuild-loader": "^2.18.0",
|
||||||
|
@ -7271,9 +7273,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@saleor/macaw-ui": {
|
"node_modules/@saleor/macaw-ui": {
|
||||||
"version": "0.8.0-pre.49",
|
"version": "0.8.0-pre.50",
|
||||||
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.49.tgz",
|
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.50.tgz",
|
||||||
"integrity": "sha512-SHUfBE1AaDsam/K3ZcotDtUH6/jcyRR9Gi/Zpvw/cTcwoosq09WcewMWZNdHgY5Fkbgve7HTKb+wCyah+sBO8w==",
|
"integrity": "sha512-PGDkk1KmabYOQTPPzu+cmBxEyjdiYQdmzAiqtMIKwo8zff8nYPVE7yVKF9rJ7o3twqAiqF7fuAObo8qVjgZUYg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/react-dom-interactions": "^0.5.0",
|
"@floating-ui/react-dom-interactions": "^0.5.0",
|
||||||
"@radix-ui/react-radio-group": "^1.1.1",
|
"@radix-ui/react-radio-group": "^1.1.1",
|
||||||
|
@ -14883,6 +14885,15 @@
|
||||||
"mocha": ">=3.1.2"
|
"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": {
|
"node_modules/cypress-repeat": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -19911,6 +19922,12 @@
|
||||||
"node": ">=8.12.0"
|
"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": {
|
"node_modules/husky": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -27389,6 +27406,17 @@
|
||||||
"node": ">= 6"
|
"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": {
|
"node_modules/pkg-dir": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
@ -27472,6 +27500,14 @@
|
||||||
"semver-compare": "^1.0.0"
|
"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": {
|
"node_modules/pnp-webpack-plugin": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -41575,9 +41611,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@saleor/macaw-ui": {
|
"@saleor/macaw-ui": {
|
||||||
"version": "0.8.0-pre.49",
|
"version": "0.8.0-pre.50",
|
||||||
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.49.tgz",
|
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.50.tgz",
|
||||||
"integrity": "sha512-SHUfBE1AaDsam/K3ZcotDtUH6/jcyRR9Gi/Zpvw/cTcwoosq09WcewMWZNdHgY5Fkbgve7HTKb+wCyah+sBO8w==",
|
"integrity": "sha512-PGDkk1KmabYOQTPPzu+cmBxEyjdiYQdmzAiqtMIKwo8zff8nYPVE7yVKF9rJ7o3twqAiqF7fuAObo8qVjgZUYg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@floating-ui/react-dom-interactions": "^0.5.0",
|
"@floating-ui/react-dom-interactions": "^0.5.0",
|
||||||
"@radix-ui/react-radio-group": "^1.1.1",
|
"@radix-ui/react-radio-group": "^1.1.1",
|
||||||
|
@ -47190,6 +47226,15 @@
|
||||||
"lodash": "^4.17.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": {
|
"cypress-repeat": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
@ -50376,6 +50421,12 @@
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"devOptional": true
|
"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": {
|
"husky": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"optional": true
|
"optional": true
|
||||||
|
@ -55373,6 +55424,14 @@
|
||||||
"version": "4.0.5",
|
"version": "4.0.5",
|
||||||
"devOptional": true
|
"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": {
|
"pkg-dir": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
@ -55429,6 +55488,11 @@
|
||||||
"semver-compare": "^1.0.0"
|
"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": {
|
"pnp-webpack-plugin": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||||
"@material-ui/styles": "^4.11.4",
|
"@material-ui/styles": "^4.11.4",
|
||||||
"@reach/auto-id": "^0.16.0",
|
"@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",
|
"@saleor/sdk": "^0.4.4",
|
||||||
"@sentry/react": "^6.0.0",
|
"@sentry/react": "^6.0.0",
|
||||||
"@types/faker": "^5.1.6",
|
"@types/faker": "^5.1.6",
|
||||||
|
@ -67,6 +67,7 @@
|
||||||
"lz-string": "^1.4.4",
|
"lz-string": "^1.4.4",
|
||||||
"marked": "^4.0.17",
|
"marked": "^4.0.17",
|
||||||
"moment-timezone": "^0.5.32",
|
"moment-timezone": "^0.5.32",
|
||||||
|
"pixelmatch": "^5.3.0",
|
||||||
"qs": "^6.9.0",
|
"qs": "^6.9.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
@ -144,6 +145,7 @@
|
||||||
"@types/webappsec-credential-management": "^0.5.1",
|
"@types/webappsec-credential-management": "^0.5.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.41.0",
|
"@typescript-eslint/eslint-plugin": "^5.41.0",
|
||||||
"@typescript-eslint/parser": "^5.41.0",
|
"@typescript-eslint/parser": "^5.41.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.2.0",
|
||||||
"babel-core": "^7.0.0-bridge.0",
|
"babel-core": "^7.0.0-bridge.0",
|
||||||
"babel-jest": "^27.5.1",
|
"babel-jest": "^27.5.1",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
|
@ -151,6 +153,7 @@
|
||||||
"codecov": "^3.7.1",
|
"codecov": "^3.7.1",
|
||||||
"core-js": "^3.7.0",
|
"core-js": "^3.7.0",
|
||||||
"cross-env": "^6.0.3",
|
"cross-env": "^6.0.3",
|
||||||
|
"cypress-recurse": "^1.27.0",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"env-var": "^7.3.0",
|
"env-var": "^7.3.0",
|
||||||
"esbuild-loader": "^2.18.0",
|
"esbuild-loader": "^2.18.0",
|
||||||
|
@ -172,7 +175,6 @@
|
||||||
"vite-plugin-html": "^3.2.0",
|
"vite-plugin-html": "^3.2.0",
|
||||||
"vite-plugin-pwa": "^0.13.3",
|
"vite-plugin-pwa": "^0.13.3",
|
||||||
"vite-plugin-sentry": "^1.1.6",
|
"vite-plugin-sentry": "^1.1.6",
|
||||||
"@vitejs/plugin-react-swc": "^3.2.0",
|
|
||||||
"workbox-cacheable-response": "^6.1.2",
|
"workbox-cacheable-response": "^6.1.2",
|
||||||
"workbox-expiration": "^6.1.2",
|
"workbox-expiration": "^6.1.2",
|
||||||
"workbox-precaching": "^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;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use `Text` instead
|
||||||
|
*/
|
||||||
export const LimitsInfo: React.FC<LimitsInfoProps> = ({ text }) => (
|
export const LimitsInfo: React.FC<LimitsInfoProps> = ({ text }) => (
|
||||||
<Box position="absolute" left={10} bottom={3}>
|
<Box position="absolute" left={10} bottom={3}>
|
||||||
{text}
|
{text}
|
||||||
|
|
|
@ -9,20 +9,22 @@ import { TopNavWrapper } from "./TopNavWrapper";
|
||||||
interface TopNavProps {
|
interface TopNavProps {
|
||||||
title: string | React.ReactNode;
|
title: string | React.ReactNode;
|
||||||
href?: string;
|
href?: string;
|
||||||
|
withoutBorder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TopNav: React.FC<PropsWithChildren<TopNavProps>> = ({
|
export const TopNav: React.FC<PropsWithChildren<TopNavProps>> = ({
|
||||||
title,
|
title,
|
||||||
href,
|
href,
|
||||||
|
withoutBorder = false,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const { availableChannels, channel, isPickerActive, setChannel } =
|
const { availableChannels, channel, isPickerActive, setChannel } =
|
||||||
useAppChannel(false);
|
useAppChannel(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopNavWrapper>
|
<TopNavWrapper withoutBorder={withoutBorder}>
|
||||||
{href && <TopNavLink to={href} />}
|
{href && <TopNavLink to={href} />}
|
||||||
<Box __flex={1} marginLeft={5}>
|
<Box __flex={1}>
|
||||||
<Text variant="title">{title}</Text>
|
<Text variant="title">{title}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" flexWrap="nowrap">
|
<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 React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ export const TopNavLink: React.FC<{
|
||||||
to: string;
|
to: string;
|
||||||
variant?: "secondary" | "tertiary";
|
variant?: "secondary" | "tertiary";
|
||||||
}> = ({ to, variant = "secondary" }) => (
|
}> = ({ to, variant = "secondary" }) => (
|
||||||
<Link to={to}>
|
<Link to={to} className={sprinkles({ marginRight: 5 })}>
|
||||||
<Button
|
<Button
|
||||||
icon={<ArrowLeftIcon />}
|
icon={<ArrowLeftIcon />}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
|
|
|
@ -3,13 +3,16 @@ import React from "react";
|
||||||
|
|
||||||
import { topBarHeight } from "../consts";
|
import { topBarHeight } from "../consts";
|
||||||
|
|
||||||
export const TopNavWrapper: React.FC = ({ children }) => (
|
export const TopNavWrapper: React.FC<{ withoutBorder?: boolean }> = ({
|
||||||
|
children,
|
||||||
|
withoutBorder,
|
||||||
|
}) => (
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
paddingX={9}
|
paddingX={9}
|
||||||
paddingY={8}
|
paddingY={8}
|
||||||
borderBottomWidth={1}
|
borderBottomWidth={withoutBorder ? 0 : 1}
|
||||||
borderBottomStyle="solid"
|
borderBottomStyle="solid"
|
||||||
borderColor="neutralPlain"
|
borderColor="neutralPlain"
|
||||||
position="relative"
|
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;
|
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> = ({
|
export const ButtonWithSelect: React.FC<ButtonWithSelectProps> = ({
|
||||||
options,
|
options,
|
||||||
children,
|
children,
|
||||||
|
|
|
@ -66,6 +66,9 @@ const useStyles = makeStyles(
|
||||||
{ name: "CardMenu" },
|
{ 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 CardMenu: React.FC<CardMenuProps> = props => {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
|
|
|
@ -4,11 +4,11 @@ import { FetchMoreProps } from "@dashboard/types";
|
||||||
import { ClickAwayListener, Grow, Popper } from "@material-ui/core";
|
import { ClickAwayListener, Grow, Popper } from "@material-ui/core";
|
||||||
import {
|
import {
|
||||||
Choice,
|
Choice,
|
||||||
ColumnsIcon,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
IconButtonProps,
|
IconButtonProps,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
} from "@saleor/macaw-ui";
|
} from "@saleor/macaw-ui";
|
||||||
|
import { TableEditIcon } from "@saleor/macaw-ui/next";
|
||||||
import { score } from "fuzzaldrin";
|
import { score } from "fuzzaldrin";
|
||||||
import sortBy from "lodash/sortBy";
|
import sortBy from "lodash/sortBy";
|
||||||
import React from "react";
|
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
|
// Component is uncontrolled but we need to reset it somehow, so we change
|
||||||
// initial prop after reset callback to force value refreshing
|
// initial prop after reset callback to force value refreshing
|
||||||
const [initialColumnsChoices, setInitialColumnsChoices] = useStateFromProps(
|
const [initialColumnsChoices, setInitialColumnsChoices] =
|
||||||
initialColumns,
|
useStateFromProps(initialColumns);
|
||||||
);
|
|
||||||
|
|
||||||
const onChange: FormChange<string[]> = event => {
|
const onChange: FormChange<string[]> = event => {
|
||||||
selectedColumns.current = event.target.value;
|
selectedColumns.current = event.target.value;
|
||||||
|
@ -112,7 +111,7 @@ const ColumnPicker: React.FC<ColumnPickerProps> = props => {
|
||||||
state={isExpanded ? "active" : "default"}
|
state={isExpanded ? "active" : "default"}
|
||||||
onClick={() => setExpansionState(prevState => !prevState)}
|
onClick={() => setExpansionState(prevState => !prevState)}
|
||||||
>
|
>
|
||||||
<ColumnsIcon />
|
<TableEditIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Popper
|
<Popper
|
||||||
className={classes.popper}
|
className={classes.popper}
|
||||||
|
|
|
@ -2,42 +2,59 @@ import "@glideapps/glide-data-grid/dist/index.css";
|
||||||
|
|
||||||
import { usePreventHistoryBack } from "@dashboard/hooks/usePreventHistoryBack";
|
import { usePreventHistoryBack } from "@dashboard/hooks/usePreventHistoryBack";
|
||||||
import DataEditor, {
|
import DataEditor, {
|
||||||
|
DataEditorProps,
|
||||||
DataEditorRef,
|
DataEditorRef,
|
||||||
EditableGridCell,
|
EditableGridCell,
|
||||||
GridCell,
|
GridCell,
|
||||||
|
GridColumn,
|
||||||
|
GridMouseEventArgs,
|
||||||
GridSelection,
|
GridSelection,
|
||||||
|
HeaderClickedEventArgs,
|
||||||
Item,
|
Item,
|
||||||
} from "@glideapps/glide-data-grid";
|
} 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 { 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 clsx from "clsx";
|
||||||
import range from "lodash/range";
|
import range from "lodash/range";
|
||||||
import throttle from "lodash/throttle";
|
import React, {
|
||||||
import React from "react";
|
MutableRefObject,
|
||||||
|
ReactElement,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
import { contentMaxWidth } from "../AppLayout/consts";
|
||||||
import { CardMenuItem } from "../CardMenu";
|
import { CardMenuItem } from "../CardMenu";
|
||||||
import ColumnPicker from "../ColumnPicker";
|
import { ColumnPickerProps } from "../ColumnPicker";
|
||||||
import { FullScreenContainer } from "./FullScreenContainer";
|
import { FullScreenContainer } from "./components/FullScreenContainer";
|
||||||
import { Header } from "./Header";
|
import { Header } from "./components/Header";
|
||||||
import { RowActions } from "./RowActions";
|
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, {
|
import useStyles, {
|
||||||
cellHeight,
|
cellHeight,
|
||||||
useDatagridTheme,
|
useDatagridTheme,
|
||||||
useFullScreenStyles,
|
useFullScreenStyles,
|
||||||
} from "./styles";
|
} from "./styles";
|
||||||
import { AvailableColumn } from "./types";
|
import { AvailableColumn } from "./types";
|
||||||
import useCells from "./useCells";
|
import { getDefultColumnPickerProps } from "./utils";
|
||||||
import useColumns from "./useColumns";
|
|
||||||
import useDatagridChange, {
|
|
||||||
DatagridChange,
|
|
||||||
OnDatagridChange,
|
|
||||||
} from "./useDatagridChange";
|
|
||||||
import { useFullScreenMode } from "./useFullScreenMode";
|
|
||||||
import { usePortalClasses } from "./usePortalClasses";
|
|
||||||
|
|
||||||
export interface GetCellContentOpts {
|
export interface GetCellContentOpts {
|
||||||
changes: React.MutableRefObject<DatagridChange[]>;
|
changes: MutableRefObject<DatagridChange[]>;
|
||||||
added: number[];
|
added: number[];
|
||||||
removed: number[];
|
removed: number[];
|
||||||
getChangeIndex: (column: string, row: number) => number;
|
getChangeIndex: (column: string, row: number) => number;
|
||||||
|
@ -48,20 +65,33 @@ export interface MenuItemsActions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatagridProps {
|
export interface DatagridProps {
|
||||||
addButtonLabel: string;
|
addButtonLabel?: string;
|
||||||
availableColumns: readonly AvailableColumn[];
|
availableColumns: readonly AvailableColumn[];
|
||||||
emptyText: string;
|
emptyText: string;
|
||||||
getCellError: (item: Item, opts: GetCellContentOpts) => boolean;
|
getCellError: (item: Item, opts: GetCellContentOpts) => boolean;
|
||||||
getCellContent: (item: Item, opts: GetCellContentOpts) => GridCell;
|
getCellContent: (item: Item, opts: GetCellContentOpts) => GridCell;
|
||||||
|
getColumnTooltipContent?: (colIndex: number) => string;
|
||||||
menuItems: (index: number) => CardMenuItem[];
|
menuItems: (index: number) => CardMenuItem[];
|
||||||
rows: number;
|
rows: number;
|
||||||
title: string;
|
title?: string;
|
||||||
fullScreenTitle?: string;
|
fullScreenTitle?: string;
|
||||||
selectionActions: (
|
selectionActions: (
|
||||||
selection: number[],
|
selection: number[],
|
||||||
actions: MenuItemsActions,
|
actions: MenuItemsActions,
|
||||||
) => React.ReactNode;
|
) => ReactNode;
|
||||||
onChange?: OnDatagridChange;
|
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> = ({
|
export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
|
@ -75,28 +105,41 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
selectionActions,
|
selectionActions,
|
||||||
title,
|
title,
|
||||||
fullScreenTitle,
|
fullScreenTitle,
|
||||||
|
onHeaderClicked,
|
||||||
onChange,
|
onChange,
|
||||||
}): React.ReactElement => {
|
renderColumnPicker,
|
||||||
const classes = useStyles();
|
onRowClick,
|
||||||
const fullScreenClasses = useFullScreenStyles(classes);
|
getColumnTooltipContent,
|
||||||
const datagridTheme = useDatagridTheme();
|
readonly = false,
|
||||||
const editor = React.useRef<DataEditorRef>();
|
rowMarkers = "checkbox",
|
||||||
|
freezeColumns = 1,
|
||||||
const { isOpen, isAnimationOpenFinished, toggle } = useFullScreenMode();
|
verticalBorder,
|
||||||
|
columnSelect = "none",
|
||||||
usePortalClasses({ className: classes.portal });
|
|
||||||
|
|
||||||
const {
|
|
||||||
availableColumnsChoices,
|
|
||||||
columns,
|
|
||||||
columnChoices,
|
|
||||||
defaultColumns,
|
|
||||||
displayedColumns,
|
|
||||||
onColumnMoved,
|
onColumnMoved,
|
||||||
onColumnResize,
|
onColumnResize,
|
||||||
onColumnsChange,
|
...datagridProps
|
||||||
picker,
|
}): ReactElement => {
|
||||||
} = useColumns(availableColumns);
|
const classes = useStyles();
|
||||||
|
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();
|
||||||
|
|
||||||
|
const { clearTooltip, tooltip, setTooltip } = useTooltipContainer();
|
||||||
|
|
||||||
|
const [selection, setSelection] = useState<GridSelection>();
|
||||||
|
const [hoverRow, setHoverRow] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
|
usePortalClasses({ className: classes.portal });
|
||||||
|
usePreventHistoryBack(scroller);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
added,
|
added,
|
||||||
|
@ -108,83 +151,132 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
onRowAdded,
|
onRowAdded,
|
||||||
} = useDatagridChange(availableColumns, rows, onChange);
|
} = 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 handleGetCellContent = useCallback(
|
||||||
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(
|
|
||||||
([column, row]: Item): GridCell => {
|
([column, row]: Item): GridCell => {
|
||||||
const item = [
|
const item = [column, row] as const;
|
||||||
availableColumns.findIndex(ac => ac.id === displayedColumns[column]),
|
|
||||||
row,
|
|
||||||
] as const;
|
|
||||||
const opts = { changes, added, removed, getChangeIndex };
|
const opts = { changes, added, removed, getChangeIndex };
|
||||||
const columnId = availableColumns[column].id;
|
|
||||||
|
const columnId = availableColumns[column]?.id;
|
||||||
const changed = !!changes.current[getChangeIndex(columnId, row)]?.data;
|
const changed = !!changes.current[getChangeIndex(columnId, row)]?.data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...getCellContent(item, opts),
|
...getCellContent(item, opts),
|
||||||
...(changed
|
...(changed
|
||||||
? { themeOverride: { bgCell: theme.palette.saleor.active[5] } }
|
? {
|
||||||
|
themeOverride: {
|
||||||
|
bgCell: themeValues.colors.background.surfaceBrandHighlight,
|
||||||
|
},
|
||||||
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(getCellError(item, opts)
|
...(getCellError(item, opts)
|
||||||
? {
|
? {
|
||||||
themeOverride: {
|
themeOverride: {
|
||||||
bgCell:
|
bgCell:
|
||||||
theme.palette.saleor.theme === "light"
|
themeValues.colors.background.interactiveCriticalHovering,
|
||||||
? theme.palette.saleor.fail.light
|
|
||||||
: theme.palette.saleor.errorAction[5],
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[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 => {
|
([column, row]: Item, newValue: EditableGridCell): void => {
|
||||||
onCellEdited(
|
onCellEdited([column, row], newValue);
|
||||||
[
|
|
||||||
availableColumns.findIndex(ac => ac.id === displayedColumns[column]),
|
|
||||||
row,
|
|
||||||
],
|
|
||||||
newValue,
|
|
||||||
);
|
|
||||||
editor.current.updateCells(
|
editor.current.updateCells(
|
||||||
range(displayedColumns.length).map(offset => ({
|
range(availableColumns.length).map(offset => ({
|
||||||
cell: [column + offset, row],
|
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[]) => {
|
(rows: number[]) => {
|
||||||
if (selection?.rows) {
|
if (selection?.rows) {
|
||||||
onRowsRemoved(rows);
|
onRowsRemoved(rows);
|
||||||
|
@ -194,19 +286,36 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
[selection, onRowsRemoved],
|
[selection, onRowsRemoved],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectionActionsComponent = React.useMemo(
|
const handleColumnResize = useCallback(
|
||||||
() =>
|
(column: GridColumn, newSize: number) => {
|
||||||
selection?.rows.length > 0
|
if (tooltip) {
|
||||||
? selectionActions(Array.from(selection.rows), { removeRows })
|
clearTooltip();
|
||||||
: null,
|
}
|
||||||
[selection, selectionActions, removeRows],
|
|
||||||
|
onColumnResize(column, newSize);
|
||||||
|
},
|
||||||
|
[clearTooltip, onColumnResize, tooltip],
|
||||||
);
|
);
|
||||||
|
|
||||||
const rowsTotal = rows - removed.length + added.length;
|
const handleColumnMoved = useCallback(
|
||||||
const hasColumnGroups = columns.some(col => col.group);
|
(startIndex: number, endIndex: number) => {
|
||||||
const headerTitle = isAnimationOpenFinished
|
if (tooltip) {
|
||||||
? fullScreenTitle ?? title
|
clearTooltip();
|
||||||
: title;
|
}
|
||||||
|
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 (
|
return (
|
||||||
<FullScreenContainer
|
<FullScreenContainer
|
||||||
|
@ -214,6 +323,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
className={fullScreenClasses.fullScreenContainer}
|
className={fullScreenClasses.fullScreenContainer}
|
||||||
>
|
>
|
||||||
<Card className={classes.root}>
|
<Card className={classes.root}>
|
||||||
|
{headerTitle && (
|
||||||
<Header title={headerTitle}>
|
<Header title={headerTitle}>
|
||||||
<Header.ButtonFullScreen isOpen={isOpen} onToggle={toggle}>
|
<Header.ButtonFullScreen isOpen={isOpen} onToggle={toggle}>
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
|
@ -230,10 +340,13 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Header.ButtonFullScreen>
|
</Header.ButtonFullScreen>
|
||||||
|
{addButtonLabel && (
|
||||||
<Header.ButtonAddRow onAddRow={onRowAdded}>
|
<Header.ButtonAddRow onAddRow={onRowAdded}>
|
||||||
{addButtonLabel}
|
{addButtonLabel}
|
||||||
</Header.ButtonAddRow>
|
</Header.ButtonAddRow>
|
||||||
|
)}
|
||||||
</Header>
|
</Header>
|
||||||
|
)}
|
||||||
<CardContent classes={{ root: classes.cardContentRoot }}>
|
<CardContent classes={{ root: classes.cardContentRoot }}>
|
||||||
{rowsTotal > 0 ? (
|
{rowsTotal > 0 ? (
|
||||||
<>
|
<>
|
||||||
|
@ -243,28 +356,43 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={classes.editorContainer}>
|
<div className={classes.editorContainer}>
|
||||||
|
<Box
|
||||||
|
backgroundColor="plain"
|
||||||
|
borderTopWidth={1}
|
||||||
|
borderTopStyle="solid"
|
||||||
|
borderColor="neutralPlain"
|
||||||
|
__maxWidth={contentMaxWidth}
|
||||||
|
margin="auto"
|
||||||
|
/>
|
||||||
<DataEditor
|
<DataEditor
|
||||||
{...props}
|
{...cellProps}
|
||||||
|
{...datagridProps}
|
||||||
|
verticalBorder={verticalBorder}
|
||||||
|
headerIcons={headerIcons}
|
||||||
theme={datagridTheme}
|
theme={datagridTheme}
|
||||||
className={classes.datagrid}
|
className={classes.datagrid}
|
||||||
getCellContent={getCellContentEnh}
|
getCellContent={handleGetCellContent}
|
||||||
onCellEdited={onCellEditedEnh}
|
onCellEdited={handleOnCellEdited}
|
||||||
columns={columns}
|
columns={availableColumns}
|
||||||
rows={rowsTotal}
|
rows={rowsTotal}
|
||||||
freezeColumns={1}
|
freezeColumns={freezeColumns}
|
||||||
smoothScrollX
|
smoothScrollX
|
||||||
rowMarkers="checkbox"
|
rowMarkers={rowMarkers}
|
||||||
rowSelect="multi"
|
rowSelect="multi"
|
||||||
rowSelectionMode="multi"
|
rowSelectionMode="multi"
|
||||||
rangeSelect="multi-rect"
|
rangeSelect="multi-rect"
|
||||||
columnSelect="none"
|
columnSelect={columnSelect}
|
||||||
getCellsForSelection
|
getCellsForSelection
|
||||||
onColumnMoved={onColumnMoved}
|
onColumnMoved={handleColumnMoved}
|
||||||
onColumnResize={onColumnResize}
|
onColumnResize={handleColumnResize}
|
||||||
onGridSelectionChange={setSelection}
|
onHeaderClicked={handleHeaderClicked}
|
||||||
|
onCellClicked={handleCellClick}
|
||||||
|
onGridSelectionChange={handleGridSelectionChange}
|
||||||
|
onItemHovered={handleRowHover}
|
||||||
|
getRowThemeOverride={handleGetThemeOverride}
|
||||||
gridSelection={selection}
|
gridSelection={selection}
|
||||||
rowHeight={cellHeight}
|
rowHeight={cellHeight}
|
||||||
headerHeight={cellHeight}
|
headerHeight={cellHeight + 16}
|
||||||
ref={editor}
|
ref={editor}
|
||||||
onPaste
|
onPaste
|
||||||
rightElementProps={{
|
rightElementProps={{
|
||||||
|
@ -274,39 +402,38 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
<div
|
<div
|
||||||
className={clsx(classes.rowActionBar, {
|
className={clsx(classes.rowActionBar, {
|
||||||
[classes.rowActionBarScrolledToRight]: scrolledToRight,
|
[classes.rowActionBarScrolledToRight]: scrolledToRight,
|
||||||
|
[classes.rowActionvBarWithItems]: hasMenuItem,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(classes.rowActionBarShadow, {
|
className={clsx(classes.rowActionBarShadow, {
|
||||||
[classes.rowActionBarShadowActive]: !scrolledToRight,
|
[classes.rowActionBarShadowActive]:
|
||||||
|
!scrolledToRight && hasMenuItem,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<div className={classes.columnPicker}>
|
<div
|
||||||
<ColumnPicker
|
className={clsx(classes.columnPicker, {
|
||||||
IconButtonProps={{
|
[classes.columnPickerBackground]: !hasMenuItem,
|
||||||
className: classes.ghostIcon,
|
})}
|
||||||
variant: "ghost",
|
>
|
||||||
hoverOutline: false,
|
{renderColumnPicker
|
||||||
}}
|
? renderColumnPicker(defualtColumnPickerProps)
|
||||||
availableColumns={availableColumnsChoices}
|
: null}
|
||||||
initialColumns={columnChoices}
|
|
||||||
defaultColumns={defaultColumns}
|
|
||||||
onSave={onColumnsChange}
|
|
||||||
hasMore={false}
|
|
||||||
loading={false}
|
|
||||||
onFetchMore={() => undefined}
|
|
||||||
onQueryChange={picker.setQuery}
|
|
||||||
query={picker.query}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{hasColumnGroups && (
|
{hasColumnGroups && (
|
||||||
<div
|
<div
|
||||||
className={clsx(classes.rowAction, {
|
className={clsx(
|
||||||
[classes.rowActionScrolledToRight]: scrolledToRight,
|
classes.rowAction,
|
||||||
})}
|
classes.rowColumnGroup,
|
||||||
|
{
|
||||||
|
[classes.rowActionScrolledToRight]:
|
||||||
|
scrolledToRight,
|
||||||
|
},
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{Array(rowsTotal)
|
{hasMenuItem &&
|
||||||
|
Array(rowsTotal)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, index) => (
|
.map((_, index) => (
|
||||||
<RowActions
|
<RowActions
|
||||||
|
@ -318,6 +445,17 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
}
|
}
|
||||||
rowMarkerWidth={48}
|
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 */}
|
{/* FIXME: https://github.com/glideapps/glide-data-grid/issues/505 */}
|
||||||
{hasColumnGroups && (
|
{hasColumnGroups && (
|
||||||
<div className={classes.columnGroupFixer} />
|
<div className={classes.columnGroupFixer} />
|
||||||
|
@ -325,10 +463,22 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
<Box
|
||||||
|
borderTopStyle="solid"
|
||||||
|
borderTopWidth={1}
|
||||||
|
borderColor="neutralHighlight"
|
||||||
|
paddingY={9}
|
||||||
|
>
|
||||||
<Typography align="center">{emptyText}</Typography>
|
<Typography align="center">{emptyText}</Typography>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
<TooltipContainer
|
||||||
|
clearTooltip={clearTooltip}
|
||||||
|
bounds={tooltip?.bounds}
|
||||||
|
title={tooltip?.title}
|
||||||
|
/>
|
||||||
</FullScreenContainer>
|
</FullScreenContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useTheme } from "@saleor/macaw-ui";
|
||||||
import React, { CSSProperties, FC, PropsWithChildren } from "react";
|
import React, { CSSProperties, FC, PropsWithChildren } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
import { useDelayedState } from "./useDelayedState";
|
import { useDelayedState } from "../hooks/useDelayedState";
|
||||||
|
|
||||||
const modalRoot =
|
const modalRoot =
|
||||||
document.getElementById("modal-root") || document.createElement("div");
|
document.getElementById("modal-root") || document.createElement("div");
|
|
@ -3,7 +3,7 @@ import { Button, makeStyles, PlusSmallIcon } from "@saleor/macaw-ui";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import React, { FC, PropsWithChildren } from "react";
|
import React, { FC, PropsWithChildren } from "react";
|
||||||
|
|
||||||
import CardTitle from "../CardTitle";
|
import CardTitle from "../../CardTitle";
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
theme => ({
|
theme => ({
|
|
@ -1,8 +1,8 @@
|
||||||
import { IconButton, MoreHorizontalIcon } from "@saleor/macaw-ui";
|
import { IconButton, MoreHorizontalIcon } from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import CardMenu, { CardMenuItem } from "../CardMenu";
|
import CardMenu, { CardMenuItem } from "../../CardMenu";
|
||||||
import useStyles from "./styles";
|
import useStyles from "../styles";
|
||||||
|
|
||||||
interface RowActionsProps {
|
interface RowActionsProps {
|
||||||
menuItems: CardMenuItem[];
|
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, {
|
import SingleAutocompleteSelectField, {
|
||||||
SingleAutocompleteSelectFieldProps,
|
SingleAutocompleteSelectFieldProps,
|
||||||
} from "../SingleAutocompleteSelectField";
|
} from "../../SingleAutocompleteSelectField";
|
||||||
import { Choice } from "../SingleSelectField";
|
import { Choice } from "../../SingleSelectField";
|
||||||
|
|
||||||
export type DropdownChoice = Choice<string, string>;
|
export type DropdownChoice = Choice<string, string>;
|
||||||
export type DropdownCellContentProps = Pick<
|
export type DropdownCellContentProps = Pick<
|
|
@ -6,7 +6,7 @@ import {
|
||||||
} from "@glideapps/glide-data-grid";
|
} from "@glideapps/glide-data-grid";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { usePriceField } from "../PriceField/usePriceField";
|
import { usePriceField } from "../../PriceField/usePriceField";
|
||||||
|
|
||||||
interface MoneyCellProps {
|
interface MoneyCellProps {
|
||||||
readonly kind: "money-cell";
|
readonly kind: "money-cell";
|
|
@ -6,7 +6,7 @@ import {
|
||||||
} from "@glideapps/glide-data-grid";
|
} from "@glideapps/glide-data-grid";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Locale } from "../Locale";
|
import { Locale } from "../../Locale";
|
||||||
|
|
||||||
export const numberCellEmptyValue = Symbol();
|
export const numberCellEmptyValue = Symbol();
|
||||||
interface NumberCellProps {
|
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 {
|
import {
|
||||||
NumberCell,
|
NumberCell,
|
||||||
numberCellEmptyValue,
|
numberCellEmptyValue,
|
||||||
} from "@dashboard/components/Datagrid/NumberCell";
|
} from "@dashboard/components/Datagrid/customCells/NumberCell";
|
||||||
import { GridCell, GridCellKind } from "@glideapps/glide-data-grid";
|
import { GridCell, GridCellKind } from "@glideapps/glide-data-grid";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -10,6 +10,7 @@ import {
|
||||||
DropdownChoice,
|
DropdownChoice,
|
||||||
} from "./DropdownCell";
|
} from "./DropdownCell";
|
||||||
import { MoneyCell } from "./MoneyCell";
|
import { MoneyCell } from "./MoneyCell";
|
||||||
|
import { ThumbnailCell } from "./ThumbnailCell";
|
||||||
|
|
||||||
const common = {
|
const common = {
|
||||||
allowOverlay: true,
|
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 {
|
export function booleanCell(value: boolean): GridCell {
|
||||||
return {
|
return {
|
||||||
...common,
|
...common,
|
||||||
|
@ -34,6 +49,13 @@ export function booleanCell(value: boolean): GridCell {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadingCell(): GridCell {
|
||||||
|
return {
|
||||||
|
kind: GridCellKind.Loading,
|
||||||
|
allowOverlay: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function numberCell(
|
export function numberCell(
|
||||||
value: number | typeof numberCellEmptyValue,
|
value: number | typeof numberCellEmptyValue,
|
||||||
): NumberCell {
|
): NumberCell {
|
||||||
|
@ -63,16 +85,18 @@ export function moneyCell(value: number | null, currency: string): MoneyCell {
|
||||||
|
|
||||||
export function dropdownCell(
|
export function dropdownCell(
|
||||||
value: DropdownChoice,
|
value: DropdownChoice,
|
||||||
opts: DropdownCellContentProps &
|
dataOpts: DropdownCellContentProps &
|
||||||
(
|
(
|
||||||
| { choices: DropdownChoice[] }
|
| { choices: DropdownChoice[] }
|
||||||
| { update: (text: string) => Promise<DropdownChoice[]> }
|
| { update: (text: string) => Promise<DropdownChoice[]> }
|
||||||
),
|
),
|
||||||
|
opts?: Partial<GridCell>,
|
||||||
): DropdownCell {
|
): DropdownCell {
|
||||||
return {
|
return {
|
||||||
...common,
|
...common,
|
||||||
data: {
|
|
||||||
...opts,
|
...opts,
|
||||||
|
data: {
|
||||||
|
...dataOpts,
|
||||||
kind: "dropdown-cell",
|
kind: "dropdown-cell",
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
|
@ -80,3 +104,21 @@ export function dropdownCell(
|
||||||
copyData: value.label,
|
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 { dropdownCellRenderer } from "./DropdownCell";
|
||||||
import { moneyCellRenderer } from "./MoneyCell";
|
import { moneyCellRenderer } from "./MoneyCell";
|
||||||
import { numberCellRenderer } from "./NumberCell";
|
import { numberCellRenderer } from "./NumberCell";
|
||||||
|
import { thumbnailCellRenderer } from "./ThumbnailCell";
|
||||||
|
|
||||||
function useCells() {
|
function useCells() {
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
|
@ -13,6 +14,7 @@ function useCells() {
|
||||||
moneyCellRenderer(),
|
moneyCellRenderer(),
|
||||||
numberCellRenderer(locale),
|
numberCellRenderer(locale),
|
||||||
dropdownCellRenderer,
|
dropdownCellRenderer,
|
||||||
|
thumbnailCellRenderer,
|
||||||
],
|
],
|
||||||
[locale],
|
[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 { GridColumn } from "@glideapps/glide-data-grid";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
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 [query, setQuery] = useState("");
|
||||||
const [displayedColumns, setDisplayedColumns] = useStateFromProps(
|
const [displayedColumns, setDisplayedColumns] = useStateFromProps(
|
||||||
availableColumns.map(({ id }) => id),
|
availableColumns.map(({ id }) => id),
|
||||||
|
@ -18,7 +20,7 @@ function useColumns(availableColumns: readonly AvailableColumn[]) {
|
||||||
addAtIndex(old[startIndex], removeAtIndex(old, startIndex), endIndex),
|
addAtIndex(old[startIndex], removeAtIndex(old, startIndex), endIndex),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[],
|
[setDisplayedColumns],
|
||||||
);
|
);
|
||||||
const onColumnResize = useCallback(
|
const onColumnResize = useCallback(
|
||||||
(column: GridColumn, newSize: number) =>
|
(column: GridColumn, newSize: number) =>
|
||||||
|
@ -29,7 +31,7 @@ function useColumns(availableColumns: readonly AvailableColumn[]) {
|
||||||
: prevColumn,
|
: prevColumn,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
[],
|
[setColumnState],
|
||||||
);
|
);
|
||||||
const onColumnsChange = useCallback(
|
const onColumnsChange = useCallback(
|
||||||
(picked: string[]) =>
|
(picked: string[]) =>
|
||||||
|
@ -39,7 +41,7 @@ function useColumns(availableColumns: readonly AvailableColumn[]) {
|
||||||
.filter(column => !prevColumns.find(c => c === column))
|
.filter(column => !prevColumns.find(c => c === column))
|
||||||
.map(column => availableColumns.find(ac => ac.id === column).id),
|
.map(column => availableColumns.find(ac => ac.id === column).id),
|
||||||
]),
|
]),
|
||||||
[availableColumns],
|
[availableColumns, setDisplayedColumns],
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
|
@ -62,9 +64,10 @@ function useColumns(availableColumns: readonly AvailableColumn[]) {
|
||||||
})),
|
})),
|
||||||
[availableColumns],
|
[availableColumns],
|
||||||
);
|
);
|
||||||
const defaultColumns = useMemo(() => availableColumns.map(({ id }) => id), [
|
const defaultColumns = useMemo(
|
||||||
availableColumns,
|
() => availableColumns.map(({ id }) => id),
|
||||||
]);
|
[availableColumns],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
availableColumnsChoices,
|
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 { act, renderHook } from "@testing-library/react-hooks";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { AvailableColumn } from "./types";
|
import { AvailableColumn } from "../types";
|
||||||
import useDatagridChange, {
|
import useDatagridChange, {
|
||||||
DatagridChangeStateContext,
|
DatagridChangeStateContext,
|
||||||
useDatagridChangeState,
|
useDatagridChangeState,
|
|
@ -11,7 +11,7 @@ import {
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { AvailableColumn } from "./types";
|
import { AvailableColumn } from "../types";
|
||||||
|
|
||||||
export interface DatagridChange {
|
export interface DatagridChange {
|
||||||
data: any;
|
data: any;
|
||||||
|
@ -32,13 +32,21 @@ export interface UseDatagridChangeState {
|
||||||
removed: number[];
|
removed: number[];
|
||||||
setRemoved: Dispatch<SetStateAction<number[]>>;
|
setRemoved: Dispatch<SetStateAction<number[]>>;
|
||||||
changes: MutableRefObject<DatagridChange[]>;
|
changes: MutableRefObject<DatagridChange[]>;
|
||||||
|
clear: () => void;
|
||||||
}
|
}
|
||||||
export function useDatagridChangeState(): UseDatagridChangeState {
|
export function useDatagridChangeState(): UseDatagridChangeState {
|
||||||
const [added, setAdded] = useState<number[]>([]);
|
const [added, setAdded] = useState<number[]>([]);
|
||||||
const [removed, setRemoved] = useState<number[]>([]);
|
const [removed, setRemoved] = useState<number[]>([]);
|
||||||
const changes = useRef<DatagridChange[]>([]);
|
const changes = useRef<DatagridChange[]>([]);
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
changes.current = [];
|
||||||
|
setAdded([]);
|
||||||
|
setRemoved([]);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
clear,
|
||||||
added,
|
added,
|
||||||
setAdded,
|
setAdded,
|
||||||
removed,
|
removed,
|
||||||
|
@ -47,9 +55,8 @@ export function useDatagridChangeState(): UseDatagridChangeState {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DatagridChangeStateContext = createContext<UseDatagridChangeState>(
|
export const DatagridChangeStateContext =
|
||||||
undefined,
|
createContext<UseDatagridChangeState>(undefined);
|
||||||
);
|
|
||||||
export const useDatagridChangeStateContext = () =>
|
export const useDatagridChangeStateContext = () =>
|
||||||
useContext(DatagridChangeStateContext);
|
useContext(DatagridChangeStateContext);
|
||||||
|
|
||||||
|
@ -58,13 +65,8 @@ function useDatagridChange(
|
||||||
rows: number,
|
rows: number,
|
||||||
onChange?: OnDatagridChange,
|
onChange?: OnDatagridChange,
|
||||||
) {
|
) {
|
||||||
const {
|
const { added, setAdded, removed, setRemoved, changes } =
|
||||||
added,
|
useDatagridChangeStateContext();
|
||||||
setAdded,
|
|
||||||
removed,
|
|
||||||
setRemoved,
|
|
||||||
changes,
|
|
||||||
} = useDatagridChangeStateContext();
|
|
||||||
const getChangeIndex = useCallback(
|
const getChangeIndex = useCallback(
|
||||||
(column: string, row: number): number =>
|
(column: string, row: number): number =>
|
||||||
changes.current.findIndex(
|
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 { Theme } from "@glideapps/glide-data-grid";
|
||||||
import { makeStyles, useTheme } from "@saleor/macaw-ui";
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
import { themes } from "@saleor/macaw-ui/next";
|
import { useTheme, vars } from "@saleor/macaw-ui/next";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export const cellHeight = 36;
|
export const cellHeight = 36;
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
theme => {
|
() => {
|
||||||
const rowActionSelected = {
|
const rowActionSelected = {
|
||||||
background: theme.palette.background.paper,
|
background: vars.colors.background.plain,
|
||||||
color: theme.palette.saleor.main[1],
|
color: vars.colors.border.neutralHighlight,
|
||||||
};
|
};
|
||||||
const activeBorderColor =
|
const activeBorderColor = vars.colors.border.neutralDefault;
|
||||||
theme.palette.saleor.theme === "light" ? "#D4D4D4" : "#232323";
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actionBtnBar: {
|
actionBtnBar: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
background: theme.palette.background.paper,
|
background: vars.colors.background.plain,
|
||||||
borderRadius: 8,
|
borderRadius: vars.borderRadius[4],
|
||||||
// Right and left toolbars
|
// Right and left toolbars
|
||||||
width: `calc(100% - 64px - ${cellHeight} - 1px)`,
|
width: `calc(100% - 64px - ${cellHeight} - 1px)`,
|
||||||
marginTop: 1,
|
marginTop: 1,
|
||||||
|
@ -28,16 +27,20 @@ const useStyles = makeStyles(
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
padding: theme.spacing(1),
|
padding: vars.space[4],
|
||||||
},
|
},
|
||||||
columnPicker: {
|
columnPicker: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
height: cellHeight,
|
height: cellHeight + 16,
|
||||||
|
},
|
||||||
|
columnPickerBackground: {
|
||||||
|
background: vars.colors.background.plain,
|
||||||
},
|
},
|
||||||
ghostIcon: {
|
ghostIcon: {
|
||||||
color: theme.palette.saleor.main[3],
|
color: vars.colors.foreground.iconNeutralPlain,
|
||||||
|
padding: vars.space["3"],
|
||||||
},
|
},
|
||||||
portal: {
|
portal: {
|
||||||
"& input::-webkit-outer-spin-button, input::-webkit-inner-spin-button":
|
"& input::-webkit-outer-spin-button, input::-webkit-inner-spin-button":
|
||||||
|
@ -49,14 +52,14 @@ const useStyles = makeStyles(
|
||||||
appearance: "textfield",
|
appearance: "textfield",
|
||||||
},
|
},
|
||||||
"& .clip-region": {
|
"& .clip-region": {
|
||||||
border: `1px solid ${theme.palette.saleor.main[1]}`,
|
border: `1px solid ${vars.colors.border.brandSubdued}`,
|
||||||
},
|
},
|
||||||
"& .gdg-growing-entry": {
|
"& .gdg-growing-entry": {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
},
|
},
|
||||||
"& .gdg-style": {
|
"& .gdg-style": {
|
||||||
background: theme.palette.background.paper,
|
background: vars.colors.background.plain,
|
||||||
border: "none",
|
border: "none",
|
||||||
// Setting these with !important because we never intend to style
|
// Setting these with !important because we never intend to style
|
||||||
// this particular element, like, never ever
|
// this particular element, like, never ever
|
||||||
|
@ -67,11 +70,11 @@ const useStyles = makeStyles(
|
||||||
appearance: "none",
|
appearance: "none",
|
||||||
background: "none",
|
background: "none",
|
||||||
border: "none",
|
border: "none",
|
||||||
fontSize: themes.defaultLight.fontSize.bodySmall,
|
fontSize: vars.fontSize.bodySmall,
|
||||||
letterSpacing: "0.015em",
|
letterSpacing: vars.letterSpacing.bodyStrongSmall,
|
||||||
lineHeight: themes.defaultLight.lineHeight.bodySmall,
|
lineHeight: vars.lineHeight.bodyEmpMedium,
|
||||||
fontWeight: themes.defaultLight.fontWeight.bodySmall,
|
fontWeight: vars.fontWeight.bodySmall,
|
||||||
padding: themes.defaultLight.space[3],
|
padding: vars.space[3],
|
||||||
outline: 0,
|
outline: 0,
|
||||||
},
|
},
|
||||||
'& input[type="number"]': {
|
'& input[type="number"]': {
|
||||||
|
@ -86,26 +89,29 @@ const useStyles = makeStyles(
|
||||||
datagrid: {
|
datagrid: {
|
||||||
"& .dvn-scroller": {
|
"& .dvn-scroller": {
|
||||||
overscrollBehaviorX: "none",
|
overscrollBehaviorX: "none",
|
||||||
|
scrollbarWidth: "none",
|
||||||
|
},
|
||||||
|
"& .dvn-scroller::-webkit-scrollbar": {
|
||||||
|
display: "none",
|
||||||
},
|
},
|
||||||
borderTop: `1px solid ${theme.palette.divider}`,
|
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
boxSizing: "content-box",
|
boxSizing: "content-box",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
paddingBottom: "1px",
|
paddingBottom: "1px",
|
||||||
color: "red",
|
|
||||||
},
|
},
|
||||||
root: {
|
root: {
|
||||||
position: "relative",
|
position: "relative",
|
||||||
},
|
},
|
||||||
rowActionBar: {
|
rowActionBar: {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
background: theme.palette.background.paper,
|
|
||||||
borderLeft: `1px solid ${activeBorderColor}`,
|
|
||||||
width: 36,
|
width: 36,
|
||||||
},
|
},
|
||||||
|
rowActionvBarWithItems: {
|
||||||
|
borderLeft: `1px solid ${activeBorderColor}`,
|
||||||
|
background: vars.colors.background.plain,
|
||||||
|
},
|
||||||
rowActionBarScrolledToRight: {
|
rowActionBarScrolledToRight: {
|
||||||
borderLeftColor: theme.palette.divider,
|
borderLeftColor: vars.colors.border.neutralHighlight,
|
||||||
},
|
},
|
||||||
rowAction: {
|
rowAction: {
|
||||||
"&:hover, $rowActionSelected": {
|
"&:hover, $rowActionSelected": {
|
||||||
|
@ -114,19 +120,22 @@ const useStyles = makeStyles(
|
||||||
"&:not(:last-child)": {
|
"&:not(:last-child)": {
|
||||||
marginBottom: -1,
|
marginBottom: -1,
|
||||||
},
|
},
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
border: `1px solid ${vars.colors.border.neutralHighlight}`,
|
||||||
borderLeftColor: activeBorderColor,
|
borderLeft: "none",
|
||||||
borderRight: "none",
|
borderRight: "none",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
color: theme.palette.saleor.main[5],
|
color: vars.colors.foreground.iconNeutralPlain,
|
||||||
marginLeft: -1,
|
marginLeft: -1,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
height: `calc(${cellHeight}px - 1px)`,
|
height: `calc(${cellHeight}px - 1px)`,
|
||||||
},
|
},
|
||||||
|
rowColumnGroup: {
|
||||||
|
height: cellHeight + 15,
|
||||||
|
},
|
||||||
rowActionScrolledToRight: {
|
rowActionScrolledToRight: {
|
||||||
borderLeftColor: theme.palette.divider,
|
borderLeftColor: vars.colors.border.neutralHighlight,
|
||||||
},
|
},
|
||||||
columnGroupFixer: {
|
columnGroupFixer: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
@ -135,7 +144,7 @@ const useStyles = makeStyles(
|
||||||
height: cellHeight,
|
height: cellHeight,
|
||||||
width: 10,
|
width: 10,
|
||||||
borderLeft: 0,
|
borderLeft: 0,
|
||||||
background: theme.palette.background.paper,
|
background: vars.colors.background.plain,
|
||||||
},
|
},
|
||||||
editorContainer: {
|
editorContainer: {
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
@ -146,9 +155,7 @@ const useStyles = makeStyles(
|
||||||
width: 1,
|
width: 1,
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
transition: theme.transitions.create("box-shadow", {
|
transition: "box-shadow .2s ease-in-out",
|
||||||
duration: theme.transitions.duration.short,
|
|
||||||
}),
|
|
||||||
boxShadow: "-1px 0px 12px transparent",
|
boxShadow: "-1px 0px 12px transparent",
|
||||||
},
|
},
|
||||||
rowActionBarShadowActive: {
|
rowActionBarShadowActive: {
|
||||||
|
@ -156,8 +163,12 @@ const useStyles = makeStyles(
|
||||||
},
|
},
|
||||||
rowActionSelected,
|
rowActionSelected,
|
||||||
cardContentRoot: {
|
cardContentRoot: {
|
||||||
padding: "0 0 2.4rem 0",
|
padding: "0",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
||||||
|
"&:last-child": {
|
||||||
|
padding: "0",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -180,38 +191,53 @@ export const useFullScreenStyles = makeStyles<ReturnType<typeof useStyles>>(
|
||||||
{ name: "Datagrid-fullscreen" },
|
{ name: "Datagrid-fullscreen" },
|
||||||
);
|
);
|
||||||
|
|
||||||
export function useDatagridTheme() {
|
export function useDatagridTheme(
|
||||||
const theme = useTheme();
|
readonly?: boolean,
|
||||||
|
hasHeaderClickable?: boolean,
|
||||||
|
) {
|
||||||
|
const { themeValues } = useTheme();
|
||||||
|
|
||||||
const datagridTheme = useMemo(
|
const datagridTheme = useMemo(
|
||||||
(): Partial<Theme> => ({
|
(): Partial<Theme> => ({
|
||||||
accentColor: theme.palette.secondary.main,
|
accentColor: themeValues.colors.background.interactiveBrandDefault,
|
||||||
accentLight: theme.palette.background.default,
|
accentLight:
|
||||||
|
themeValues.colors.background.interactiveBrandSecondaryPressing,
|
||||||
accentFg: "transparent",
|
accentFg: "transparent",
|
||||||
bgCell: theme.palette.background.paper,
|
bgCell: themeValues.colors.background.plain,
|
||||||
bgHeader: theme.palette.background.paper,
|
bgHeader: themeValues.colors.background.plain,
|
||||||
bgHeaderHasFocus: theme.palette.background.paper,
|
bgHeaderHasFocus: themeValues.colors.background.plain,
|
||||||
bgHeaderHovered: theme.palette.background.paper,
|
bgHeaderHovered: hasHeaderClickable
|
||||||
bgBubbleSelected: theme.palette.background.paper,
|
? themeValues.colors.background.surfaceNeutralHighlight
|
||||||
textHeader: theme.palette.text.secondary,
|
: themeValues.colors.background.plain,
|
||||||
borderColor: theme.palette.divider,
|
bgBubbleSelected: themeValues.colors.background.plain,
|
||||||
|
textHeader: themeValues.colors.foreground.iconNeutralPlain,
|
||||||
|
borderColor: themeValues.colors.border.neutralHighlight,
|
||||||
fontFamily: "'Inter var', sans-serif",
|
fontFamily: "'Inter var', sans-serif",
|
||||||
baseFontStyle: themes.defaultLight.fontSize.bodySmall,
|
baseFontStyle: themeValues.fontSize.bodySmall,
|
||||||
headerFontStyle: themes.defaultLight.fontSize.bodySmall,
|
headerFontStyle: themeValues.fontSize.captionSmall,
|
||||||
editorFontSize: themes.defaultLight.fontSize.bodySmall,
|
editorFontSize: themeValues.fontSize.bodySmall,
|
||||||
textMedium: theme.palette.text.primary,
|
textMedium: themeValues.colors.background.interactiveNeutralDefault,
|
||||||
textGroupHeader: theme.palette.text.secondary,
|
textGroupHeader: themeValues.colors.foreground.iconNeutralPlain,
|
||||||
textBubble: theme.palette.text.primary,
|
textBubble: themeValues.colors.background.interactiveNeutralDefault,
|
||||||
textDark: theme.palette.text.primary,
|
textDark: themeValues.colors.background.interactiveNeutralDefault,
|
||||||
textLight: theme.palette.text.primary,
|
textLight: themeValues.colors.background.interactiveNeutralDefault,
|
||||||
|
textHeaderSelected: themeValues.colors.foreground.textBrandDefault,
|
||||||
cellHorizontalPadding: 8,
|
cellHorizontalPadding: 8,
|
||||||
cellVerticalPadding: 8,
|
cellVerticalPadding: 8,
|
||||||
lineHeight: 20,
|
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;
|
export default useStyles;
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
import { Theme } from "@glideapps/glide-data-grid";
|
||||||
|
|
||||||
export interface AvailableColumn {
|
export interface AvailableColumn {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
width: number;
|
width: number;
|
||||||
group?: string;
|
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 {
|
SearchBarProps {
|
||||||
errorMessages?: FilterErrorMessages<TKeys>;
|
errorMessages?: FilterErrorMessages<TKeys>;
|
||||||
filterStructure: IFilter<TKeys>;
|
filterStructure: IFilter<TKeys>;
|
||||||
|
withoutBorder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles<{ withoutBorder?: boolean }>(
|
||||||
theme => ({
|
theme => ({
|
||||||
root: {
|
root: {
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
borderBottom: props =>
|
||||||
|
props.withoutBorder ? "none" : `1px solid ${theme.palette.divider}`,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
padding: theme.spacing(1, 4),
|
padding: theme.spacing(1, 4),
|
||||||
|
|
|
@ -21,11 +21,8 @@ export const useExitFormDialog = (
|
||||||
const id = useRef(formId || Symbol()).current;
|
const id = useRef(formId || Symbol()).current;
|
||||||
|
|
||||||
const exitDialogProps = useContext(ExitFormDialogContext);
|
const exitDialogProps = useContext(ExitFormDialogContext);
|
||||||
const {
|
const { setIsDirty, setIsSubmitDisabled, setExitDialogSubmitRef } =
|
||||||
setIsDirty,
|
exitDialogProps;
|
||||||
setIsSubmitDisabled,
|
|
||||||
setExitDialogSubmitRef,
|
|
||||||
} = exitDialogProps;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isDisabled !== undefined) {
|
if (isDisabled !== undefined) {
|
||||||
|
|
|
@ -50,10 +50,10 @@ export const formatMoneyRange = (
|
||||||
locale: string,
|
locale: string,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const formattedMoneyRange = (Intl.NumberFormat(locale, {
|
const formattedMoneyRange = Intl.NumberFormat(locale, {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: moneyFrom.currency,
|
currency: moneyFrom.currency,
|
||||||
}) as any).formatRange(moneyFrom.amount, moneyTo.amount);
|
}).formatRange(moneyFrom.amount, moneyTo.amount);
|
||||||
// TODO: remove casting from formatRange when typescript
|
// TODO: remove casting from formatRange when typescript
|
||||||
// is updated to 4.7 or higher
|
// is updated to 4.7 or higher
|
||||||
return formattedMoneyRange;
|
return formattedMoneyRange;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import { LocaleConsumer } from "../Locale";
|
import { LocaleConsumer } from "../Locale";
|
||||||
import { formatMoney, formatMoneyRange } from "../Money";
|
import { getMoneyRange } from "./utils";
|
||||||
|
|
||||||
export interface MoneyRangeProps {
|
export interface MoneyRangeProps {
|
||||||
from?: IMoney;
|
from?: IMoney;
|
||||||
|
@ -15,38 +15,7 @@ export const MoneyRange: React.FC<MoneyRangeProps> = ({ from, to }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocaleConsumer>
|
<LocaleConsumer>
|
||||||
{({ locale }) => {
|
{({ locale }) => getMoneyRange(locale, intl, from, to)}
|
||||||
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 "-";
|
|
||||||
}}
|
|
||||||
</LocaleConsumer>
|
</LocaleConsumer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export { default } from "./MoneyRange";
|
export { default } from "./MoneyRange";
|
||||||
export * 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 {
|
import {
|
||||||
Pagination,
|
Pagination,
|
||||||
PaginationProps as MacawPaginationProps,
|
PaginationProps as MacawPaginationProps,
|
||||||
|
PaginationRowNumberSelectLabels,
|
||||||
} from "@saleor/macaw-ui";
|
} from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
@ -27,6 +28,7 @@ export interface PaginationProps
|
||||||
prevHref?: string;
|
prevHref?: string;
|
||||||
nextHref?: string;
|
nextHref?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
labels?: PaginationRowNumberSelectLabels;
|
||||||
}
|
}
|
||||||
export const TablePagination: React.FC<PaginationProps> = ({
|
export const TablePagination: React.FC<PaginationProps> = ({
|
||||||
component,
|
component,
|
||||||
|
@ -38,6 +40,7 @@ export const TablePagination: React.FC<PaginationProps> = ({
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
hasPreviousPage,
|
hasPreviousPage,
|
||||||
disabled,
|
disabled,
|
||||||
|
labels,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
@ -50,7 +53,8 @@ export const TablePagination: React.FC<PaginationProps> = ({
|
||||||
hasNextPage={hasNextPage && !disabled}
|
hasNextPage={hasNextPage && !disabled}
|
||||||
hasPreviousPage={hasPreviousPage && !disabled}
|
hasPreviousPage={hasPreviousPage && !disabled}
|
||||||
labels={{
|
labels={{
|
||||||
noOfRows: intl.formatMessage(commonMessages.noOfRows),
|
noOfRows:
|
||||||
|
labels?.noOfRows ?? intl.formatMessage(commonMessages.noOfRows),
|
||||||
}}
|
}}
|
||||||
rowNumber={settings?.rowNumber}
|
rowNumber={settings?.rowNumber}
|
||||||
onRowNumberUpdate={
|
onRowNumberUpdate={
|
||||||
|
|
|
@ -6,4 +6,8 @@ export const commonTooltipMessages = defineMessages({
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
"Sorting by this column requires active filter: {filterName}",
|
"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 =
|
export type ProductListColumns =
|
||||||
| "productType"
|
| "productType"
|
||||||
|
| "description"
|
||||||
| "availability"
|
| "availability"
|
||||||
| "price"
|
| "price"
|
||||||
| "date";
|
| "date";
|
||||||
|
@ -92,7 +93,7 @@ export const defaultListSettings: AppListViewSettings = {
|
||||||
rowNumber: PAGINATE_BY,
|
rowNumber: PAGINATE_BY,
|
||||||
},
|
},
|
||||||
[ListViews.PRODUCT_LIST]: {
|
[ListViews.PRODUCT_LIST]: {
|
||||||
columns: ["availability", "price", "productType", "date"],
|
columns: ["availability", "description", "price", "productType", "date"],
|
||||||
rowNumber: PAGINATE_BY,
|
rowNumber: PAGINATE_BY,
|
||||||
},
|
},
|
||||||
[ListViews.SALES_LIST]: {
|
[ListViews.SALES_LIST]: {
|
||||||
|
|
|
@ -19,11 +19,11 @@ const attachVariablesLink = new ApolloLink((operation, forward) =>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const link = attachVariablesLink.concat(
|
export const link = attachVariablesLink.concat(
|
||||||
(createUploadLink({
|
createUploadLink({
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
uri: getApiUrl(),
|
uri: getApiUrl(),
|
||||||
fetch: createFetch(),
|
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({
|
export const apolloClient = new ApolloClient({
|
||||||
|
|
|
@ -12684,6 +12684,7 @@ export const ProductListDocument = gql`
|
||||||
node {
|
node {
|
||||||
...ProductWithChannelListings
|
...ProductWithChannelListings
|
||||||
updatedAt
|
updatedAt
|
||||||
|
description
|
||||||
attributes @include(if: $hasSelectedAttributes) {
|
attributes @include(if: $hasSelectedAttributes) {
|
||||||
...ProductListAttribute
|
...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<{
|
export type ProductCountQueryVariables = Exact<{
|
||||||
filter?: InputMaybe<ProductFilterInput>;
|
filter?: InputMaybe<ProductFilterInput>;
|
||||||
|
|
|
@ -151,6 +151,10 @@ export const commonMessages = defineMessages({
|
||||||
id: "RrCui3",
|
id: "RrCui3",
|
||||||
defaultMessage: "Summary",
|
defaultMessage: "Summary",
|
||||||
},
|
},
|
||||||
|
type: {
|
||||||
|
defaultMessage: "Type",
|
||||||
|
id: "+U6ozc",
|
||||||
|
},
|
||||||
translationAttributes: {
|
translationAttributes: {
|
||||||
id: "+xTpT1",
|
id: "+xTpT1",
|
||||||
defaultMessage: "Attributes",
|
defaultMessage: "Attributes",
|
||||||
|
@ -203,6 +207,10 @@ export const commonMessages = defineMessages({
|
||||||
defaultMessage: "No. of rows",
|
defaultMessage: "No. of rows",
|
||||||
description: "pagination",
|
description: "pagination",
|
||||||
},
|
},
|
||||||
|
product: {
|
||||||
|
defaultMessage: "Product",
|
||||||
|
id: "x/ZVlU",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const errorMessages = defineMessages({
|
export const errorMessages = defineMessages({
|
||||||
|
|
|
@ -105,8 +105,7 @@ const props: OrderGrantRefundPageProps = {
|
||||||
id: "RnVsZmlsbG1lbnRMaW5lOjIw",
|
id: "RnVsZmlsbG1lbnRMaW5lOjIw",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
orderLine: {
|
orderLine: {
|
||||||
id:
|
id: "T3JkZXJMaW5lOjIxOTM0YTBjLWQ2NGQtNDdlMS05ZTc4LTJmMjIyYzU5NjQ5ZA==",
|
||||||
"T3JkZXJMaW5lOjIxOTM0YTBjLWQ2NGQtNDdlMS05ZTc4LTJmMjIyYzU5NjQ5ZA==",
|
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
url: placeholderImage,
|
url: placeholderImage,
|
||||||
__typename: "Image",
|
__typename: "Image",
|
||||||
|
@ -139,8 +138,7 @@ const props: OrderGrantRefundPageProps = {
|
||||||
id: "RnVsZmlsbG1lbnRMaW5lOjIx",
|
id: "RnVsZmlsbG1lbnRMaW5lOjIx",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
orderLine: {
|
orderLine: {
|
||||||
id:
|
id: "T3JkZXJMaW5lOjIxOTM0YTBjLWQ2NGQtNDdlMS05ZTc4LTJmMjIyYzU5NjQ5ZA==",
|
||||||
"T3JkZXJMaW5lOjIxOTM0YTBjLWQ2NGQtNDdlMS05ZTc4LTJmMjIyYzU5NjQ5ZA==",
|
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
url: placeholderImage,
|
url: placeholderImage,
|
||||||
__typename: "Image",
|
__typename: "Image",
|
||||||
|
@ -173,8 +171,7 @@ const props: OrderGrantRefundPageProps = {
|
||||||
id: "RnVsZmlsbG1lbnRMaW5lOjIy",
|
id: "RnVsZmlsbG1lbnRMaW5lOjIy",
|
||||||
quantity: 2,
|
quantity: 2,
|
||||||
orderLine: {
|
orderLine: {
|
||||||
id:
|
id: "T3JkZXJMaW5lOmU0N2UyN2IyLTQzZWYtNGY5OS05ZTU0LTkyZmZkZWE5MmY4OA==",
|
||||||
"T3JkZXJMaW5lOmU0N2UyN2IyLTQzZWYtNGY5OS05ZTU0LTkyZmZkZWE5MmY4OA==",
|
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
url: placeholderImage,
|
url: placeholderImage,
|
||||||
__typename: "Image",
|
__typename: "Image",
|
||||||
|
@ -199,8 +196,7 @@ const props: OrderGrantRefundPageProps = {
|
||||||
id: "RnVsZmlsbG1lbnRMaW5lOjIz",
|
id: "RnVsZmlsbG1lbnRMaW5lOjIz",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
orderLine: {
|
orderLine: {
|
||||||
id:
|
id: "T3JkZXJMaW5lOmZjOWU1YzFlLWFiMDItNDU5MS05YTRmLTA5YmJiN2U0NmZlMg==",
|
||||||
"T3JkZXJMaW5lOmZjOWU1YzFlLWFiMDItNDU5MS05YTRmLTA5YmJiN2U0NmZlMg==",
|
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
url: placeholderImage,
|
url: placeholderImage,
|
||||||
__typename: "Image",
|
__typename: "Image",
|
||||||
|
|
|
@ -37,9 +37,9 @@ export const ProductsCard: React.FC<ProductsCardProps> = ({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getHandleAmountChange = (line: OrderLineGrantRefundFragment) => (
|
const getHandleAmountChange =
|
||||||
e: React.ChangeEvent<HTMLInputElement>,
|
(line: OrderLineGrantRefundFragment) =>
|
||||||
) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const parsedValue = parseInt(e.target.value, 10);
|
const parsedValue = parseInt(e.target.value, 10);
|
||||||
const value = Number.isNaN(parsedValue) ? 0 : parsedValue;
|
const value = Number.isNaN(parsedValue) ? 0 : parsedValue;
|
||||||
|
|
||||||
|
|
|
@ -36,10 +36,9 @@ type OrderManualTransactionDialogProps = {
|
||||||
dialogProps: DialogProps;
|
dialogProps: DialogProps;
|
||||||
} & OrderManualTransactionFormProps;
|
} & OrderManualTransactionFormProps;
|
||||||
|
|
||||||
export const OrderManualTransactionDialog: React.FC<OrderManualTransactionDialogProps> = ({
|
export const OrderManualTransactionDialog: React.FC<
|
||||||
dialogProps,
|
OrderManualTransactionDialogProps
|
||||||
...props
|
> = ({ dialogProps, ...props }) => {
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
|
|
|
@ -18,10 +18,9 @@ export interface OrderManualTransactionFormProps {
|
||||||
initialData?: Partial<OrderManualTransactionSubmitVariables>;
|
initialData?: Partial<OrderManualTransactionSubmitVariables>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OrderManualTransactionForm: React.FC<OrderManualTransactionFormProps> = ({
|
export const OrderManualTransactionForm: React.FC<
|
||||||
children,
|
OrderManualTransactionFormProps
|
||||||
...props
|
> = ({ children, ...props }) => {
|
||||||
}) => {
|
|
||||||
const { submitState, initialData } = props;
|
const { submitState, initialData } = props;
|
||||||
const hookData = useManualRefund({ submitState, initialData });
|
const hookData = useManualRefund({ submitState, initialData });
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,11 @@ import React from "react";
|
||||||
|
|
||||||
import { useManualTransactionContext } from "../context";
|
import { useManualTransactionContext } from "../context";
|
||||||
|
|
||||||
export const DescriptionField: React.FC<Omit<
|
export const DescriptionField: React.FC<
|
||||||
TextFieldProps,
|
Omit<TextFieldProps, "onChange" | "value">
|
||||||
"onChange" | "value"
|
> = ({ disabled, ...props }) => {
|
||||||
>> = ({ disabled, ...props }) => {
|
const { submitState, handleChangeDescription, description } =
|
||||||
const {
|
useManualTransactionContext();
|
||||||
submitState,
|
|
||||||
handleChangeDescription,
|
|
||||||
description,
|
|
||||||
} = useManualTransactionContext();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
|
|
|
@ -6,12 +6,8 @@ export const Form: React.FC<React.HTMLProps<HTMLFormElement>> = ({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const { amount, description, pspReference, onAddTransaction } =
|
||||||
amount,
|
useManualTransactionContext();
|
||||||
description,
|
|
||||||
pspReference,
|
|
||||||
onAddTransaction,
|
|
||||||
} = useManualTransactionContext();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
|
|
|
@ -3,16 +3,11 @@ import React from "react";
|
||||||
|
|
||||||
import { useManualTransactionContext } from "../context";
|
import { useManualTransactionContext } from "../context";
|
||||||
|
|
||||||
export const PriceInputField: React.FC<Omit<
|
export const PriceInputField: React.FC<
|
||||||
PriceFieldProps,
|
Omit<PriceFieldProps, "currencySymbol" | "onChange" | "value">
|
||||||
"currencySymbol" | "onChange" | "value"
|
> = ({ disabled, ...props }) => {
|
||||||
>> = ({ disabled, ...props }) => {
|
const { currency, submitState, handleChangeAmount, amount } =
|
||||||
const {
|
useManualTransactionContext();
|
||||||
currency,
|
|
||||||
submitState,
|
|
||||||
handleChangeAmount,
|
|
||||||
amount,
|
|
||||||
} = useManualTransactionContext();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PriceField
|
<PriceField
|
||||||
|
|
|
@ -3,15 +3,11 @@ import React from "react";
|
||||||
|
|
||||||
import { useManualTransactionContext } from "../context";
|
import { useManualTransactionContext } from "../context";
|
||||||
|
|
||||||
export const PspReferenceField: React.FC<Omit<
|
export const PspReferenceField: React.FC<
|
||||||
TextFieldProps,
|
Omit<TextFieldProps, "onChange" | "value">
|
||||||
"onChange" | "value"
|
> = ({ disabled, variant = "outlined", ...props }) => {
|
||||||
>> = ({ disabled, variant = "outlined", ...props }) => {
|
const { submitState, pspReference, handleChangePspReference } =
|
||||||
const {
|
useManualTransactionContext();
|
||||||
submitState,
|
|
||||||
pspReference,
|
|
||||||
handleChangePspReference,
|
|
||||||
} = useManualTransactionContext();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
|
|
|
@ -5,10 +5,9 @@ import React from "react";
|
||||||
|
|
||||||
import { useManualTransactionContext } from "../context";
|
import { useManualTransactionContext } from "../context";
|
||||||
|
|
||||||
export const SubmitButton: React.FC<Omit<
|
export const SubmitButton: React.FC<
|
||||||
ConfirmButtonProps,
|
Omit<ConfirmButtonProps, "type" | "transitionState">
|
||||||
"type" | "transitionState"
|
> = ({ disabled, ...props }) => {
|
||||||
>> = ({ disabled, ...props }) => {
|
|
||||||
const { submitState, amount } = useManualTransactionContext();
|
const { submitState, amount } = useManualTransactionContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -35,7 +35,9 @@ export const useManualRefund = ({
|
||||||
}
|
}
|
||||||
}, [submitState]);
|
}, [submitState]);
|
||||||
|
|
||||||
const handleChangeDescription: React.ChangeEventHandler<HTMLInputElement> = e => {
|
const handleChangeDescription: React.ChangeEventHandler<
|
||||||
|
HTMLInputElement
|
||||||
|
> = e => {
|
||||||
setDescription(e.target.value);
|
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);
|
setPspReference(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,8 @@ interface OrderManualTransactionCombinedComponent {
|
||||||
PspReferenceField: typeof PspReferenceField;
|
PspReferenceField: typeof PspReferenceField;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OrderManualTransactionForm = MainComponent as OrderManualTransactionCombinedComponent;
|
export const OrderManualTransactionForm =
|
||||||
|
MainComponent as OrderManualTransactionCombinedComponent;
|
||||||
OrderManualTransactionForm.DescriptionField = DescriptionField;
|
OrderManualTransactionForm.DescriptionField = DescriptionField;
|
||||||
OrderManualTransactionForm.ErrorText = ErrorText;
|
OrderManualTransactionForm.ErrorText = ErrorText;
|
||||||
OrderManualTransactionForm.Form = Form;
|
OrderManualTransactionForm.Form = Form;
|
||||||
|
|
|
@ -59,9 +59,9 @@ export const ItemsCard: React.FC<OrderReturnRefundLinesCardProps> = ({
|
||||||
const classes = useItemCardStyles({});
|
const classes = useItemCardStyles({});
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const handleChangeQuantity = (id: string) => (
|
const handleChangeQuantity =
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
(id: string) => (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
) => onChangeQuantity(id, parseInt(event.target.value, 10));
|
onChangeQuantity(id, parseInt(event.target.value, 10));
|
||||||
|
|
||||||
const fulfillment = order?.fulfillments.find(getById(fulfilmentId));
|
const fulfillment = order?.fulfillments.find(getById(fulfilmentId));
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,9 @@ import {
|
||||||
import { manualRefundMessages, refundPageMessages } from "../messages";
|
import { manualRefundMessages, refundPageMessages } from "../messages";
|
||||||
import { useManualRefundCardStyles } from "../styles";
|
import { useManualRefundCardStyles } from "../styles";
|
||||||
|
|
||||||
export const ManualRefundCard: React.FC<OrderManualTransactionFormProps> = props => {
|
export const ManualRefundCard: React.FC<
|
||||||
|
OrderManualTransactionFormProps
|
||||||
|
> = props => {
|
||||||
const classes = useManualRefundCardStyles();
|
const classes = useManualRefundCardStyles();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
|
|
@ -22,13 +22,9 @@ export interface OrderTransactionActionDialogProps extends DialogProps {
|
||||||
action: TransactionActionEnum;
|
action: TransactionActionEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OrderTransactionActionDialog: React.FC<OrderTransactionActionDialogProps> = ({
|
export const OrderTransactionActionDialog: React.FC<
|
||||||
confirmButtonState,
|
OrderTransactionActionDialogProps
|
||||||
open,
|
> = ({ confirmButtonState, open, onClose, onSubmit, action }) => {
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
action,
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const actionIntl = action
|
const actionIntl = action
|
||||||
|
|
|
@ -64,10 +64,8 @@ const OrderTransactionPayment: React.FC<OrderTransactionPaymentProps> = ({
|
||||||
__typename: "FakeTransaction",
|
__typename: "FakeTransaction",
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTransactionAction: OrderTransactionProps["onTransactionAction"] = (
|
const handleTransactionAction: OrderTransactionProps["onTransactionAction"] =
|
||||||
_,
|
(_, action) => {
|
||||||
action,
|
|
||||||
) => {
|
|
||||||
if (action === TransactionActionEnum.CHARGE) {
|
if (action === TransactionActionEnum.CHARGE) {
|
||||||
onCapture();
|
onCapture();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
import { defineMessages } from "react-intl";
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
published: {
|
emptyText: {
|
||||||
id: "Sd0Ppm",
|
id: "Q1Uzbb",
|
||||||
defaultMessage: "Published on {date}",
|
defaultMessage: "No products found",
|
||||||
description: "product publication date",
|
|
||||||
},
|
},
|
||||||
unpublished: {
|
addProduct: {
|
||||||
id: "qpQ0uB",
|
id: "tiY7bx",
|
||||||
defaultMessage: "Unpublished",
|
defaultMessage: "Add new product",
|
||||||
description: "product publication date",
|
|
||||||
},
|
},
|
||||||
willBePublished: {
|
editProduct: {
|
||||||
id: "r0hgpM",
|
defaultMessage: "Edit Product",
|
||||||
defaultMessage: "Becomes published on {date}",
|
id: "Q4m1CG",
|
||||||
description: "product publication date",
|
},
|
||||||
|
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,
|
...sortPageProps.sort,
|
||||||
sort: ProductListUrlSortField.name,
|
sort: ProductListUrlSortField.name,
|
||||||
},
|
},
|
||||||
|
channels: [],
|
||||||
|
columnQuery: "",
|
||||||
|
availableInGridAttributes: [],
|
||||||
|
onColumnQueryChange: () => undefined,
|
||||||
},
|
},
|
||||||
activeAttributeSortId: undefined,
|
activeAttributeSortId: undefined,
|
||||||
availableInGridAttributes: attributes,
|
|
||||||
columnQuery: "",
|
|
||||||
onColumnQueryChange: () => undefined,
|
|
||||||
currencySymbol: "USD",
|
currencySymbol: "USD",
|
||||||
defaultSettings: defaultListSettings[ListViews.PRODUCT_LIST],
|
defaultSettings: defaultListSettings[ListViews.PRODUCT_LIST],
|
||||||
filterOpts: productListFilterOpts,
|
filterOpts: productListFilterOpts,
|
||||||
|
|
|
@ -4,16 +4,13 @@ import {
|
||||||
mapToMenuItemsForProductOverviewActions,
|
mapToMenuItemsForProductOverviewActions,
|
||||||
useExtensions,
|
useExtensions,
|
||||||
} from "@dashboard/apps/hooks/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 { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
||||||
import { ButtonWithSelect } from "@dashboard/components/ButtonWithSelect";
|
import { ButtonWithDropdown } from "@dashboard/components/ButtonWithDropdown";
|
||||||
import CardMenu from "@dashboard/components/CardMenu";
|
|
||||||
import ColumnPicker from "@dashboard/components/ColumnPicker";
|
|
||||||
import { getByName } from "@dashboard/components/Filter/utils";
|
import { getByName } from "@dashboard/components/Filter/utils";
|
||||||
import FilterBar from "@dashboard/components/FilterBar";
|
|
||||||
import { ListPageLayout } from "@dashboard/components/Layouts";
|
import { ListPageLayout } from "@dashboard/components/Layouts";
|
||||||
import LimitReachedAlert from "@dashboard/components/LimitReachedAlert";
|
import LimitReachedAlert from "@dashboard/components/LimitReachedAlert";
|
||||||
import { MultiAutocompleteChoiceType } from "@dashboard/components/MultiAutocompleteSelectField";
|
import { TopNavMenu } from "@dashboard/components/TopNavMenu";
|
||||||
import { ProductListColumns } from "@dashboard/config";
|
import { ProductListColumns } from "@dashboard/config";
|
||||||
import {
|
import {
|
||||||
GridAttributesQuery,
|
GridAttributesQuery,
|
||||||
|
@ -21,6 +18,8 @@ import {
|
||||||
RefreshLimitsQuery,
|
RefreshLimitsQuery,
|
||||||
SearchAvailableInGridAttributesQuery,
|
SearchAvailableInGridAttributesQuery,
|
||||||
} from "@dashboard/graphql";
|
} from "@dashboard/graphql";
|
||||||
|
import useLocalStorage from "@dashboard/hooks/useLocalStorage";
|
||||||
|
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||||
import { sectionNames } from "@dashboard/intl";
|
import { sectionNames } from "@dashboard/intl";
|
||||||
import {
|
import {
|
||||||
ChannelProps,
|
ChannelProps,
|
||||||
|
@ -33,19 +32,19 @@ import {
|
||||||
} from "@dashboard/types";
|
} from "@dashboard/types";
|
||||||
import { hasLimits, isLimitReached } from "@dashboard/utils/limits";
|
import { hasLimits, isLimitReached } from "@dashboard/utils/limits";
|
||||||
import { Card } from "@material-ui/core";
|
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 React from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import { ProductListUrlSortField } from "../../urls";
|
import { ProductListUrlSortField, productUrl } from "../../urls";
|
||||||
import ProductList from "../ProductList";
|
import { ProductListDatagrid } from "../ProductListDatagrid";
|
||||||
import { columnsMessages } from "../ProductList/messages";
|
import { ProductListTiles } from "../ProductListTiles/ProductListTiles";
|
||||||
|
import { ProductListViewSwitch } from "../ProductListViewSwitch";
|
||||||
import {
|
import {
|
||||||
createFilterStructure,
|
createFilterStructure,
|
||||||
ProductFilterKeys,
|
ProductFilterKeys,
|
||||||
ProductListFilterOpts,
|
ProductListFilterOpts,
|
||||||
} from "./filters";
|
} from "./filters";
|
||||||
import { getAttributeColumnValue } from "./utils";
|
|
||||||
|
|
||||||
export interface ProductListPageProps
|
export interface ProductListPageProps
|
||||||
extends PageListProps<ProductListColumns>,
|
extends PageListProps<ProductListColumns>,
|
||||||
|
@ -69,30 +68,13 @@ export interface ProductListPageProps
|
||||||
onColumnQueryChange: (query: string) => void;
|
onColumnQueryChange: (query: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
export type ProductListViewType = "datagrid" | "tile";
|
||||||
theme => ({
|
const DEFAULT_PRODUCT_LIST_VIEW_TYPE: ProductListViewType = "datagrid";
|
||||||
columnPicker: {
|
|
||||||
marginRight: theme.spacing(3),
|
|
||||||
[theme.breakpoints.down("xs")]: {
|
|
||||||
"& > button": {
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
[theme.breakpoints.up("sm")]: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{ name: "ProductListPage" },
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
const {
|
const {
|
||||||
columnQuery,
|
columnQuery,
|
||||||
currencySymbol,
|
currencySymbol,
|
||||||
currentTab,
|
|
||||||
defaultSettings,
|
defaultSettings,
|
||||||
gridAttributes,
|
gridAttributes,
|
||||||
limits,
|
limits,
|
||||||
|
@ -102,75 +84,25 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
initialSearch,
|
initialSearch,
|
||||||
loading,
|
loading,
|
||||||
settings,
|
settings,
|
||||||
tabs,
|
|
||||||
onAdd,
|
onAdd,
|
||||||
onAll,
|
|
||||||
onColumnQueryChange,
|
onColumnQueryChange,
|
||||||
onExport,
|
onExport,
|
||||||
onFetchMore,
|
onFetchMore,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
onFilterAttributeFocus,
|
onFilterAttributeFocus,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onTabChange,
|
|
||||||
onTabDelete,
|
|
||||||
onTabSave,
|
|
||||||
onUpdateListSettings,
|
onUpdateListSettings,
|
||||||
selectedChannelId,
|
selectedChannelId,
|
||||||
selectedProductIds,
|
selectedProductIds,
|
||||||
|
activeAttributeSortId,
|
||||||
...listProps
|
...listProps
|
||||||
} = props;
|
} = props;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const classes = useStyles(props);
|
const navigate = useNavigator();
|
||||||
|
|
||||||
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 filterStructure = createFilterStructure(intl, filterOpts);
|
const filterStructure = createFilterStructure(intl, filterOpts);
|
||||||
|
|
||||||
const filterDependency = filterStructure.find(getByName("channel"));
|
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 limitReached = isLimitReached(limits, "productVariants");
|
||||||
const { PRODUCT_OVERVIEW_CREATE, PRODUCT_OVERVIEW_MORE_ACTIONS } =
|
const { PRODUCT_OVERVIEW_CREATE, PRODUCT_OVERVIEW_MORE_ACTIONS } =
|
||||||
useExtensions(extensionMountPoints.PRODUCT_LIST);
|
useExtensions(extensionMountPoints.PRODUCT_LIST);
|
||||||
|
@ -181,12 +113,35 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
);
|
);
|
||||||
const extensionCreateButtonItems = mapToMenuItems(PRODUCT_OVERVIEW_CREATE);
|
const extensionCreateButtonItems = mapToMenuItems(PRODUCT_OVERVIEW_CREATE);
|
||||||
|
|
||||||
|
const [storedProductListViewType, setProductListViewType] =
|
||||||
|
useLocalStorage<ProductListViewType>(
|
||||||
|
"productListViewType",
|
||||||
|
DEFAULT_PRODUCT_LIST_VIEW_TYPE,
|
||||||
|
);
|
||||||
|
const isDatagridView = storedProductListViewType === "datagrid";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListPageLayout>
|
<ListPageLayout>
|
||||||
<TopNav title={intl.formatMessage(sectionNames.products)}>
|
<TopNav withoutBorder title={intl.formatMessage(sectionNames.products)}>
|
||||||
<CardMenu
|
<Box display="flex" alignItems="center" gap={5}>
|
||||||
className={classes.settings}
|
{hasLimits(limits, "productVariants") && (
|
||||||
menuItems={[
|
<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={[
|
||||||
{
|
{
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
id: "7FL+WZ",
|
id: "7FL+WZ",
|
||||||
|
@ -198,48 +153,29 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
},
|
},
|
||||||
...extensionMenuItems,
|
...extensionMenuItems,
|
||||||
]}
|
]}
|
||||||
data-test-id="menu"
|
|
||||||
/>
|
/>
|
||||||
<ColumnPicker
|
{extensionCreateButtonItems.length > 0 ? (
|
||||||
className={classes.columnPicker}
|
<ButtonWithDropdown
|
||||||
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}
|
onClick={onAdd}
|
||||||
|
testId={"add-product"}
|
||||||
|
options={extensionCreateButtonItems}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="JFmOfi"
|
id="JFmOfi"
|
||||||
defaultMessage="Create Product"
|
defaultMessage="Create Product"
|
||||||
description="button"
|
description="button"
|
||||||
/>
|
/>
|
||||||
</ButtonWithSelect>
|
</ButtonWithDropdown>
|
||||||
{hasLimits(limits, "productVariants") && (
|
) : (
|
||||||
<LimitsInfo
|
<Button data-test-id="add-product" onClick={onAdd}>
|
||||||
text={intl.formatMessage(
|
<FormattedMessage
|
||||||
{
|
id="JFmOfi"
|
||||||
id: "Kw0jHS",
|
defaultMessage="Create Product"
|
||||||
defaultMessage: "{count}/{max} SKUs used",
|
description="button"
|
||||||
description: "created products counter",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
count: limits.currentUsage.productVariants,
|
|
||||||
max: limits.allowedUsage.productVariants,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
</TopNav>
|
</TopNav>
|
||||||
{limitReached && (
|
{limitReached && (
|
||||||
<LimitReachedAlert
|
<LimitReachedAlert
|
||||||
|
@ -256,37 +192,66 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
</LimitReachedAlert>
|
</LimitReachedAlert>
|
||||||
)}
|
)}
|
||||||
<Card>
|
<Card>
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
width="100%"
|
||||||
|
alignItems="stretch"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
<FilterBar
|
<FilterBar
|
||||||
currencySymbol={currencySymbol}
|
currencySymbol={currencySymbol}
|
||||||
currentTab={currentTab}
|
|
||||||
initialSearch={initialSearch}
|
initialSearch={initialSearch}
|
||||||
onAll={onAll}
|
|
||||||
onFilterChange={onFilterChange}
|
onFilterChange={onFilterChange}
|
||||||
onFilterAttributeFocus={onFilterAttributeFocus}
|
onFilterAttributeFocus={onFilterAttributeFocus}
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={onSearchChange}
|
||||||
onTabChange={onTabChange}
|
|
||||||
onTabDelete={onTabDelete}
|
|
||||||
onTabSave={onTabSave}
|
|
||||||
tabs={tabs}
|
|
||||||
allTabLabel={intl.formatMessage({
|
|
||||||
id: "aFLtLk",
|
|
||||||
defaultMessage: "All Products",
|
|
||||||
description: "tab name",
|
|
||||||
})}
|
|
||||||
filterStructure={filterStructure}
|
filterStructure={filterStructure}
|
||||||
searchPlaceholder={intl.formatMessage({
|
searchPlaceholder={intl.formatMessage({
|
||||||
id: "kIvvax",
|
id: "kIvvax",
|
||||||
defaultMessage: "Search Products...",
|
defaultMessage: "Search Products...",
|
||||||
})}
|
})}
|
||||||
|
actions={
|
||||||
|
<ProductListViewSwitch
|
||||||
|
defaultValue={storedProductListViewType}
|
||||||
|
setProductListViewType={setProductListViewType}
|
||||||
/>
|
/>
|
||||||
<ProductList
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{isDatagridView ? (
|
||||||
|
<ProductListDatagrid
|
||||||
{...listProps}
|
{...listProps}
|
||||||
|
filterDependency={filterDependency}
|
||||||
|
activeAttributeSortId={activeAttributeSortId}
|
||||||
|
columnQuery={columnQuery}
|
||||||
|
defaultSettings={defaultSettings}
|
||||||
|
availableInGridAttributes={availableInGridAttributes}
|
||||||
|
isAttributeLoading={loading}
|
||||||
|
loading={listProps.disabled}
|
||||||
|
hasMore={hasMore}
|
||||||
gridAttributes={gridAttributes}
|
gridAttributes={gridAttributes}
|
||||||
|
onColumnQueryChange={onColumnQueryChange}
|
||||||
|
onFetchMore={onFetchMore}
|
||||||
|
products={listProps.products}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
selectedChannelId={selectedChannelId}
|
selectedChannelId={selectedChannelId}
|
||||||
onUpdateListSettings={onUpdateListSettings}
|
onUpdateListSettings={onUpdateListSettings}
|
||||||
filterDependency={filterDependency}
|
onRowClick={id => {
|
||||||
|
navigate(productUrl(id));
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ProductListTiles
|
||||||
|
{...listProps}
|
||||||
|
settings={settings}
|
||||||
|
loading={listProps.disabled}
|
||||||
|
onUpdateListSettings={onUpdateListSettings}
|
||||||
|
products={listProps.products}
|
||||||
|
onTileClick={id => {
|
||||||
|
navigate(productUrl(id));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</ListPageLayout>
|
</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