Category listing datagrid (#3760)
Co-authored-by: wojteknowacki <wojciech.nowacki@saleor.io>
This commit is contained in:
parent
1cb6e8b5fc
commit
b4f11eff66
37 changed files with 997 additions and 812 deletions
5
.changeset/strange-carrots-juggle.md
Normal file
5
.changeset/strange-carrots-juggle.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"saleor-dashboard": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Introduce datagrid on category listing page
|
|
@ -3,10 +3,7 @@
|
||||||
|
|
||||||
import faker from "faker";
|
import faker from "faker";
|
||||||
|
|
||||||
import {
|
import { CATEGORIES_LIST_SELECTORS } from "../../elements/catalog/categories/categories-list";
|
||||||
CATEGORIES_LIST_SELECTORS,
|
|
||||||
categoryRow,
|
|
||||||
} from "../../elements/catalog/categories/categories-list";
|
|
||||||
import { CATEGORY_DETAILS_SELECTORS } from "../../elements/catalog/categories/category-details";
|
import { CATEGORY_DETAILS_SELECTORS } from "../../elements/catalog/categories/category-details";
|
||||||
import { BUTTON_SELECTORS } from "../../elements/shared/button-selectors";
|
import { BUTTON_SELECTORS } from "../../elements/shared/button-selectors";
|
||||||
import { SHARED_ELEMENTS } from "../../elements/shared/sharedElements";
|
import { SHARED_ELEMENTS } from "../../elements/shared/sharedElements";
|
||||||
|
@ -17,6 +14,7 @@ import {
|
||||||
} from "../../support/api/requests/Category";
|
} from "../../support/api/requests/Category";
|
||||||
import * as channelsUtils from "../../support/api/utils/channelsUtils";
|
import * as channelsUtils from "../../support/api/utils/channelsUtils";
|
||||||
import * as productsUtils from "../../support/api/utils/products/productsUtils";
|
import * as productsUtils from "../../support/api/utils/products/productsUtils";
|
||||||
|
import { ensureCanvasStatic } from "../../support/customCommands/sharedElementsOperations/canvas";
|
||||||
import {
|
import {
|
||||||
createCategory,
|
createCategory,
|
||||||
updateCategory,
|
updateCategory,
|
||||||
|
@ -109,9 +107,13 @@ describe("As an admin I want to manage categories", () => {
|
||||||
.click();
|
.click();
|
||||||
createCategory({ name: categoryName, description: categoryName })
|
createCategory({ name: categoryName, description: categoryName })
|
||||||
.visit(categoryDetailsUrl(category.id))
|
.visit(categoryDetailsUrl(category.id))
|
||||||
.contains(CATEGORY_DETAILS_SELECTORS.categoryChildrenRow, categoryName)
|
.get(SHARED_ELEMENTS.dataGridTable)
|
||||||
.scrollIntoView()
|
.scrollIntoView();
|
||||||
.should("be.visible");
|
ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable);
|
||||||
|
|
||||||
|
cy.contains(SHARED_ELEMENTS.dataGridTable, categoryName).should(
|
||||||
|
"be.visible",
|
||||||
|
);
|
||||||
getCategory(category.id).then(categoryResp => {
|
getCategory(category.id).then(categoryResp => {
|
||||||
expect(categoryResp.children.edges[0].node.name).to.eq(categoryName);
|
expect(categoryResp.children.edges[0].node.name).to.eq(categoryName);
|
||||||
});
|
});
|
||||||
|
@ -136,19 +138,22 @@ describe("As an admin I want to manage categories", () => {
|
||||||
"should be able to remove product from category. TC: SALEOR_0204",
|
"should be able to remove product from category. TC: SALEOR_0204",
|
||||||
{ tags: ["@category", "@allEnv", "@stable"] },
|
{ tags: ["@category", "@allEnv", "@stable"] },
|
||||||
() => {
|
() => {
|
||||||
|
cy.addAliasToGraphRequest("productBulkDelete");
|
||||||
cy.visit(categoryDetailsUrl(category.id))
|
cy.visit(categoryDetailsUrl(category.id))
|
||||||
.get(CATEGORY_DETAILS_SELECTORS.productsTab)
|
.get(CATEGORY_DETAILS_SELECTORS.productsTab)
|
||||||
.click();
|
.click();
|
||||||
cy.contains(CATEGORY_DETAILS_SELECTORS.productRow, product.name)
|
ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable);
|
||||||
.find(BUTTON_SELECTORS.checkbox)
|
cy.contains(SHARED_ELEMENTS.dataGridTable, product.name).should(
|
||||||
|
"be.visible",
|
||||||
|
);
|
||||||
|
// selects first row
|
||||||
|
cy.clickGridCell(0, 0);
|
||||||
|
cy.get(CATEGORY_DETAILS_SELECTORS.deleteCategoriesButton)
|
||||||
.click()
|
.click()
|
||||||
.get(BUTTON_SELECTORS.deleteIcon)
|
|
||||||
.click()
|
|
||||||
.addAliasToGraphRequest("productBulkDelete")
|
|
||||||
.get(BUTTON_SELECTORS.submit)
|
.get(BUTTON_SELECTORS.submit)
|
||||||
.click()
|
.click()
|
||||||
.confirmationMessageShouldDisappear();
|
.confirmationMessageShouldDisappear();
|
||||||
cy.contains(CATEGORY_DETAILS_SELECTORS.productRow, product.name)
|
cy.contains(SHARED_ELEMENTS.dataGridTable, product.name)
|
||||||
.should("not.exist")
|
.should("not.exist")
|
||||||
.waitForRequestAndCheckIfNoErrors("@productBulkDelete");
|
.waitForRequestAndCheckIfNoErrors("@productBulkDelete");
|
||||||
getCategory(category.id).then(categoryResp => {
|
getCategory(category.id).then(categoryResp => {
|
||||||
|
@ -164,7 +169,12 @@ describe("As an admin I want to manage categories", () => {
|
||||||
cy.visit(urlList.categories)
|
cy.visit(urlList.categories)
|
||||||
.get(SHARED_ELEMENTS.searchInput)
|
.get(SHARED_ELEMENTS.searchInput)
|
||||||
.type(category.name);
|
.type(category.name);
|
||||||
cy.contains(SHARED_ELEMENTS.tableRow, category.name).click();
|
ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable);
|
||||||
|
cy.contains(SHARED_ELEMENTS.dataGridTable, category.name).should(
|
||||||
|
"be.visible",
|
||||||
|
);
|
||||||
|
// opens first row details
|
||||||
|
cy.clickGridCell(1, 0);
|
||||||
cy.contains(SHARED_ELEMENTS.header, category.name).should("be.visible");
|
cy.contains(SHARED_ELEMENTS.header, category.name).should("be.visible");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -174,6 +184,7 @@ describe("As an admin I want to manage categories", () => {
|
||||||
{ tags: ["@category", "@allEnv", "@stable"] },
|
{ tags: ["@category", "@allEnv", "@stable"] },
|
||||||
() => {
|
() => {
|
||||||
const categoryName = `${startsWith}${faker.datatype.number()}`;
|
const categoryName = `${startsWith}${faker.datatype.number()}`;
|
||||||
|
cy.addAliasToGraphRequest("CategoryDelete");
|
||||||
|
|
||||||
createCategoryRequest({
|
createCategoryRequest({
|
||||||
name: categoryName,
|
name: categoryName,
|
||||||
|
@ -181,7 +192,6 @@ describe("As an admin I want to manage categories", () => {
|
||||||
cy.visit(categoryDetailsUrl(categoryResp.id))
|
cy.visit(categoryDetailsUrl(categoryResp.id))
|
||||||
.get(BUTTON_SELECTORS.deleteButton)
|
.get(BUTTON_SELECTORS.deleteButton)
|
||||||
.click()
|
.click()
|
||||||
.addAliasToGraphRequest("CategoryDelete")
|
|
||||||
.get(BUTTON_SELECTORS.submit)
|
.get(BUTTON_SELECTORS.submit)
|
||||||
.click()
|
.click()
|
||||||
.waitForRequestAndCheckIfNoErrors("@CategoryDelete");
|
.waitForRequestAndCheckIfNoErrors("@CategoryDelete");
|
||||||
|
@ -222,35 +232,43 @@ describe("As an admin I want to manage categories", () => {
|
||||||
() => {
|
() => {
|
||||||
const firstCategoryName = `${startsWith}${faker.datatype.number()}`;
|
const firstCategoryName = `${startsWith}${faker.datatype.number()}`;
|
||||||
const secondCategoryName = `${startsWith}${faker.datatype.number()}`;
|
const secondCategoryName = `${startsWith}${faker.datatype.number()}`;
|
||||||
let firstCategory;
|
cy.addAliasToGraphRequest("CategoryBulkDelete");
|
||||||
let secondCategory;
|
|
||||||
|
|
||||||
createCategoryRequest({
|
createCategoryRequest({
|
||||||
name: firstCategoryName,
|
name: firstCategoryName,
|
||||||
}).then(categoryResp => {
|
|
||||||
firstCategory = categoryResp;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
createCategoryRequest({
|
createCategoryRequest({
|
||||||
name: secondCategoryName,
|
name: secondCategoryName,
|
||||||
}).then(categoryResp => {
|
}).then(() => {
|
||||||
secondCategory = categoryResp;
|
cy.visit(urlList.categories).searchInTable(startsWith);
|
||||||
cy.visit(urlList.categories)
|
ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable);
|
||||||
.searchInTable(startsWith)
|
cy.get(SHARED_ELEMENTS.firstRowDataGrid)
|
||||||
.get(categoryRow(firstCategory.id))
|
.invoke("text")
|
||||||
.find(BUTTON_SELECTORS.checkbox)
|
.then(firstOnListCategoryName => {
|
||||||
|
cy.get(SHARED_ELEMENTS.secondRowDataGrid)
|
||||||
|
.invoke("text")
|
||||||
|
.then(secondOnListCategoryName => {
|
||||||
|
// deletes two first rows from categories list view
|
||||||
|
cy.clickGridCell(0, 0);
|
||||||
|
cy.clickGridCell(0, 1);
|
||||||
|
|
||||||
|
cy.get(CATEGORY_DETAILS_SELECTORS.deleteCategoriesButton)
|
||||||
.click()
|
.click()
|
||||||
.get(categoryRow(secondCategory.id))
|
|
||||||
.find(BUTTON_SELECTORS.checkbox)
|
|
||||||
.click()
|
|
||||||
.get(BUTTON_SELECTORS.deleteIcon)
|
|
||||||
.click()
|
|
||||||
.addAliasToGraphRequest("CategoryBulkDelete")
|
|
||||||
.get(BUTTON_SELECTORS.submit)
|
.get(BUTTON_SELECTORS.submit)
|
||||||
.click()
|
.click()
|
||||||
.waitForRequestAndCheckIfNoErrors("@CategoryBulkDelete");
|
.waitForRequestAndCheckIfNoErrors("@CategoryBulkDelete");
|
||||||
cy.get(categoryRow(firstCategory.id)).should("not.exist");
|
ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable);
|
||||||
cy.get(categoryRow(secondCategory.id)).should("not.exist");
|
|
||||||
|
cy.contains(
|
||||||
|
SHARED_ELEMENTS.dataGridTable,
|
||||||
|
firstOnListCategoryName,
|
||||||
|
).should("not.exist");
|
||||||
|
cy.contains(
|
||||||
|
SHARED_ELEMENTS.dataGridTable,
|
||||||
|
secondOnListCategoryName,
|
||||||
|
).should("not.exist");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -263,6 +281,7 @@ describe("As an admin I want to manage categories", () => {
|
||||||
const mainCategoryName = `${startsWith}${faker.datatype.number()}`;
|
const mainCategoryName = `${startsWith}${faker.datatype.number()}`;
|
||||||
let subCategory;
|
let subCategory;
|
||||||
let mainCategory;
|
let mainCategory;
|
||||||
|
cy.addAliasToGraphRequest("CategoryBulkDelete");
|
||||||
|
|
||||||
createCategoryRequest({
|
createCategoryRequest({
|
||||||
name: mainCategoryName,
|
name: mainCategoryName,
|
||||||
|
@ -277,14 +296,16 @@ describe("As an admin I want to manage categories", () => {
|
||||||
.then(categoryResp => {
|
.then(categoryResp => {
|
||||||
subCategory = categoryResp;
|
subCategory = categoryResp;
|
||||||
cy.visit(categoryDetailsUrl(mainCategory.id))
|
cy.visit(categoryDetailsUrl(mainCategory.id))
|
||||||
.get(categoryRow(subCategory.id))
|
.get(SHARED_ELEMENTS.dataGridTable)
|
||||||
.find(BUTTON_SELECTORS.checkbox)
|
.scrollIntoView();
|
||||||
|
ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable);
|
||||||
|
// selects first row of subcategories
|
||||||
|
cy.clickGridCell(0, 0);
|
||||||
|
cy.get(CATEGORY_DETAILS_SELECTORS.deleteCategoriesButton)
|
||||||
.click()
|
.click()
|
||||||
.get(BUTTON_SELECTORS.deleteIcon)
|
|
||||||
.click()
|
|
||||||
.addAliasToGraphRequest("CategoryBulkDelete")
|
|
||||||
.get(BUTTON_SELECTORS.submit)
|
.get(BUTTON_SELECTORS.submit)
|
||||||
.click()
|
.click()
|
||||||
|
.confirmationMessageShouldDisappear()
|
||||||
.waitForRequestAndCheckIfNoErrors("@CategoryBulkDelete");
|
.waitForRequestAndCheckIfNoErrors("@CategoryBulkDelete");
|
||||||
getCategory(subCategory.id).should("be.null");
|
getCategory(subCategory.id).should("be.null");
|
||||||
getCategory(mainCategory.id);
|
getCategory(mainCategory.id);
|
||||||
|
|
|
@ -6,4 +6,5 @@ export const CATEGORY_DETAILS_SELECTORS = {
|
||||||
productsTab: '[data-test-id="products-tab"]',
|
productsTab: '[data-test-id="products-tab"]',
|
||||||
addProducts: '[data-test-id="add-products"]',
|
addProducts: '[data-test-id="add-products"]',
|
||||||
productRow: '[data-test-id="product-row"]',
|
productRow: '[data-test-id="product-row"]',
|
||||||
|
deleteCategoriesButton: '[data-test-id="delete-categories-button"]',
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,6 +7,8 @@ export const SHARED_ELEMENTS = {
|
||||||
dataGridTable: "[data-testid='data-grid-canvas']",
|
dataGridTable: "[data-testid='data-grid-canvas']",
|
||||||
skeleton: '[data-test-id="skeleton"]',
|
skeleton: '[data-test-id="skeleton"]',
|
||||||
table: 'table[class*="Table"]',
|
table: 'table[class*="Table"]',
|
||||||
|
firstRowDataGrid: "[data-testid='glide-cell-1-0']",
|
||||||
|
secondRowDataGrid: "[id='glide-cell-1-1']",
|
||||||
tableRow: '[data-test-id*="id"], [class*="MuiTableRow"]',
|
tableRow: '[data-test-id*="id"], [class*="MuiTableRow"]',
|
||||||
notificationSuccess:
|
notificationSuccess:
|
||||||
'[data-test-id="notification"][data-test-type="success"]',
|
'[data-test-id="notification"][data-test-type="success"]',
|
||||||
|
|
|
@ -14,17 +14,16 @@ export function updateCategory({ name, description }) {
|
||||||
|
|
||||||
export function fillUpCategoryGeneralInfo({ name, description }) {
|
export function fillUpCategoryGeneralInfo({ name, description }) {
|
||||||
return cy
|
return cy
|
||||||
|
.get(CATEGORY_DETAILS_SELECTORS.nameInput)
|
||||||
|
.clearAndType(name)
|
||||||
.get(CATEGORY_DETAILS_SELECTORS.descriptionInput)
|
.get(CATEGORY_DETAILS_SELECTORS.descriptionInput)
|
||||||
.find(SHARED_ELEMENTS.contentEditable)
|
.find(SHARED_ELEMENTS.contentEditable)
|
||||||
.should("be.visible")
|
.should("be.visible")
|
||||||
|
|
||||||
.get(CATEGORY_DETAILS_SELECTORS.descriptionInput)
|
.get(CATEGORY_DETAILS_SELECTORS.descriptionInput)
|
||||||
.click()
|
.click()
|
||||||
.get(CATEGORY_DETAILS_SELECTORS.descriptionInput)
|
.get(CATEGORY_DETAILS_SELECTORS.descriptionInput)
|
||||||
.find(SHARED_ELEMENTS.contentEditable)
|
.clearAndType(description);
|
||||||
.get(CATEGORY_DETAILS_SELECTORS.descriptionInput)
|
|
||||||
.clearAndType(description)
|
|
||||||
.get(CATEGORY_DETAILS_SELECTORS.nameInput)
|
|
||||||
.clearAndType(name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveCategory(alias = "CategoryCreate") {
|
export function saveCategory(alias = "CategoryCreate") {
|
||||||
|
|
|
@ -545,6 +545,9 @@
|
||||||
"context": "product variants, title",
|
"context": "product variants, title",
|
||||||
"string": "Variants"
|
"string": "Variants"
|
||||||
},
|
},
|
||||||
|
"1X6HtI": {
|
||||||
|
"string": "All Categories"
|
||||||
|
},
|
||||||
"1div9r": {
|
"1div9r": {
|
||||||
"string": "Search Attribute"
|
"string": "Search Attribute"
|
||||||
},
|
},
|
||||||
|
@ -1784,10 +1787,6 @@
|
||||||
"context": "webhooks and events section name",
|
"context": "webhooks and events section name",
|
||||||
"string": "Webhooks & Events"
|
"string": "Webhooks & Events"
|
||||||
},
|
},
|
||||||
"BHQrgz": {
|
|
||||||
"context": "number of subcategories",
|
|
||||||
"string": "Subcategories"
|
|
||||||
},
|
|
||||||
"BJtUQI": {
|
"BJtUQI": {
|
||||||
"context": "button",
|
"context": "button",
|
||||||
"string": "Add"
|
"string": "Add"
|
||||||
|
@ -2329,6 +2328,9 @@
|
||||||
"context": "Webhook details objects",
|
"context": "Webhook details objects",
|
||||||
"string": "Objects"
|
"string": "Objects"
|
||||||
},
|
},
|
||||||
|
"F7DxHw": {
|
||||||
|
"string": "Subcategories"
|
||||||
|
},
|
||||||
"F8gsds": {
|
"F8gsds": {
|
||||||
"context": "unpublish page, button",
|
"context": "unpublish page, button",
|
||||||
"string": "Unpublish"
|
"string": "Unpublish"
|
||||||
|
@ -3057,9 +3059,6 @@
|
||||||
"context": "dialog search placeholder",
|
"context": "dialog search placeholder",
|
||||||
"string": "Search by collection name, etc..."
|
"string": "Search by collection name, etc..."
|
||||||
},
|
},
|
||||||
"JiXNEV": {
|
|
||||||
"string": "Search Category"
|
|
||||||
},
|
|
||||||
"Jj0de8": {
|
"Jj0de8": {
|
||||||
"context": "voucher status",
|
"context": "voucher status",
|
||||||
"string": "Scheduled"
|
"string": "Scheduled"
|
||||||
|
@ -4306,6 +4305,9 @@
|
||||||
"context": "header",
|
"context": "header",
|
||||||
"string": "Create Variant"
|
"string": "Create Variant"
|
||||||
},
|
},
|
||||||
|
"T83iU7": {
|
||||||
|
"string": "Search categories..."
|
||||||
|
},
|
||||||
"T8rvXs": {
|
"T8rvXs": {
|
||||||
"context": "order subtotal price",
|
"context": "order subtotal price",
|
||||||
"string": "Subtotal"
|
"string": "Subtotal"
|
||||||
|
@ -4659,10 +4661,6 @@
|
||||||
"VOiUXQ": {
|
"VOiUXQ": {
|
||||||
"string": "Used to calculate rates for shipping for products of this product type, when specific weight is not given"
|
"string": "Used to calculate rates for shipping for products of this product type, when specific weight is not given"
|
||||||
},
|
},
|
||||||
"VQLIXd": {
|
|
||||||
"context": "product",
|
|
||||||
"string": "Name"
|
|
||||||
},
|
|
||||||
"VSj89H": {
|
"VSj89H": {
|
||||||
"context": "fulfill button label",
|
"context": "fulfill button label",
|
||||||
"string": "Fulfill anyway"
|
"string": "Fulfill anyway"
|
||||||
|
@ -5247,6 +5245,9 @@
|
||||||
"ZMy18J": {
|
"ZMy18J": {
|
||||||
"string": "You have reached your channel limit, you will be no longer able to add channels to your store. If you would like to up your limit, contact your administration staff about raising your limits."
|
"string": "You have reached your channel limit, you will be no longer able to add channels to your store. If you would like to up your limit, contact your administration staff about raising your limits."
|
||||||
},
|
},
|
||||||
|
"ZN5IZl": {
|
||||||
|
"string": "Bulk categories delete"
|
||||||
|
},
|
||||||
"ZPOyI1": {
|
"ZPOyI1": {
|
||||||
"context": "fulfilled fulfillment, section header",
|
"context": "fulfilled fulfillment, section header",
|
||||||
"string": "Fulfilled from {warehouseName}"
|
"string": "Fulfilled from {warehouseName}"
|
||||||
|
@ -5671,6 +5672,9 @@
|
||||||
"context": "product attribute type",
|
"context": "product attribute type",
|
||||||
"string": "Multiple Select"
|
"string": "Multiple Select"
|
||||||
},
|
},
|
||||||
|
"cLcy6F": {
|
||||||
|
"string": "Number of products"
|
||||||
|
},
|
||||||
"cMFlOp": {
|
"cMFlOp": {
|
||||||
"context": "input label",
|
"context": "input label",
|
||||||
"string": "New Password"
|
"string": "New Password"
|
||||||
|
@ -5774,6 +5778,9 @@
|
||||||
"context": "config type section title",
|
"context": "config type section title",
|
||||||
"string": "Configuration Type"
|
"string": "Configuration Type"
|
||||||
},
|
},
|
||||||
|
"cxOmce": {
|
||||||
|
"string": "Bulk products delete"
|
||||||
|
},
|
||||||
"cy8sV7": {
|
"cy8sV7": {
|
||||||
"context": "volume units types",
|
"context": "volume units types",
|
||||||
"string": "Volume"
|
"string": "Volume"
|
||||||
|
@ -6607,10 +6614,6 @@
|
||||||
"context": "tooltip content when product is in preorder",
|
"context": "tooltip content when product is in preorder",
|
||||||
"string": "This product is still in preorder. You will be able to fulfill it after it reaches it’s release date"
|
"string": "This product is still in preorder. You will be able to fulfill it after it reaches it’s release date"
|
||||||
},
|
},
|
||||||
"k8ZJ5L": {
|
|
||||||
"context": "number of products",
|
|
||||||
"string": "No. of Products"
|
|
||||||
},
|
|
||||||
"k8bltk": {
|
"k8bltk": {
|
||||||
"string": "No Results"
|
"string": "No Results"
|
||||||
},
|
},
|
||||||
|
@ -6712,6 +6715,9 @@
|
||||||
"context": "balance amound missing error message",
|
"context": "balance amound missing error message",
|
||||||
"string": "Balance amount is missing"
|
"string": "Balance amount is missing"
|
||||||
},
|
},
|
||||||
|
"kgVqk1": {
|
||||||
|
"string": "Category name"
|
||||||
|
},
|
||||||
"ki7Mr8": {
|
"ki7Mr8": {
|
||||||
"context": "product export to csv file, header",
|
"context": "product export to csv file, header",
|
||||||
"string": "Export Settings"
|
"string": "Export Settings"
|
||||||
|
@ -7454,6 +7460,9 @@
|
||||||
"context": "order line total price",
|
"context": "order line total price",
|
||||||
"string": "Total"
|
"string": "Total"
|
||||||
},
|
},
|
||||||
|
"qU/z0Q": {
|
||||||
|
"string": "Bulk category delete"
|
||||||
|
},
|
||||||
"qZHHed": {
|
"qZHHed": {
|
||||||
"context": "stock exceeded dialog title",
|
"context": "stock exceeded dialog title",
|
||||||
"string": "Not enough stock"
|
"string": "Not enough stock"
|
||||||
|
@ -7626,9 +7635,6 @@
|
||||||
"context": "header",
|
"context": "header",
|
||||||
"string": "Top Products"
|
"string": "Top Products"
|
||||||
},
|
},
|
||||||
"rrbzZt": {
|
|
||||||
"string": "No subcategories found"
|
|
||||||
},
|
|
||||||
"rs815i": {
|
"rs815i": {
|
||||||
"context": "text field label",
|
"context": "text field label",
|
||||||
"string": "Group name"
|
"string": "Group name"
|
||||||
|
@ -8234,10 +8240,6 @@
|
||||||
"context": "draft order",
|
"context": "draft order",
|
||||||
"string": "Created"
|
"string": "Created"
|
||||||
},
|
},
|
||||||
"vy7fjd": {
|
|
||||||
"context": "tab name",
|
|
||||||
"string": "All Categories"
|
|
||||||
},
|
|
||||||
"vzce9B": {
|
"vzce9B": {
|
||||||
"context": "customer gift cards card subtitle",
|
"context": "customer gift cards card subtitle",
|
||||||
"string": "Only five newest gift cards are shown here"
|
"string": "Only five newest gift cards are shown here"
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { Button, Tooltip, TrashBinIcon } from "@saleor/macaw-ui/next";
|
||||||
|
import React, { forwardRef, ReactNode, useState } from "react";
|
||||||
|
|
||||||
|
interface CategoryDeleteButtonProps {
|
||||||
|
onClick: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CategoryDeleteButton = forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
CategoryDeleteButtonProps
|
||||||
|
>(({ onClick, children }, ref) => {
|
||||||
|
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip open={isTooltipOpen}>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
onMouseOver={() => {
|
||||||
|
setIsTooltipOpen(true);
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setIsTooltipOpen(false);
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
icon={<TrashBinIcon />}
|
||||||
|
variant="secondary"
|
||||||
|
data-test-id="delete-categories-button"
|
||||||
|
/>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content side="bottom">
|
||||||
|
<Tooltip.Arrow />
|
||||||
|
{children}
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
1
src/categories/components/CategoryDeleteButton/index.ts
Normal file
1
src/categories/components/CategoryDeleteButton/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./CategoryDeleteButton";
|
|
@ -1,218 +0,0 @@
|
||||||
// @ts-strict-ignore
|
|
||||||
import {
|
|
||||||
CategoryListUrlSortField,
|
|
||||||
categoryUrl,
|
|
||||||
} from "@dashboard/categories/urls";
|
|
||||||
import Checkbox from "@dashboard/components/Checkbox";
|
|
||||||
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
|
|
||||||
import Skeleton from "@dashboard/components/Skeleton";
|
|
||||||
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 { CategoryFragment } from "@dashboard/graphql";
|
|
||||||
import { maybe, renderCollection } from "@dashboard/misc";
|
|
||||||
import { ListActions, ListProps, SortPage } from "@dashboard/types";
|
|
||||||
import { getArrowDirection } from "@dashboard/utils/sort";
|
|
||||||
import { TableBody, TableCell, TableFooter } from "@material-ui/core";
|
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import React from "react";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
|
||||||
theme => ({
|
|
||||||
[theme.breakpoints.up("lg")]: {
|
|
||||||
colName: {
|
|
||||||
width: "auto",
|
|
||||||
},
|
|
||||||
colProducts: {
|
|
||||||
width: 160,
|
|
||||||
},
|
|
||||||
colSubcategories: {
|
|
||||||
width: 160,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
colName: {
|
|
||||||
paddingLeft: 0,
|
|
||||||
},
|
|
||||||
colProducts: {
|
|
||||||
textAlign: "center",
|
|
||||||
},
|
|
||||||
colSubcategories: {
|
|
||||||
textAlign: "center",
|
|
||||||
},
|
|
||||||
tableRow: {
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{ name: "CategoryList" },
|
|
||||||
);
|
|
||||||
|
|
||||||
interface CategoryListProps
|
|
||||||
extends ListProps,
|
|
||||||
ListActions,
|
|
||||||
SortPage<CategoryListUrlSortField> {
|
|
||||||
categories?: CategoryFragment[];
|
|
||||||
isRoot: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CategoryList: React.FC<CategoryListProps> = props => {
|
|
||||||
const {
|
|
||||||
categories,
|
|
||||||
disabled,
|
|
||||||
settings,
|
|
||||||
sort,
|
|
||||||
isChecked,
|
|
||||||
isRoot,
|
|
||||||
selected,
|
|
||||||
toggle,
|
|
||||||
toggleAll,
|
|
||||||
toolbar,
|
|
||||||
onUpdateListSettings,
|
|
||||||
onSort,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const classes = useStyles(props);
|
|
||||||
const numberOfColumns = categories?.length === 0 ? 3 : 4;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResponsiveTable>
|
|
||||||
<TableHead
|
|
||||||
colSpan={numberOfColumns}
|
|
||||||
selected={selected}
|
|
||||||
disabled={disabled}
|
|
||||||
items={categories}
|
|
||||||
toggleAll={toggleAll}
|
|
||||||
toolbar={toolbar}
|
|
||||||
>
|
|
||||||
<TableCellHeader
|
|
||||||
direction={
|
|
||||||
isRoot && sort.sort === CategoryListUrlSortField.name
|
|
||||||
? getArrowDirection(sort.asc)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
arrowPosition="right"
|
|
||||||
className={classes.colName}
|
|
||||||
disabled={!isRoot}
|
|
||||||
onClick={() => isRoot && onSort(CategoryListUrlSortField.name)}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="vEYtiq" defaultMessage="Category Name" />
|
|
||||||
</TableCellHeader>
|
|
||||||
<TableCellHeader
|
|
||||||
direction={
|
|
||||||
isRoot && sort.sort === CategoryListUrlSortField.subcategoryCount
|
|
||||||
? getArrowDirection(sort.asc)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
className={classes.colSubcategories}
|
|
||||||
disabled={!isRoot}
|
|
||||||
onClick={() =>
|
|
||||||
isRoot && onSort(CategoryListUrlSortField.subcategoryCount)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id="BHQrgz"
|
|
||||||
defaultMessage="Subcategories"
|
|
||||||
description="number of subcategories"
|
|
||||||
/>
|
|
||||||
</TableCellHeader>
|
|
||||||
<TableCellHeader
|
|
||||||
direction={
|
|
||||||
isRoot && sort.sort === CategoryListUrlSortField.productCount
|
|
||||||
? getArrowDirection(sort.asc)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
className={classes.colProducts}
|
|
||||||
disabled={!isRoot}
|
|
||||||
onClick={() =>
|
|
||||||
isRoot && onSort(CategoryListUrlSortField.productCount)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id="k8ZJ5L"
|
|
||||||
defaultMessage="No. of Products"
|
|
||||||
description="number of products"
|
|
||||||
/>
|
|
||||||
</TableCellHeader>
|
|
||||||
</TableHead>
|
|
||||||
<TableFooter>
|
|
||||||
<TableRowLink>
|
|
||||||
<TablePaginationWithContext
|
|
||||||
colSpan={numberOfColumns}
|
|
||||||
settings={settings}
|
|
||||||
onUpdateListSettings={onUpdateListSettings}
|
|
||||||
/>
|
|
||||||
</TableRowLink>
|
|
||||||
</TableFooter>
|
|
||||||
<TableBody>
|
|
||||||
{renderCollection(
|
|
||||||
categories,
|
|
||||||
category => {
|
|
||||||
const isSelected = category ? isChecked(category.id) : false;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowLink
|
|
||||||
className={classes.tableRow}
|
|
||||||
hover={!!category}
|
|
||||||
href={category && categoryUrl(category.id)}
|
|
||||||
key={category ? category.id : "skeleton"}
|
|
||||||
selected={isSelected}
|
|
||||||
data-test-id={"id-" + maybe(() => category.id)}
|
|
||||||
>
|
|
||||||
<TableCell padding="checkbox">
|
|
||||||
<Checkbox
|
|
||||||
checked={isSelected}
|
|
||||||
disabled={disabled}
|
|
||||||
disableClickPropagation
|
|
||||||
onChange={() => toggle(category.id)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.colName} data-test-id="name">
|
|
||||||
{category && category.name ? category.name : <Skeleton />}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.colSubcategories}>
|
|
||||||
{category &&
|
|
||||||
category.children &&
|
|
||||||
category.children.totalCount !== undefined ? (
|
|
||||||
category.children.totalCount
|
|
||||||
) : (
|
|
||||||
<Skeleton />
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className={classes.colProducts}>
|
|
||||||
{category &&
|
|
||||||
category.products &&
|
|
||||||
category.products.totalCount !== undefined ? (
|
|
||||||
category.products.totalCount
|
|
||||||
) : (
|
|
||||||
<Skeleton />
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRowLink>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
() => (
|
|
||||||
<TableRowLink>
|
|
||||||
<TableCell colSpan={numberOfColumns}>
|
|
||||||
{isRoot ? (
|
|
||||||
<FormattedMessage
|
|
||||||
id="dM86a2"
|
|
||||||
defaultMessage="No categories found"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FormattedMessage
|
|
||||||
id="rrbzZt"
|
|
||||||
defaultMessage="No subcategories found"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRowLink>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</ResponsiveTable>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
CategoryList.displayName = "CategoryList";
|
|
||||||
export default CategoryList;
|
|
|
@ -1,2 +0,0 @@
|
||||||
export { default } from "./CategoryList";
|
|
||||||
export * from "./CategoryList";
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
// @ts-strict-ignore
|
||||||
|
import {
|
||||||
|
CategoryListUrlSortField,
|
||||||
|
categoryUrl,
|
||||||
|
} from "@dashboard/categories/urls";
|
||||||
|
import ColumnPicker from "@dashboard/components/ColumnPicker";
|
||||||
|
import Datagrid from "@dashboard/components/Datagrid/Datagrid";
|
||||||
|
import { useColumnsDefault } from "@dashboard/components/Datagrid/hooks/useColumnsDefault";
|
||||||
|
import {
|
||||||
|
DatagridChangeStateContext,
|
||||||
|
useDatagridChangeState,
|
||||||
|
} from "@dashboard/components/Datagrid/hooks/useDatagridChange";
|
||||||
|
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
|
||||||
|
import { CategoryFragment } from "@dashboard/graphql";
|
||||||
|
import { PageListProps, SortPage } from "@dashboard/types";
|
||||||
|
import { Item } from "@glideapps/glide-data-grid";
|
||||||
|
import { Box } from "@saleor/macaw-ui/next";
|
||||||
|
import React, { ReactNode, useCallback, useMemo } from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import { createGetCellContent, getColumns } from "./datagrid";
|
||||||
|
import { messages } from "./messages";
|
||||||
|
|
||||||
|
interface CategoryListDatagridProps
|
||||||
|
extends Partial<SortPage<CategoryListUrlSortField>>,
|
||||||
|
PageListProps {
|
||||||
|
categories?: CategoryFragment[];
|
||||||
|
disabled: boolean;
|
||||||
|
onSelectCategoriesIds: (ids: number[], clearSelection: () => void) => void;
|
||||||
|
selectionActionButton?: ReactNode | null;
|
||||||
|
hasRowHover?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CategoryListDatagrid = ({
|
||||||
|
sort,
|
||||||
|
onSort,
|
||||||
|
categories,
|
||||||
|
disabled,
|
||||||
|
onSelectCategoriesIds,
|
||||||
|
settings,
|
||||||
|
onUpdateListSettings,
|
||||||
|
selectionActionButton = null,
|
||||||
|
hasRowHover = true,
|
||||||
|
}: CategoryListDatagridProps) => {
|
||||||
|
const datagridState = useDatagridChangeState();
|
||||||
|
const intl = useIntl();
|
||||||
|
const availableColumns = useMemo(() => getColumns(intl, sort), [intl, sort]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
availableColumnsChoices,
|
||||||
|
columnChoices,
|
||||||
|
columns,
|
||||||
|
defaultColumns,
|
||||||
|
onColumnMoved,
|
||||||
|
onColumnResize,
|
||||||
|
onColumnsChange,
|
||||||
|
picker,
|
||||||
|
} = useColumnsDefault(availableColumns);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const getCellContent = useCallback(
|
||||||
|
createGetCellContent(categories, columns),
|
||||||
|
[categories, columns],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleHeaderClick = useCallback(
|
||||||
|
(col: number) => {
|
||||||
|
if (sort !== undefined) {
|
||||||
|
onSort(columns[col].id as CategoryListUrlSortField);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[columns, onSort, sort],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRowAnchor = useCallback(
|
||||||
|
([, row]: Item) => categoryUrl(categories[row].id),
|
||||||
|
[categories],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DatagridChangeStateContext.Provider value={datagridState}>
|
||||||
|
<Datagrid
|
||||||
|
readonly
|
||||||
|
hasRowHover={hasRowHover}
|
||||||
|
loading={disabled}
|
||||||
|
columnSelect={sort !== undefined ? "single" : undefined}
|
||||||
|
verticalBorder={col => col > 0}
|
||||||
|
rowMarkers="checkbox"
|
||||||
|
availableColumns={columns}
|
||||||
|
rows={categories?.length ?? 0}
|
||||||
|
getCellContent={getCellContent}
|
||||||
|
getCellError={() => false}
|
||||||
|
emptyText={intl.formatMessage(messages.noData)}
|
||||||
|
onHeaderClicked={handleHeaderClick}
|
||||||
|
rowAnchor={handleRowAnchor}
|
||||||
|
menuItems={() => []}
|
||||||
|
actionButtonPosition="right"
|
||||||
|
selectionActions={() => selectionActionButton}
|
||||||
|
onColumnResize={onColumnResize}
|
||||||
|
onColumnMoved={onColumnMoved}
|
||||||
|
onRowSelectionChange={onSelectCategoriesIds}
|
||||||
|
renderColumnPicker={defaultProps => (
|
||||||
|
<ColumnPicker
|
||||||
|
{...defaultProps}
|
||||||
|
availableColumns={availableColumnsChoices}
|
||||||
|
initialColumns={columnChoices}
|
||||||
|
defaultColumns={defaultColumns}
|
||||||
|
onSave={onColumnsChange}
|
||||||
|
hasMore={false}
|
||||||
|
loading={false}
|
||||||
|
onFetchMore={() => undefined}
|
||||||
|
onQueryChange={picker.setQuery}
|
||||||
|
query={picker.query}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box paddingX={6}>
|
||||||
|
<TablePaginationWithContext
|
||||||
|
component="div"
|
||||||
|
colSpan={1}
|
||||||
|
settings={settings}
|
||||||
|
disabled={disabled}
|
||||||
|
onUpdateListSettings={onUpdateListSettings}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</DatagridChangeStateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
67
src/categories/components/CategoryListDatagrid/datagrid.ts
Normal file
67
src/categories/components/CategoryListDatagrid/datagrid.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// @ts-strict-ignore
|
||||||
|
import { CategoryListUrlSortField } from "@dashboard/categories/urls";
|
||||||
|
import { readonlyTextCell } from "@dashboard/components/Datagrid/customCells/cells";
|
||||||
|
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
|
||||||
|
import { CategoryFragment } from "@dashboard/graphql";
|
||||||
|
import { Sort } from "@dashboard/types";
|
||||||
|
import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon";
|
||||||
|
import { GridCell, Item } from "@glideapps/glide-data-grid";
|
||||||
|
import { IntlShape } from "react-intl";
|
||||||
|
|
||||||
|
import { columnsMessages } from "./messages";
|
||||||
|
|
||||||
|
export const getColumns = (
|
||||||
|
intl: IntlShape,
|
||||||
|
sort?: Sort<CategoryListUrlSortField>,
|
||||||
|
): AvailableColumn[] => [
|
||||||
|
{
|
||||||
|
id: "name",
|
||||||
|
title: intl.formatMessage(columnsMessages.categoryName),
|
||||||
|
width: 350,
|
||||||
|
icon: sort
|
||||||
|
? getColumnSortDirectionIcon(sort, CategoryListUrlSortField.name)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "subcategories",
|
||||||
|
title: intl.formatMessage(columnsMessages.subcategories),
|
||||||
|
width: 300,
|
||||||
|
icon: sort
|
||||||
|
? getColumnSortDirectionIcon(
|
||||||
|
sort,
|
||||||
|
CategoryListUrlSortField.subcategoryCount,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "products",
|
||||||
|
title: intl.formatMessage(columnsMessages.numberOfProducts),
|
||||||
|
width: 300,
|
||||||
|
icon: sort
|
||||||
|
? getColumnSortDirectionIcon(sort, CategoryListUrlSortField.productCount)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const createGetCellContent =
|
||||||
|
(categories: CategoryFragment[], columns: AvailableColumn[]) =>
|
||||||
|
([column, row]: Item): GridCell => {
|
||||||
|
const columnId = columns[column]?.id;
|
||||||
|
|
||||||
|
if (!columnId) {
|
||||||
|
return readonlyTextCell("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowData = categories[row];
|
||||||
|
|
||||||
|
switch (columnId) {
|
||||||
|
case "name":
|
||||||
|
return readonlyTextCell(rowData?.name ?? "");
|
||||||
|
case "subcategories":
|
||||||
|
return readonlyTextCell(rowData?.children?.totalCount.toString() ?? "");
|
||||||
|
case "products":
|
||||||
|
return readonlyTextCell(rowData?.products?.totalCount.toString() ?? "");
|
||||||
|
default:
|
||||||
|
return readonlyTextCell("", false);
|
||||||
|
}
|
||||||
|
};
|
1
src/categories/components/CategoryListDatagrid/index.ts
Normal file
1
src/categories/components/CategoryListDatagrid/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./CategoryListDatagrid";
|
23
src/categories/components/CategoryListDatagrid/messages.ts
Normal file
23
src/categories/components/CategoryListDatagrid/messages.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { defineMessages } from "react-intl";
|
||||||
|
|
||||||
|
export const columnsMessages = defineMessages({
|
||||||
|
categoryName: {
|
||||||
|
id: "kgVqk1",
|
||||||
|
defaultMessage: "Category name",
|
||||||
|
},
|
||||||
|
subcategories: {
|
||||||
|
defaultMessage: "Subcategories",
|
||||||
|
id: "F7DxHw",
|
||||||
|
},
|
||||||
|
numberOfProducts: {
|
||||||
|
defaultMessage: "Number of products",
|
||||||
|
id: "cLcy6F",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const messages = defineMessages({
|
||||||
|
noData: {
|
||||||
|
defaultMessage: "No categories found",
|
||||||
|
id: "dM86a2",
|
||||||
|
},
|
||||||
|
});
|
|
@ -25,6 +25,11 @@ const categoryTableProps: CategoryTableProps = {
|
||||||
...sortPageProps.sort,
|
...sortPageProps.sort,
|
||||||
sort: CategoryListUrlSortField.name,
|
sort: CategoryListUrlSortField.name,
|
||||||
},
|
},
|
||||||
|
onCategoriesDelete: () => undefined,
|
||||||
|
onSelectCategoriesIds: () => undefined,
|
||||||
|
selectedCategoriesIds: [],
|
||||||
|
hasPresetsChanged: false,
|
||||||
|
onTabUpdate: () => undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const meta: Meta<typeof CategoryListPage> = {
|
const meta: Meta<typeof CategoryListPage> = {
|
||||||
|
@ -47,6 +52,7 @@ export const Default: Story = {
|
||||||
export const Loading: Story = {
|
export const Loading: Story = {
|
||||||
args: {
|
args: {
|
||||||
...categoryTableProps,
|
...categoryTableProps,
|
||||||
|
disabled: true,
|
||||||
categories: undefined,
|
categories: undefined,
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
|
|
|
@ -2,32 +2,40 @@ import {
|
||||||
categoryAddUrl,
|
categoryAddUrl,
|
||||||
CategoryListUrlSortField,
|
CategoryListUrlSortField,
|
||||||
} from "@dashboard/categories/urls";
|
} from "@dashboard/categories/urls";
|
||||||
|
import SearchInput from "@dashboard/components/AppLayout/ListFilters/components/SearchInput";
|
||||||
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
||||||
import { Button } from "@dashboard/components/Button";
|
import { Button } from "@dashboard/components/Button";
|
||||||
|
import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
|
||||||
import { ListPageLayout } from "@dashboard/components/Layouts";
|
import { ListPageLayout } from "@dashboard/components/Layouts";
|
||||||
import SearchBar from "@dashboard/components/SearchBar";
|
|
||||||
import { CategoryFragment } from "@dashboard/graphql";
|
import { CategoryFragment } from "@dashboard/graphql";
|
||||||
import { sectionNames } from "@dashboard/intl";
|
import { sectionNames } from "@dashboard/intl";
|
||||||
import {
|
import {
|
||||||
ListActions,
|
|
||||||
PageListProps,
|
PageListProps,
|
||||||
SearchPageProps,
|
SearchPageProps,
|
||||||
SortPage,
|
SortPage,
|
||||||
TabPageProps,
|
TabPageProps,
|
||||||
} from "@dashboard/types";
|
} from "@dashboard/types";
|
||||||
import { Card } from "@material-ui/core";
|
import { Card } from "@material-ui/core";
|
||||||
import React from "react";
|
import { Box, ChevronRightIcon } from "@saleor/macaw-ui/next";
|
||||||
|
import React, { useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import CategoryList from "../CategoryList";
|
import { CategoryDeleteButton } from "../CategoryDeleteButton";
|
||||||
|
import { CategoryListDatagrid } from "../CategoryListDatagrid";
|
||||||
|
import { messages } from "./messages";
|
||||||
|
|
||||||
export interface CategoryTableProps
|
export interface CategoryTableProps
|
||||||
extends PageListProps,
|
extends PageListProps,
|
||||||
ListActions,
|
|
||||||
SearchPageProps,
|
SearchPageProps,
|
||||||
SortPage<CategoryListUrlSortField>,
|
SortPage<CategoryListUrlSortField>,
|
||||||
TabPageProps {
|
Omit<TabPageProps, "onTabDelete"> {
|
||||||
categories: CategoryFragment[];
|
categories: CategoryFragment[];
|
||||||
|
hasPresetsChanged: boolean;
|
||||||
|
selectedCategoriesIds: string[];
|
||||||
|
onTabDelete: (tabIndex: number) => void;
|
||||||
|
onTabUpdate: (tabName: string) => void;
|
||||||
|
onCategoriesDelete: () => void;
|
||||||
|
onSelectCategoriesIds: (ids: number[], clearSelection: () => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CategoryListPage: React.FC<CategoryTableProps> = ({
|
export const CategoryListPage: React.FC<CategoryTableProps> = ({
|
||||||
|
@ -35,69 +43,88 @@ export const CategoryListPage: React.FC<CategoryTableProps> = ({
|
||||||
currentTab,
|
currentTab,
|
||||||
disabled,
|
disabled,
|
||||||
initialSearch,
|
initialSearch,
|
||||||
isChecked,
|
|
||||||
selected,
|
|
||||||
settings,
|
|
||||||
tabs,
|
tabs,
|
||||||
toggle,
|
|
||||||
toggleAll,
|
|
||||||
toolbar,
|
|
||||||
onAll,
|
onAll,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
onTabDelete,
|
onTabDelete,
|
||||||
onTabSave,
|
onTabSave,
|
||||||
onUpdateListSettings,
|
onTabUpdate,
|
||||||
|
hasPresetsChanged,
|
||||||
|
onCategoriesDelete,
|
||||||
|
selectedCategoriesIds,
|
||||||
...listProps
|
...listProps
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const [isFilterPresetOpen, setFilterPresetOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListPageLayout>
|
<ListPageLayout>
|
||||||
<TopNav title={intl.formatMessage(sectionNames.categories)}>
|
<TopNav
|
||||||
|
title={intl.formatMessage(sectionNames.categories)}
|
||||||
|
isAlignToRight={false}
|
||||||
|
withoutBorder
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
__flex={1}
|
||||||
|
display="flex"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Box display="flex">
|
||||||
|
<Box marginX={3} display="flex" alignItems="center">
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<FilterPresetsSelect
|
||||||
|
presetsChanged={hasPresetsChanged}
|
||||||
|
onSelect={onTabChange}
|
||||||
|
onRemove={onTabDelete}
|
||||||
|
onUpdate={onTabUpdate}
|
||||||
|
savedPresets={tabs}
|
||||||
|
activePreset={currentTab}
|
||||||
|
onSelectAll={onAll}
|
||||||
|
onSave={onTabSave}
|
||||||
|
isOpen={isFilterPresetOpen}
|
||||||
|
onOpenChange={setFilterPresetOpen}
|
||||||
|
selectAllLabel={intl.formatMessage(messages.allCategories)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
href={categoryAddUrl()}
|
href={categoryAddUrl()}
|
||||||
data-test-id="create-category"
|
data-test-id="create-category"
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage {...messages.createCategory} />
|
||||||
id="vof5TR"
|
|
||||||
defaultMessage="Create category"
|
|
||||||
description="button"
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
|
</Box>
|
||||||
</TopNav>
|
</TopNav>
|
||||||
<Card>
|
<Card>
|
||||||
<SearchBar
|
<Box
|
||||||
allTabLabel={intl.formatMessage({
|
display="flex"
|
||||||
id: "vy7fjd",
|
justifyContent="space-between"
|
||||||
defaultMessage: "All Categories",
|
alignItems="center"
|
||||||
description: "tab name",
|
paddingX={6}
|
||||||
})}
|
marginBottom={2}
|
||||||
currentTab={currentTab}
|
>
|
||||||
|
<Box __width="320px">
|
||||||
|
<SearchInput
|
||||||
initialSearch={initialSearch}
|
initialSearch={initialSearch}
|
||||||
searchPlaceholder={intl.formatMessage({
|
placeholder={intl.formatMessage(messages.searchCategory)}
|
||||||
id: "JiXNEV",
|
|
||||||
defaultMessage: "Search Category",
|
|
||||||
})}
|
|
||||||
tabs={tabs}
|
|
||||||
onAll={onAll}
|
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={onSearchChange}
|
||||||
onTabChange={onTabChange}
|
|
||||||
onTabDelete={onTabDelete}
|
|
||||||
onTabSave={onTabSave}
|
|
||||||
/>
|
/>
|
||||||
<CategoryList
|
</Box>
|
||||||
categories={categories}
|
{selectedCategoriesIds.length > 0 && (
|
||||||
|
<CategoryDeleteButton onClick={onCategoriesDelete}>
|
||||||
|
<FormattedMessage {...messages.bulkCategoryDelete} />
|
||||||
|
</CategoryDeleteButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<CategoryListDatagrid
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
isChecked={isChecked}
|
categories={categories}
|
||||||
isRoot={true}
|
hasRowHover={!isFilterPresetOpen}
|
||||||
selected={selected}
|
|
||||||
settings={settings}
|
|
||||||
toggle={toggle}
|
|
||||||
toggleAll={toggleAll}
|
|
||||||
toolbar={toolbar}
|
|
||||||
onUpdateListSettings={onUpdateListSettings}
|
|
||||||
{...listProps}
|
{...listProps}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
21
src/categories/components/CategoryListPage/messages.ts
Normal file
21
src/categories/components/CategoryListPage/messages.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { defineMessages } from "react-intl";
|
||||||
|
|
||||||
|
export const messages = defineMessages({
|
||||||
|
allCategories: {
|
||||||
|
id: "1X6HtI",
|
||||||
|
defaultMessage: "All Categories",
|
||||||
|
},
|
||||||
|
createCategory: {
|
||||||
|
id: "vof5TR",
|
||||||
|
defaultMessage: "Create category",
|
||||||
|
description: "button",
|
||||||
|
},
|
||||||
|
searchCategory: {
|
||||||
|
id: "T83iU7",
|
||||||
|
defaultMessage: "Search categories...",
|
||||||
|
},
|
||||||
|
bulkCategoryDelete: {
|
||||||
|
defaultMessage: "Bulk category delete",
|
||||||
|
id: "qU/z0Q",
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,156 +0,0 @@
|
||||||
// @ts-strict-ignore
|
|
||||||
import Checkbox from "@dashboard/components/Checkbox";
|
|
||||||
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
|
|
||||||
import Skeleton from "@dashboard/components/Skeleton";
|
|
||||||
import TableCellAvatar from "@dashboard/components/TableCellAvatar";
|
|
||||||
import { AVATAR_MARGIN } from "@dashboard/components/TableCellAvatar/Avatar";
|
|
||||||
import TableHead from "@dashboard/components/TableHead";
|
|
||||||
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
|
|
||||||
import TableRowLink from "@dashboard/components/TableRowLink";
|
|
||||||
import { CategoryDetailsQuery } from "@dashboard/graphql";
|
|
||||||
import { maybe, renderCollection } from "@dashboard/misc";
|
|
||||||
import { productUrl } from "@dashboard/products/urls";
|
|
||||||
import { ListActions, ListProps, RelayToFlat } from "@dashboard/types";
|
|
||||||
import { TableBody, TableCell, TableFooter } from "@material-ui/core";
|
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import React from "react";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
|
||||||
theme => ({
|
|
||||||
[theme.breakpoints.up("lg")]: {
|
|
||||||
colName: {
|
|
||||||
width: "auto",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
colFill: {
|
|
||||||
padding: 0,
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
colName: {},
|
|
||||||
colNameHeader: {
|
|
||||||
marginLeft: AVATAR_MARGIN,
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
tableLayout: "fixed",
|
|
||||||
},
|
|
||||||
tableContainer: {
|
|
||||||
overflowX: "scroll",
|
|
||||||
},
|
|
||||||
textLeft: {
|
|
||||||
textAlign: "left",
|
|
||||||
},
|
|
||||||
textRight: {
|
|
||||||
textAlign: "right",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: "CategoryProductList",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
interface CategoryProductListProps extends ListProps, ListActions {
|
|
||||||
products: RelayToFlat<CategoryDetailsQuery["category"]["products"]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CategoryProductList: React.FC<
|
|
||||||
CategoryProductListProps
|
|
||||||
> = props => {
|
|
||||||
const {
|
|
||||||
disabled,
|
|
||||||
isChecked,
|
|
||||||
products,
|
|
||||||
selected,
|
|
||||||
toggle,
|
|
||||||
toggleAll,
|
|
||||||
toolbar,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const classes = useStyles(props);
|
|
||||||
|
|
||||||
const numberOfColumns = 2;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.tableContainer}>
|
|
||||||
<ResponsiveTable className={classes.table}>
|
|
||||||
<colgroup>
|
|
||||||
<col />
|
|
||||||
<col className={classes.colName} />
|
|
||||||
</colgroup>
|
|
||||||
<TableHead
|
|
||||||
colSpan={numberOfColumns}
|
|
||||||
selected={selected}
|
|
||||||
disabled={disabled}
|
|
||||||
items={products}
|
|
||||||
toggleAll={toggleAll}
|
|
||||||
toolbar={toolbar}
|
|
||||||
>
|
|
||||||
<TableCell className={classes.colName}>
|
|
||||||
<span className={classes.colNameHeader}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="VQLIXd"
|
|
||||||
defaultMessage="Name"
|
|
||||||
description="product"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
</TableHead>
|
|
||||||
<TableFooter>
|
|
||||||
<TableRowLink>
|
|
||||||
<TablePaginationWithContext colSpan={numberOfColumns} />
|
|
||||||
</TableRowLink>
|
|
||||||
</TableFooter>
|
|
||||||
<TableBody>
|
|
||||||
{renderCollection(
|
|
||||||
products,
|
|
||||||
product => {
|
|
||||||
const isSelected = product ? isChecked(product.id) : false;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowLink
|
|
||||||
data-test-id="product-row"
|
|
||||||
selected={isSelected}
|
|
||||||
hover={!!product}
|
|
||||||
key={product ? product.id : "skeleton"}
|
|
||||||
href={product && productUrl(product.id)}
|
|
||||||
className={classes.link}
|
|
||||||
>
|
|
||||||
<TableCell padding="checkbox">
|
|
||||||
<Checkbox
|
|
||||||
checked={isSelected}
|
|
||||||
disabled={disabled}
|
|
||||||
disableClickPropagation
|
|
||||||
onChange={() => toggle(product.id)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCellAvatar
|
|
||||||
className={classes.colName}
|
|
||||||
thumbnail={maybe(() => product.thumbnail.url)}
|
|
||||||
>
|
|
||||||
{product ? product.name : <Skeleton />}
|
|
||||||
</TableCellAvatar>
|
|
||||||
</TableRowLink>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
() => (
|
|
||||||
<TableRowLink>
|
|
||||||
<TableCell colSpan={numberOfColumns}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="Q1Uzbb"
|
|
||||||
defaultMessage="No products found"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRowLink>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</ResponsiveTable>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
CategoryProductList.displayName = "CategoryProductList";
|
|
||||||
export default CategoryProductList;
|
|
|
@ -1,2 +0,0 @@
|
||||||
export { default } from "./CategoryProductList";
|
|
||||||
export * from "./CategoryProductList";
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
// @ts-strict-ignore
|
||||||
|
import Datagrid from "@dashboard/components/Datagrid/Datagrid";
|
||||||
|
import { useColumnsDefault } from "@dashboard/components/Datagrid/hooks/useColumnsDefault";
|
||||||
|
import {
|
||||||
|
DatagridChangeStateContext,
|
||||||
|
useDatagridChangeState,
|
||||||
|
} from "@dashboard/components/Datagrid/hooks/useDatagridChange";
|
||||||
|
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
|
||||||
|
import { CategoryDetailsQuery } from "@dashboard/graphql";
|
||||||
|
import { productUrl } from "@dashboard/products/urls";
|
||||||
|
import { PageListProps, RelayToFlat } from "@dashboard/types";
|
||||||
|
import { Item } from "@glideapps/glide-data-grid";
|
||||||
|
import { Box } from "@saleor/macaw-ui/next";
|
||||||
|
import React, { ReactNode, useCallback, useMemo } from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import { createGetCellContent, getColumns } from "./datagrid";
|
||||||
|
|
||||||
|
interface CategoryListDatagridProps extends PageListProps {
|
||||||
|
products?: RelayToFlat<CategoryDetailsQuery["category"]["products"]>;
|
||||||
|
disabled: boolean;
|
||||||
|
selectionActionButton?: ReactNode | null;
|
||||||
|
onSelectProductsIds: (ids: number[], clearSelection: () => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CategoryProductListDatagrid = ({
|
||||||
|
products,
|
||||||
|
disabled,
|
||||||
|
onSelectProductsIds,
|
||||||
|
settings,
|
||||||
|
onUpdateListSettings,
|
||||||
|
selectionActionButton = null,
|
||||||
|
}: CategoryListDatagridProps) => {
|
||||||
|
const datagridState = useDatagridChangeState();
|
||||||
|
const intl = useIntl();
|
||||||
|
const availableColumns = useMemo(() => getColumns(intl), [intl]);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const getCellContent = useCallback(
|
||||||
|
createGetCellContent(products, availableColumns),
|
||||||
|
[products, availableColumns],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { columns, onColumnMoved, onColumnResize } =
|
||||||
|
useColumnsDefault(availableColumns);
|
||||||
|
|
||||||
|
const handleRowAnchor = useCallback(
|
||||||
|
([, row]: Item) => productUrl(products[row].id),
|
||||||
|
[products],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DatagridChangeStateContext.Provider value={datagridState}>
|
||||||
|
<Datagrid
|
||||||
|
hasRowHover
|
||||||
|
readonly
|
||||||
|
actionButtonPosition="right"
|
||||||
|
loading={disabled}
|
||||||
|
verticalBorder={false}
|
||||||
|
rowMarkers="checkbox"
|
||||||
|
availableColumns={columns}
|
||||||
|
rows={products?.length ?? 0}
|
||||||
|
getCellContent={getCellContent}
|
||||||
|
getCellError={() => false}
|
||||||
|
emptyText={intl.formatMessage({
|
||||||
|
defaultMessage: "No products found",
|
||||||
|
id: "Q1Uzbb",
|
||||||
|
})}
|
||||||
|
rowAnchor={handleRowAnchor}
|
||||||
|
menuItems={() => []}
|
||||||
|
selectionActions={() => selectionActionButton}
|
||||||
|
onColumnResize={onColumnResize}
|
||||||
|
onColumnMoved={onColumnMoved}
|
||||||
|
onRowSelectionChange={onSelectProductsIds}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box paddingX={6}>
|
||||||
|
<TablePaginationWithContext
|
||||||
|
component="div"
|
||||||
|
colSpan={1}
|
||||||
|
settings={settings}
|
||||||
|
disabled={disabled}
|
||||||
|
onUpdateListSettings={onUpdateListSettings}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</DatagridChangeStateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,45 @@
|
||||||
|
// @ts-strict-ignore
|
||||||
|
import {
|
||||||
|
readonlyTextCell,
|
||||||
|
thumbnailCell,
|
||||||
|
} from "@dashboard/components/Datagrid/customCells/cells";
|
||||||
|
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
|
||||||
|
import { CategoryDetailsQuery } from "@dashboard/graphql";
|
||||||
|
import { RelayToFlat } from "@dashboard/types";
|
||||||
|
import { GridCell, Item } from "@glideapps/glide-data-grid";
|
||||||
|
import { IntlShape } from "react-intl";
|
||||||
|
|
||||||
|
import { columnsMessages } from "./messages";
|
||||||
|
|
||||||
|
export const getColumns = (intl: IntlShape): AvailableColumn[] => [
|
||||||
|
{
|
||||||
|
id: "name",
|
||||||
|
title: intl.formatMessage(columnsMessages.name),
|
||||||
|
width: 500,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const createGetCellContent =
|
||||||
|
(
|
||||||
|
products: RelayToFlat<CategoryDetailsQuery["category"]["products"]>,
|
||||||
|
columns: AvailableColumn[],
|
||||||
|
) =>
|
||||||
|
([column, row]: Item): GridCell => {
|
||||||
|
const columnId = columns[column]?.id;
|
||||||
|
|
||||||
|
if (!columnId) {
|
||||||
|
return readonlyTextCell("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowData = products[row];
|
||||||
|
|
||||||
|
switch (columnId) {
|
||||||
|
case "name":
|
||||||
|
const name = rowData?.name ?? "";
|
||||||
|
return thumbnailCell(name, rowData?.thumbnail?.url ?? "", {
|
||||||
|
cursor: "pointer",
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return readonlyTextCell("", false);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./CategoryProductListDatagrid";
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { defineMessages } from "react-intl";
|
||||||
|
|
||||||
|
export const columnsMessages = defineMessages({
|
||||||
|
name: {
|
||||||
|
id: "HAlOn1",
|
||||||
|
defaultMessage: "Name",
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,57 +1,50 @@
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Button } from "@dashboard/components/Button";
|
import { DashboardCard } from "@dashboard/components/Card";
|
||||||
import CardTitle from "@dashboard/components/CardTitle";
|
|
||||||
import HorizontalSpacer from "@dashboard/components/HorizontalSpacer";
|
|
||||||
import { InternalLink } from "@dashboard/components/InternalLink";
|
import { InternalLink } from "@dashboard/components/InternalLink";
|
||||||
import { CategoryDetailsQuery } from "@dashboard/graphql";
|
import { CategoryDetailsQuery } from "@dashboard/graphql";
|
||||||
import { productAddUrl, productListUrl } from "@dashboard/products/urls";
|
import { productAddUrl, productListUrl } from "@dashboard/products/urls";
|
||||||
import { Card } from "@material-ui/core";
|
import { RelayToFlat } from "@dashboard/types";
|
||||||
|
import { Box, Button } from "@saleor/macaw-ui/next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { ListActions, PageListProps, RelayToFlat } from "../../../types";
|
import { CategoryDeleteButton } from "../CategoryDeleteButton";
|
||||||
import CategoryProductList from "../CategoryProductList";
|
import { CategoryProductListDatagrid } from "../CategoryProductListDatagrid";
|
||||||
import { useStyles } from "./styles";
|
|
||||||
|
|
||||||
interface CategoryProductsProps extends PageListProps, ListActions {
|
interface CategoryProductsProps {
|
||||||
products: RelayToFlat<CategoryDetailsQuery["category"]["products"]>;
|
category: CategoryDetailsQuery["category"];
|
||||||
categoryName: string;
|
|
||||||
categoryId: string;
|
categoryId: string;
|
||||||
|
products: RelayToFlat<CategoryDetailsQuery["category"]["products"]>;
|
||||||
|
disabled: boolean;
|
||||||
|
onProductsDelete: () => void;
|
||||||
|
onSelectProductsIds: (ids: number[], clearSelection: () => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CategoryProducts: React.FC<CategoryProductsProps> = ({
|
export const CategoryProducts = ({
|
||||||
|
category,
|
||||||
|
categoryId,
|
||||||
products,
|
products,
|
||||||
disabled,
|
disabled,
|
||||||
categoryId,
|
onProductsDelete,
|
||||||
categoryName,
|
onSelectProductsIds,
|
||||||
isChecked,
|
}: CategoryProductsProps) => (
|
||||||
selected,
|
<DashboardCard>
|
||||||
toggle,
|
<DashboardCard.Title>
|
||||||
toggleAll,
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
toolbar,
|
<FormattedMessage
|
||||||
}) => {
|
id="+43JV5"
|
||||||
const intl = useIntl();
|
defaultMessage="Products in {categoryName}"
|
||||||
const classes = useStyles();
|
description="header"
|
||||||
|
values={{ categoryName: category?.name }}
|
||||||
|
/>
|
||||||
|
|
||||||
return (
|
<Box display="flex" gap={4}>
|
||||||
<Card>
|
|
||||||
<CardTitle
|
|
||||||
title={intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: "+43JV5",
|
|
||||||
defaultMessage: "Products in {categoryName}",
|
|
||||||
description: "header",
|
|
||||||
},
|
|
||||||
{ categoryName },
|
|
||||||
)}
|
|
||||||
toolbar={
|
|
||||||
<div className={classes.toolbar}>
|
|
||||||
<InternalLink
|
<InternalLink
|
||||||
to={productListUrl({
|
to={productListUrl({
|
||||||
categories: [categoryId],
|
categories: [categoryId],
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Button variant="tertiary" data-test-id="view-products">
|
<Button variant="secondary" data-test-id="view-products">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="z8jo8h"
|
id="z8jo8h"
|
||||||
defaultMessage="View products"
|
defaultMessage="View products"
|
||||||
|
@ -59,33 +52,34 @@ export const CategoryProducts: React.FC<CategoryProductsProps> = ({
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</InternalLink>
|
</InternalLink>
|
||||||
<HorizontalSpacer />
|
|
||||||
<Button
|
<InternalLink to={productAddUrl()}>
|
||||||
variant="tertiary"
|
<Button variant="secondary" data-test-id="add-products">
|
||||||
href={productAddUrl()}
|
|
||||||
data-test-id="add-products"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="x/pIZ9"
|
id="x/pIZ9"
|
||||||
defaultMessage="Add product"
|
defaultMessage="Add product"
|
||||||
description="button"
|
description="button"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</InternalLink>
|
||||||
}
|
</Box>
|
||||||
/>
|
</Box>
|
||||||
<CategoryProductList
|
</DashboardCard.Title>
|
||||||
|
|
||||||
|
<CategoryProductListDatagrid
|
||||||
products={products}
|
products={products}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
selected={selected}
|
onSelectProductsIds={onSelectProductsIds}
|
||||||
isChecked={isChecked}
|
selectionActionButton={
|
||||||
toggle={toggle}
|
<Box paddingRight={5}>
|
||||||
toggleAll={toggleAll}
|
<CategoryDeleteButton onClick={onProductsDelete}>
|
||||||
toolbar={toolbar}
|
<FormattedMessage
|
||||||
|
defaultMessage="Bulk products delete"
|
||||||
|
id="cxOmce"
|
||||||
/>
|
/>
|
||||||
</Card>
|
</CategoryDeleteButton>
|
||||||
);
|
</Box>
|
||||||
};
|
}
|
||||||
|
/>
|
||||||
CategoryProducts.displayName = "CategoryProducts";
|
</DashboardCard>
|
||||||
export default CategoryProducts;
|
);
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export { default } from "./CategoryProducts";
|
export * from "./CategoryProducts";
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
|
|
||||||
export const useStyles = makeStyles(
|
|
||||||
() => ({
|
|
||||||
toolbar: {
|
|
||||||
display: "flex",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{ name: "CategoryProducts" },
|
|
||||||
);
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
// @ts-strict-ignore
|
||||||
|
import { categoryAddUrl } from "@dashboard/categories/urls";
|
||||||
|
import { DashboardCard } from "@dashboard/components/Card";
|
||||||
|
import { InternalLink } from "@dashboard/components/InternalLink";
|
||||||
|
import { CategoryDetailsQuery } from "@dashboard/graphql";
|
||||||
|
import { RelayToFlat } from "@dashboard/types";
|
||||||
|
import { Box, Button } from "@saleor/macaw-ui/next";
|
||||||
|
import React from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
import { CategoryDeleteButton } from "../CategoryDeleteButton";
|
||||||
|
import { CategoryListDatagrid } from "../CategoryListDatagrid";
|
||||||
|
|
||||||
|
interface CategorySubcategoriesProps {
|
||||||
|
categoryId: string;
|
||||||
|
disabled: boolean;
|
||||||
|
subcategories: RelayToFlat<CategoryDetailsQuery["category"]["children"]>;
|
||||||
|
onCategoriesDelete: () => void;
|
||||||
|
onSelectCategoriesIds: (ids: number[], clearSelection: () => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CategorySubcategories = ({
|
||||||
|
categoryId,
|
||||||
|
subcategories,
|
||||||
|
disabled,
|
||||||
|
onCategoriesDelete,
|
||||||
|
onSelectCategoriesIds,
|
||||||
|
}: CategorySubcategoriesProps) => (
|
||||||
|
<DashboardCard>
|
||||||
|
<DashboardCard.Title>
|
||||||
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
|
<FormattedMessage
|
||||||
|
id="NivJal"
|
||||||
|
defaultMessage="All Subcategories"
|
||||||
|
description="section header"
|
||||||
|
/>
|
||||||
|
<InternalLink to={categoryAddUrl(categoryId)}>
|
||||||
|
<Button variant="secondary" data-test-id="create-subcategory">
|
||||||
|
<FormattedMessage
|
||||||
|
id="UycVMp"
|
||||||
|
defaultMessage="Create subcategory"
|
||||||
|
description="button"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</InternalLink>
|
||||||
|
</Box>
|
||||||
|
</DashboardCard.Title>
|
||||||
|
|
||||||
|
<CategoryListDatagrid
|
||||||
|
categories={subcategories}
|
||||||
|
disabled={disabled}
|
||||||
|
onSelectCategoriesIds={onSelectCategoriesIds}
|
||||||
|
selectionActionButton={
|
||||||
|
<Box paddingRight={5}>
|
||||||
|
<CategoryDeleteButton onClick={onCategoriesDelete}>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Bulk categories delete"
|
||||||
|
id="ZN5IZl"
|
||||||
|
/>
|
||||||
|
</CategoryDeleteButton>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</DashboardCard>
|
||||||
|
);
|
1
src/categories/components/CategorySubcategories/index.ts
Normal file
1
src/categories/components/CategorySubcategories/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./CategorySubcategories";
|
|
@ -26,11 +26,13 @@ const updateProps: Omit<CategoryUpdatePageProps, "classes"> = {
|
||||||
onImageDelete: () => undefined,
|
onImageDelete: () => undefined,
|
||||||
onImageUpload: () => undefined,
|
onImageUpload: () => undefined,
|
||||||
onSubmit: () => undefined,
|
onSubmit: () => undefined,
|
||||||
productListToolbar: null,
|
|
||||||
products: mapEdgesToItems(category.products),
|
products: mapEdgesToItems(category.products),
|
||||||
saveButtonBarState: "default",
|
saveButtonBarState: "default",
|
||||||
subcategories: mapEdgesToItems(category.children),
|
subcategories: mapEdgesToItems(category.children),
|
||||||
subcategoryListToolbar: null,
|
onCategoriesDelete: () => undefined,
|
||||||
|
onProductsDelete: () => undefined,
|
||||||
|
onSelectCategoriesIds: () => undefined,
|
||||||
|
onSelectProductsIds: () => undefined,
|
||||||
...listActionsProps,
|
...listActionsProps,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import {
|
import { categoryListUrl, categoryUrl } from "@dashboard/categories/urls";
|
||||||
categoryAddUrl,
|
|
||||||
categoryListUrl,
|
|
||||||
categoryUrl,
|
|
||||||
} from "@dashboard/categories/urls";
|
|
||||||
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
||||||
import { Button } from "@dashboard/components/Button";
|
|
||||||
import { CardSpacer } from "@dashboard/components/CardSpacer";
|
import { CardSpacer } from "@dashboard/components/CardSpacer";
|
||||||
import CardTitle from "@dashboard/components/CardTitle";
|
|
||||||
import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton";
|
import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton";
|
||||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||||
import { Metadata } from "@dashboard/components/Metadata/Metadata";
|
import { Metadata } from "@dashboard/components/Metadata/Metadata";
|
||||||
|
@ -17,17 +11,16 @@ import { Tab, TabContainer } from "@dashboard/components/Tab";
|
||||||
import { CategoryDetailsQuery, ProductErrorFragment } from "@dashboard/graphql";
|
import { CategoryDetailsQuery, ProductErrorFragment } from "@dashboard/graphql";
|
||||||
import { SubmitPromise } from "@dashboard/hooks/useForm";
|
import { SubmitPromise } from "@dashboard/hooks/useForm";
|
||||||
import useNavigator from "@dashboard/hooks/useNavigator";
|
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||||
import { Card } from "@material-ui/core";
|
|
||||||
import { sprinkles } from "@saleor/macaw-ui/next";
|
import { sprinkles } 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 { maybe } from "../../../misc";
|
import { maybe } from "../../../misc";
|
||||||
import { RelayToFlat, TabListActions } from "../../../types";
|
import { RelayToFlat } from "../../../types";
|
||||||
import CategoryDetailsForm from "../../components/CategoryDetailsForm";
|
import CategoryDetailsForm from "../../components/CategoryDetailsForm";
|
||||||
import CategoryList from "../../components/CategoryList";
|
|
||||||
import CategoryBackground from "../CategoryBackground";
|
import CategoryBackground from "../CategoryBackground";
|
||||||
import CategoryProducts from "../CategoryProducts";
|
import { CategoryProducts } from "../CategoryProducts";
|
||||||
|
import { CategorySubcategories } from "../CategorySubcategories";
|
||||||
import CategoryUpdateForm, { CategoryUpdateData } from "./form";
|
import CategoryUpdateForm, { CategoryUpdateData } from "./form";
|
||||||
|
|
||||||
export enum CategoryPageTab {
|
export enum CategoryPageTab {
|
||||||
|
@ -35,8 +28,7 @@ export enum CategoryPageTab {
|
||||||
products = "products",
|
products = "products",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CategoryUpdatePageProps
|
export interface CategoryUpdatePageProps {
|
||||||
extends TabListActions<"productListToolbar" | "subcategoryListToolbar"> {
|
|
||||||
categoryId: string;
|
categoryId: string;
|
||||||
changeTab: (index: CategoryPageTab) => void;
|
changeTab: (index: CategoryPageTab) => void;
|
||||||
currentTab: CategoryPageTab;
|
currentTab: CategoryPageTab;
|
||||||
|
@ -49,6 +41,10 @@ export interface CategoryUpdatePageProps
|
||||||
addProductHref: string;
|
addProductHref: string;
|
||||||
onImageDelete: () => void;
|
onImageDelete: () => void;
|
||||||
onSubmit: (data: CategoryUpdateData) => SubmitPromise;
|
onSubmit: (data: CategoryUpdateData) => SubmitPromise;
|
||||||
|
onCategoriesDelete: () => void;
|
||||||
|
onProductsDelete: () => void;
|
||||||
|
onSelectProductsIds: (ids: number[], clearSelection: () => void) => void;
|
||||||
|
onSelectCategoriesIds: (ids: number[], clearSelection: () => void) => void;
|
||||||
onImageUpload(file: File);
|
onImageUpload(file: File);
|
||||||
onDelete();
|
onDelete();
|
||||||
}
|
}
|
||||||
|
@ -70,12 +66,10 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onImageDelete,
|
onImageDelete,
|
||||||
onImageUpload,
|
onImageUpload,
|
||||||
isChecked,
|
onSelectCategoriesIds,
|
||||||
productListToolbar,
|
onCategoriesDelete,
|
||||||
selected,
|
onProductsDelete,
|
||||||
subcategoryListToolbar,
|
onSelectProductsIds,
|
||||||
toggle,
|
|
||||||
toggleAll,
|
|
||||||
}: CategoryUpdatePageProps) => {
|
}: CategoryUpdatePageProps) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const navigate = useNavigator();
|
const navigate = useNavigator();
|
||||||
|
@ -100,7 +94,9 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
|
||||||
errors={errors}
|
errors={errors}
|
||||||
onChange={change}
|
onChange={change}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CardSpacer />
|
<CardSpacer />
|
||||||
|
|
||||||
<CategoryBackground
|
<CategoryBackground
|
||||||
data={data}
|
data={data}
|
||||||
onImageUpload={onImageUpload}
|
onImageUpload={onImageUpload}
|
||||||
|
@ -108,7 +104,9 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
|
||||||
image={maybe(() => category.backgroundImage)}
|
image={maybe(() => category.backgroundImage)}
|
||||||
onChange={change}
|
onChange={change}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CardSpacer />
|
<CardSpacer />
|
||||||
|
|
||||||
<SeoForm
|
<SeoForm
|
||||||
helperText={intl.formatMessage({
|
helperText={intl.formatMessage({
|
||||||
id: "wQdR8M",
|
id: "wQdR8M",
|
||||||
|
@ -126,9 +124,13 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
|
||||||
onChange={change}
|
onChange={change}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CardSpacer />
|
<CardSpacer />
|
||||||
|
|
||||||
<Metadata data={data} onChange={handlers.changeMetadata} />
|
<Metadata data={data} onChange={handlers.changeMetadata} />
|
||||||
|
|
||||||
<CardSpacer />
|
<CardSpacer />
|
||||||
|
|
||||||
<TabContainer className={sprinkles({ paddingX: 9 })}>
|
<TabContainer className={sprinkles({ paddingX: 9 })}>
|
||||||
<CategoriesTab
|
<CategoriesTab
|
||||||
isActive={currentTab === CategoryPageTab.categories}
|
isActive={currentTab === CategoryPageTab.categories}
|
||||||
|
@ -140,6 +142,7 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
|
||||||
description="number of subcategories in category"
|
description="number of subcategories in category"
|
||||||
/>
|
/>
|
||||||
</CategoriesTab>
|
</CategoriesTab>
|
||||||
|
|
||||||
<ProductsTab
|
<ProductsTab
|
||||||
testId="products-tab"
|
testId="products-tab"
|
||||||
isActive={currentTab === CategoryPageTab.products}
|
isActive={currentTab === CategoryPageTab.products}
|
||||||
|
@ -152,56 +155,30 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
|
||||||
/>
|
/>
|
||||||
</ProductsTab>
|
</ProductsTab>
|
||||||
</TabContainer>
|
</TabContainer>
|
||||||
|
|
||||||
<CardSpacer />
|
<CardSpacer />
|
||||||
|
|
||||||
{currentTab === CategoryPageTab.categories && (
|
{currentTab === CategoryPageTab.categories && (
|
||||||
<Card>
|
<CategorySubcategories
|
||||||
<CardTitle
|
|
||||||
title={intl.formatMessage({
|
|
||||||
id: "NivJal",
|
|
||||||
defaultMessage: "All Subcategories",
|
|
||||||
description: "section header",
|
|
||||||
})}
|
|
||||||
toolbar={
|
|
||||||
<Button
|
|
||||||
variant="tertiary"
|
|
||||||
href={categoryAddUrl(categoryId)}
|
|
||||||
data-test-id="create-subcategory"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id="UycVMp"
|
|
||||||
defaultMessage="Create subcategory"
|
|
||||||
description="button"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<CategoryList
|
|
||||||
categories={subcategories}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
isChecked={isChecked}
|
subcategories={subcategories}
|
||||||
isRoot={false}
|
onCategoriesDelete={onCategoriesDelete}
|
||||||
selected={selected}
|
onSelectCategoriesIds={onSelectCategoriesIds}
|
||||||
sort={undefined}
|
categoryId={categoryId}
|
||||||
toggle={toggle}
|
|
||||||
toggleAll={toggleAll}
|
|
||||||
toolbar={subcategoryListToolbar}
|
|
||||||
onSort={() => undefined}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentTab === CategoryPageTab.products && (
|
{currentTab === CategoryPageTab.products && (
|
||||||
<CategoryProducts
|
<CategoryProducts
|
||||||
categoryId={category?.id}
|
category={category}
|
||||||
categoryName={category?.name}
|
categoryId={categoryId}
|
||||||
products={products}
|
products={products}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
toggle={toggle}
|
onProductsDelete={onProductsDelete}
|
||||||
toggleAll={toggleAll}
|
onSelectProductsIds={onSelectProductsIds}
|
||||||
selected={selected}
|
|
||||||
isChecked={isChecked}
|
|
||||||
toolbar={productListToolbar}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Savebar
|
<Savebar
|
||||||
onCancel={() => navigate(backHref)}
|
onCancel={() => navigate(backHref)}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
|
|
|
@ -15,21 +15,21 @@ import {
|
||||||
useUpdateMetadataMutation,
|
useUpdateMetadataMutation,
|
||||||
useUpdatePrivateMetadataMutation,
|
useUpdatePrivateMetadataMutation,
|
||||||
} from "@dashboard/graphql";
|
} from "@dashboard/graphql";
|
||||||
import useBulkActions from "@dashboard/hooks/useBulkActions";
|
|
||||||
import useLocalPaginator, {
|
import useLocalPaginator, {
|
||||||
useSectionLocalPaginationState,
|
useSectionLocalPaginationState,
|
||||||
} from "@dashboard/hooks/useLocalPaginator";
|
} from "@dashboard/hooks/useLocalPaginator";
|
||||||
import useNavigator from "@dashboard/hooks/useNavigator";
|
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||||
import useNotifier from "@dashboard/hooks/useNotifier";
|
import useNotifier from "@dashboard/hooks/useNotifier";
|
||||||
import { PaginatorContext } from "@dashboard/hooks/usePaginator";
|
import { PaginatorContext } from "@dashboard/hooks/usePaginator";
|
||||||
|
import { useRowSelection } from "@dashboard/hooks/useRowSelection";
|
||||||
import { commonMessages, errorMessages } from "@dashboard/intl";
|
import { commonMessages, errorMessages } from "@dashboard/intl";
|
||||||
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
|
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
|
||||||
import createMetadataUpdateHandler from "@dashboard/utils/handlers/metadataUpdateHandler";
|
import createMetadataUpdateHandler from "@dashboard/utils/handlers/metadataUpdateHandler";
|
||||||
import { mapEdgesToItems } from "@dashboard/utils/maps";
|
import { mapEdgesToItems } from "@dashboard/utils/maps";
|
||||||
import { getParsedDataForJsonStringField } from "@dashboard/utils/richText/misc";
|
import { getParsedDataForJsonStringField } from "@dashboard/utils/richText/misc";
|
||||||
import { DialogContentText } from "@material-ui/core";
|
import { DialogContentText } from "@material-ui/core";
|
||||||
import { DeleteIcon, IconButton } from "@saleor/macaw-ui";
|
import isEqual from "lodash/isEqual";
|
||||||
import React, { useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import { PAGINATE_BY } from "../../config";
|
import { PAGINATE_BY } from "../../config";
|
||||||
|
@ -64,13 +64,26 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigator();
|
const navigate = useNavigator();
|
||||||
const notify = useNotifier();
|
const notify = useNotifier();
|
||||||
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
|
|
||||||
params.ids,
|
|
||||||
);
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [updateMetadata] = useUpdateMetadataMutation({});
|
const [updateMetadata] = useUpdateMetadataMutation({});
|
||||||
const [updatePrivateMetadata] = useUpdatePrivateMetadataMutation({});
|
const [updatePrivateMetadata] = useUpdatePrivateMetadataMutation({});
|
||||||
|
|
||||||
|
const {
|
||||||
|
clearRowSelection: clearProductRowSelection,
|
||||||
|
selectedRowIds: selectedProductRowIds,
|
||||||
|
setClearDatagridRowSelectionCallback:
|
||||||
|
setClearProductDatagridRowSelectionCallback,
|
||||||
|
setSelectedRowIds: setSelectedProductRowIds,
|
||||||
|
} = useRowSelection();
|
||||||
|
|
||||||
|
const {
|
||||||
|
clearRowSelection: clearCategryRowSelection,
|
||||||
|
selectedRowIds: selectedCategoryRowIds,
|
||||||
|
setClearDatagridRowSelectionCallback:
|
||||||
|
setClearCategoryDatagridRowSelectionCallback,
|
||||||
|
setSelectedRowIds: setSelectedCategoryRowIds,
|
||||||
|
} = useRowSelection();
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<CategoryPageTab>(
|
const [activeTab, setActiveTab] = useState<CategoryPageTab>(
|
||||||
CategoryPageTab.categories,
|
CategoryPageTab.categories,
|
||||||
);
|
);
|
||||||
|
@ -80,7 +93,8 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
||||||
);
|
);
|
||||||
const paginate = useLocalPaginator(setPaginationState);
|
const paginate = useLocalPaginator(setPaginationState);
|
||||||
const changeTab = (tab: CategoryPageTab) => {
|
const changeTab = (tab: CategoryPageTab) => {
|
||||||
reset();
|
clearProductRowSelection();
|
||||||
|
clearCategryRowSelection();
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -90,6 +104,8 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const category = data?.category;
|
const category = data?.category;
|
||||||
|
const subcategories = mapEdgesToItems(data?.category?.children);
|
||||||
|
const products = mapEdgesToItems(data?.category?.products);
|
||||||
|
|
||||||
const handleCategoryDelete = (data: CategoryDeleteMutation) => {
|
const handleCategoryDelete = (data: CategoryDeleteMutation) => {
|
||||||
if (data.categoryDelete.errors.length === 0) {
|
if (data.categoryDelete.errors.length === 0) {
|
||||||
|
@ -100,6 +116,7 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
||||||
defaultMessage: "Category deleted",
|
defaultMessage: "Category deleted",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
clearProductRowSelection();
|
||||||
navigate(categoryListUrl());
|
navigate(categoryListUrl());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -109,6 +126,7 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCategoryUpdate = (data: CategoryUpdateMutation) => {
|
const handleCategoryUpdate = (data: CategoryUpdateMutation) => {
|
||||||
|
clearProductRowSelection();
|
||||||
if (data.categoryUpdate.errors.length > 0) {
|
if (data.categoryUpdate.errors.length > 0) {
|
||||||
const backgroundImageError = data.categoryUpdate.errors.find(
|
const backgroundImageError = data.categoryUpdate.errors.find(
|
||||||
error => error.field === ("backgroundImage" as keyof CategoryInput),
|
error => error.field === ("backgroundImage" as keyof CategoryInput),
|
||||||
|
@ -133,13 +151,13 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleBulkCategoryDelete = (data: CategoryBulkDeleteMutation) => {
|
const handleBulkCategoryDelete = (data: CategoryBulkDeleteMutation) => {
|
||||||
|
clearCategryRowSelection();
|
||||||
if (data.categoryBulkDelete.errors.length === 0) {
|
if (data.categoryBulkDelete.errors.length === 0) {
|
||||||
closeModal();
|
closeModal();
|
||||||
notify({
|
notify({
|
||||||
status: "success",
|
status: "success",
|
||||||
text: intl.formatMessage(commonMessages.savedChanges),
|
text: intl.formatMessage(commonMessages.savedChanges),
|
||||||
});
|
});
|
||||||
reset();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -151,6 +169,7 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
||||||
const [productBulkDelete, productBulkDeleteOpts] =
|
const [productBulkDelete, productBulkDeleteOpts] =
|
||||||
useProductBulkDeleteMutation({
|
useProductBulkDeleteMutation({
|
||||||
onCompleted: data => {
|
onCompleted: data => {
|
||||||
|
clearProductRowSelection();
|
||||||
if (data.productBulkDelete.errors.length === 0) {
|
if (data.productBulkDelete.errors.length === 0) {
|
||||||
closeModal();
|
closeModal();
|
||||||
notify({
|
notify({
|
||||||
|
@ -158,7 +177,6 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
||||||
text: intl.formatMessage(commonMessages.savedChanges),
|
text: intl.formatMessage(commonMessages.savedChanges),
|
||||||
});
|
});
|
||||||
refetch();
|
refetch();
|
||||||
reset();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -194,6 +212,52 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSetSelectedCategoryIds = useCallback(
|
||||||
|
(rows: number[], clearSelection: () => void) => {
|
||||||
|
if (!subcategories) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowsIds = rows.map(row => subcategories[row].id);
|
||||||
|
const haveSaveValues = isEqual(rowsIds, selectedCategoryRowIds);
|
||||||
|
|
||||||
|
if (!haveSaveValues) {
|
||||||
|
setSelectedCategoryRowIds(rowsIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
setClearCategoryDatagridRowSelectionCallback(clearSelection);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
selectedCategoryRowIds,
|
||||||
|
setClearCategoryDatagridRowSelectionCallback,
|
||||||
|
setSelectedCategoryRowIds,
|
||||||
|
subcategories,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSetSelectedPrductIds = useCallback(
|
||||||
|
(rows: number[], clearSelection: () => void) => {
|
||||||
|
if (!products) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowsIds = rows.map(row => products[row].id);
|
||||||
|
const haveSaveValues = isEqual(rowsIds, selectedProductRowIds);
|
||||||
|
|
||||||
|
if (!haveSaveValues) {
|
||||||
|
setSelectedProductRowIds(rowsIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
setClearProductDatagridRowSelectionCallback(clearSelection);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
products,
|
||||||
|
selectedProductRowIds,
|
||||||
|
setClearProductDatagridRowSelectionCallback,
|
||||||
|
setSelectedProductRowIds,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSubmit = createMetadataUpdateHandler(
|
const handleSubmit = createMetadataUpdateHandler(
|
||||||
data?.category,
|
data?.category,
|
||||||
handleUpdate,
|
handleUpdate,
|
||||||
|
@ -238,41 +302,19 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
products={mapEdgesToItems(data?.category?.products)}
|
products={products}
|
||||||
saveButtonBarState={updateResult.status}
|
saveButtonBarState={updateResult.status}
|
||||||
subcategories={mapEdgesToItems(data?.category?.children)}
|
subcategories={subcategories}
|
||||||
subcategoryListToolbar={
|
onSelectCategoriesIds={handleSetSelectedCategoryIds}
|
||||||
<IconButton
|
onSelectProductsIds={handleSetSelectedPrductIds}
|
||||||
data-test-id="delete-icon"
|
onCategoriesDelete={() => {
|
||||||
variant="secondary"
|
openModal("delete-categories");
|
||||||
color="primary"
|
}}
|
||||||
onClick={() =>
|
onProductsDelete={() => {
|
||||||
openModal("delete-categories", {
|
openModal("delete-products");
|
||||||
ids: listElements,
|
}}
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
productListToolbar={
|
|
||||||
<IconButton
|
|
||||||
data-test-id="delete-icon"
|
|
||||||
color="primary"
|
|
||||||
onClick={() =>
|
|
||||||
openModal("delete-products", {
|
|
||||||
ids: listElements,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
isChecked={isSelected}
|
|
||||||
selected={listElements.length}
|
|
||||||
toggle={toggle}
|
|
||||||
toggleAll={toggleAll}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ActionDialog
|
<ActionDialog
|
||||||
confirmButtonState={deleteResult.status}
|
confirmButtonState={deleteResult.status}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
|
@ -303,16 +345,14 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
||||||
/>
|
/>
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</ActionDialog>
|
</ActionDialog>
|
||||||
|
|
||||||
<ActionDialog
|
<ActionDialog
|
||||||
open={
|
open={params.action === "delete-categories"}
|
||||||
params.action === "delete-categories" &&
|
|
||||||
maybe(() => params.ids.length > 0)
|
|
||||||
}
|
|
||||||
confirmButtonState={categoryBulkDeleteOpts.status}
|
confirmButtonState={categoryBulkDeleteOpts.status}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
onConfirm={() =>
|
onConfirm={() =>
|
||||||
categoryBulkDelete({
|
categoryBulkDelete({
|
||||||
variables: { ids: params.ids },
|
variables: { ids: selectedCategoryRowIds },
|
||||||
}).then(() => refetch())
|
}).then(() => refetch())
|
||||||
}
|
}
|
||||||
title={intl.formatMessage({
|
title={intl.formatMessage({
|
||||||
|
@ -327,9 +367,9 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
||||||
id="Pp/7T7"
|
id="Pp/7T7"
|
||||||
defaultMessage="{counter,plural,one{Are you sure you want to delete this category?} other{Are you sure you want to delete {displayQuantity} categories?}}"
|
defaultMessage="{counter,plural,one{Are you sure you want to delete this category?} other{Are you sure you want to delete {displayQuantity} categories?}}"
|
||||||
values={{
|
values={{
|
||||||
counter: maybe(() => params.ids.length),
|
counter: maybe(() => selectedCategoryRowIds.length),
|
||||||
displayQuantity: (
|
displayQuantity: (
|
||||||
<strong>{maybe(() => params.ids.length)}</strong>
|
<strong>{maybe(() => selectedCategoryRowIds.length)}</strong>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -341,13 +381,14 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
||||||
/>
|
/>
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</ActionDialog>
|
</ActionDialog>
|
||||||
|
|
||||||
<ActionDialog
|
<ActionDialog
|
||||||
open={params.action === "delete-products"}
|
open={params.action === "delete-products"}
|
||||||
confirmButtonState={productBulkDeleteOpts.status}
|
confirmButtonState={productBulkDeleteOpts.status}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
onConfirm={() =>
|
onConfirm={() =>
|
||||||
productBulkDelete({
|
productBulkDelete({
|
||||||
variables: { ids: params.ids },
|
variables: { ids: selectedProductRowIds },
|
||||||
}).then(() => refetch())
|
}).then(() => refetch())
|
||||||
}
|
}
|
||||||
title={intl.formatMessage({
|
title={intl.formatMessage({
|
||||||
|
@ -362,9 +403,9 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
||||||
id="7l5Bh9"
|
id="7l5Bh9"
|
||||||
defaultMessage="{counter,plural,one{Are you sure you want to delete this product?} other{Are you sure you want to delete {displayQuantity} products?}}"
|
defaultMessage="{counter,plural,one{Are you sure you want to delete this product?} other{Are you sure you want to delete {displayQuantity} products?}}"
|
||||||
values={{
|
values={{
|
||||||
counter: maybe(() => params.ids.length),
|
counter: maybe(() => selectedProductRowIds.length),
|
||||||
displayQuantity: (
|
displayQuantity: (
|
||||||
<strong>{maybe(() => params.ids.length)}</strong>
|
<strong>{maybe(() => selectedProductRowIds.length)}</strong>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import ActionDialog from "@dashboard/components/ActionDialog";
|
import ActionDialog from "@dashboard/components/ActionDialog";
|
||||||
import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog";
|
import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog";
|
||||||
import SaveFilterTabDialog, {
|
import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog";
|
||||||
SaveFilterTabDialogFormData,
|
|
||||||
} from "@dashboard/components/SaveFilterTabDialog";
|
|
||||||
import {
|
import {
|
||||||
CategoryBulkDeleteMutation,
|
CategoryBulkDeleteMutation,
|
||||||
useCategoryBulkDeleteMutation,
|
useCategoryBulkDeleteMutation,
|
||||||
useRootCategoriesQuery,
|
useRootCategoriesQuery,
|
||||||
} from "@dashboard/graphql";
|
} from "@dashboard/graphql";
|
||||||
import useBulkActions from "@dashboard/hooks/useBulkActions";
|
import { useFilterPresets } from "@dashboard/hooks/useFilterPresets";
|
||||||
import useListSettings from "@dashboard/hooks/useListSettings";
|
import useListSettings from "@dashboard/hooks/useListSettings";
|
||||||
import useNavigator from "@dashboard/hooks/useNavigator";
|
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||||
import { usePaginationReset } from "@dashboard/hooks/usePaginationReset";
|
import { usePaginationReset } from "@dashboard/hooks/usePaginationReset";
|
||||||
|
@ -17,6 +15,7 @@ import usePaginator, {
|
||||||
createPaginationState,
|
createPaginationState,
|
||||||
PaginatorContext,
|
PaginatorContext,
|
||||||
} from "@dashboard/hooks/usePaginator";
|
} from "@dashboard/hooks/usePaginator";
|
||||||
|
import { useRowSelection } from "@dashboard/hooks/useRowSelection";
|
||||||
import { maybe } from "@dashboard/misc";
|
import { maybe } from "@dashboard/misc";
|
||||||
import { ListViews } from "@dashboard/types";
|
import { ListViews } from "@dashboard/types";
|
||||||
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
|
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
|
||||||
|
@ -24,8 +23,8 @@ import createSortHandler from "@dashboard/utils/handlers/sortHandler";
|
||||||
import { mapEdgesToItems } from "@dashboard/utils/maps";
|
import { mapEdgesToItems } from "@dashboard/utils/maps";
|
||||||
import { getSortParams } from "@dashboard/utils/sort";
|
import { getSortParams } from "@dashboard/utils/sort";
|
||||||
import { DialogContentText } from "@material-ui/core";
|
import { DialogContentText } from "@material-ui/core";
|
||||||
import { DeleteIcon, IconButton } from "@saleor/macaw-ui";
|
import isEqual from "lodash/isEqual";
|
||||||
import React from "react";
|
import React, { useCallback } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import { CategoryListPage } from "../../components/CategoryListPage/CategoryListPage";
|
import { CategoryListPage } from "../../components/CategoryListPage/CategoryListPage";
|
||||||
|
@ -35,14 +34,7 @@ import {
|
||||||
CategoryListUrlFilters,
|
CategoryListUrlFilters,
|
||||||
CategoryListUrlQueryParams,
|
CategoryListUrlQueryParams,
|
||||||
} from "../../urls";
|
} from "../../urls";
|
||||||
import {
|
import { getActiveFilters, getFilterVariables, storageUtils } from "./filter";
|
||||||
deleteFilterTab,
|
|
||||||
getActiveFilters,
|
|
||||||
getFiltersCurrentTab,
|
|
||||||
getFilterTabs,
|
|
||||||
getFilterVariables,
|
|
||||||
saveFilterTab,
|
|
||||||
} from "./filter";
|
|
||||||
import { getSortQueryVariables } from "./sort";
|
import { getSortQueryVariables } from "./sort";
|
||||||
|
|
||||||
interface CategoryListProps {
|
interface CategoryListProps {
|
||||||
|
@ -51,18 +43,44 @@ interface CategoryListProps {
|
||||||
|
|
||||||
export const CategoryList: React.FC<CategoryListProps> = ({ params }) => {
|
export const CategoryList: React.FC<CategoryListProps> = ({ params }) => {
|
||||||
const navigate = useNavigator();
|
const navigate = useNavigator();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
const { isSelected, listElements, toggle, toggleAll, reset } = useBulkActions(
|
|
||||||
params.ids,
|
|
||||||
);
|
|
||||||
const { updateListSettings, settings } = useListSettings(
|
const { updateListSettings, settings } = useListSettings(
|
||||||
ListViews.CATEGORY_LIST,
|
ListViews.CATEGORY_LIST,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSort = createSortHandler(navigate, categoryListUrl, params);
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedRowIds,
|
||||||
|
setSelectedRowIds,
|
||||||
|
clearRowSelection,
|
||||||
|
setClearDatagridRowSelectionCallback,
|
||||||
|
} = useRowSelection(params);
|
||||||
|
|
||||||
|
const {
|
||||||
|
hasPresetsChange,
|
||||||
|
onPresetChange,
|
||||||
|
onPresetDelete,
|
||||||
|
onPresetSave,
|
||||||
|
onPresetUpdate,
|
||||||
|
presetIdToDelete,
|
||||||
|
presets,
|
||||||
|
selectedPreset,
|
||||||
|
setPresetIdToDelete,
|
||||||
|
} = useFilterPresets({
|
||||||
|
params,
|
||||||
|
storageUtils,
|
||||||
|
getUrl: categoryListUrl,
|
||||||
|
reset: clearRowSelection,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [openModal, closeModal] = createDialogActionHandlers<
|
||||||
|
CategoryListUrlDialog,
|
||||||
|
CategoryListUrlQueryParams
|
||||||
|
>(navigate, categoryListUrl, params);
|
||||||
|
|
||||||
usePaginationReset(categoryListUrl, params, settings.rowNumber);
|
usePaginationReset(categoryListUrl, params, settings.rowNumber);
|
||||||
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const paginationState = createPaginationState(settings.rowNumber, params);
|
const paginationState = createPaginationState(settings.rowNumber, params);
|
||||||
const queryVariables = React.useMemo(
|
const queryVariables = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
@ -70,111 +88,98 @@ export const CategoryList: React.FC<CategoryListProps> = ({ params }) => {
|
||||||
filter: getFilterVariables(params),
|
filter: getFilterVariables(params),
|
||||||
sort: getSortQueryVariables(params),
|
sort: getSortQueryVariables(params),
|
||||||
}),
|
}),
|
||||||
[params, settings.rowNumber],
|
[paginationState, params],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, loading, refetch } = useRootCategoriesQuery({
|
const { data, loading, refetch } = useRootCategoriesQuery({
|
||||||
displayLoader: true,
|
displayLoader: true,
|
||||||
variables: queryVariables,
|
variables: queryVariables,
|
||||||
});
|
});
|
||||||
|
const categories = mapEdgesToItems(data?.categories);
|
||||||
|
|
||||||
const tabs = getFilterTabs();
|
const paginationValues = usePaginator({
|
||||||
|
pageInfo: data?.categories?.pageInfo,
|
||||||
const currentTab = getFiltersCurrentTab(params, tabs);
|
paginationState,
|
||||||
|
queryString: params,
|
||||||
|
});
|
||||||
|
|
||||||
const changeFilterField = (filter: CategoryListUrlFilters) => {
|
const changeFilterField = (filter: CategoryListUrlFilters) => {
|
||||||
reset();
|
clearRowSelection();
|
||||||
navigate(
|
navigate(
|
||||||
categoryListUrl({
|
categoryListUrl({
|
||||||
...getActiveFilters(params),
|
...getActiveFilters(params),
|
||||||
...filter,
|
...filter,
|
||||||
activeTab: undefined,
|
activeTab: !filter.query?.length ? undefined : params.activeTab,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [openModal, closeModal] = createDialogActionHandlers<
|
|
||||||
CategoryListUrlDialog,
|
|
||||||
CategoryListUrlQueryParams
|
|
||||||
>(navigate, categoryListUrl, params);
|
|
||||||
|
|
||||||
const handleTabChange = (tab: number) => {
|
|
||||||
reset();
|
|
||||||
navigate(
|
|
||||||
categoryListUrl({
|
|
||||||
activeTab: tab.toString(),
|
|
||||||
...getFilterTabs()[tab - 1].data,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTabDelete = () => {
|
|
||||||
deleteFilterTab(currentTab);
|
|
||||||
reset();
|
|
||||||
navigate(categoryListUrl());
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTabSave = (data: SaveFilterTabDialogFormData) => {
|
|
||||||
saveFilterTab(data.name, getActiveFilters(params));
|
|
||||||
handleTabChange(tabs.length + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const paginationValues = usePaginator({
|
|
||||||
pageInfo: maybe(() => data.categories.pageInfo),
|
|
||||||
paginationState,
|
|
||||||
queryString: params,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleCategoryBulkDelete = (data: CategoryBulkDeleteMutation) => {
|
const handleCategoryBulkDelete = (data: CategoryBulkDeleteMutation) => {
|
||||||
if (data.categoryBulkDelete.errors.length === 0) {
|
if (data.categoryBulkDelete.errors.length === 0) {
|
||||||
navigate(categoryListUrl(), { replace: true });
|
navigate(categoryListUrl(), { replace: true });
|
||||||
refetch();
|
refetch();
|
||||||
reset();
|
clearRowSelection();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSetSelectedCategoryIds = useCallback(
|
||||||
|
(rows: number[], clearSelection: () => void) => {
|
||||||
|
if (!categories) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowsIds = rows.map(row => categories[row].id);
|
||||||
|
const haveSaveValues = isEqual(rowsIds, selectedRowIds);
|
||||||
|
|
||||||
|
if (!haveSaveValues) {
|
||||||
|
setSelectedRowIds(rowsIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
setClearDatagridRowSelectionCallback(clearSelection);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
categories,
|
||||||
|
setClearDatagridRowSelectionCallback,
|
||||||
|
selectedRowIds,
|
||||||
|
setSelectedRowIds,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
const [categoryBulkDelete, categoryBulkDeleteOpts] =
|
const [categoryBulkDelete, categoryBulkDeleteOpts] =
|
||||||
useCategoryBulkDeleteMutation({
|
useCategoryBulkDeleteMutation({
|
||||||
onCompleted: handleCategoryBulkDelete,
|
onCompleted: handleCategoryBulkDelete,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSort = createSortHandler(navigate, categoryListUrl, params);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaginatorContext.Provider value={paginationValues}>
|
<PaginatorContext.Provider value={paginationValues}>
|
||||||
<CategoryListPage
|
<CategoryListPage
|
||||||
|
hasPresetsChanged={hasPresetsChange()}
|
||||||
categories={mapEdgesToItems(data?.categories)}
|
categories={mapEdgesToItems(data?.categories)}
|
||||||
currentTab={currentTab}
|
currentTab={selectedPreset}
|
||||||
initialSearch={params.query || ""}
|
initialSearch={params.query || ""}
|
||||||
onSearchChange={query => changeFilterField({ query })}
|
onSearchChange={query => changeFilterField({ query })}
|
||||||
onAll={() => navigate(categoryListUrl())}
|
onAll={() => navigate(categoryListUrl())}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={onPresetChange}
|
||||||
onTabDelete={() => openModal("delete-search")}
|
onTabDelete={(tabIndex: number) => {
|
||||||
|
setPresetIdToDelete(tabIndex);
|
||||||
|
openModal("delete-search");
|
||||||
|
}}
|
||||||
|
onTabUpdate={onPresetUpdate}
|
||||||
onTabSave={() => openModal("save-search")}
|
onTabSave={() => openModal("save-search")}
|
||||||
tabs={tabs.map(tab => tab.name)}
|
tabs={presets.map(tab => tab.name)}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
sort={getSortParams(params)}
|
sort={getSortParams(params)}
|
||||||
onSort={handleSort}
|
onSort={handleSort}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onUpdateListSettings={updateListSettings}
|
onUpdateListSettings={(...props) => {
|
||||||
isChecked={isSelected}
|
clearRowSelection();
|
||||||
selected={listElements.length}
|
updateListSettings(...props);
|
||||||
toggle={toggle}
|
}}
|
||||||
toggleAll={toggleAll}
|
selectedCategoriesIds={selectedRowIds}
|
||||||
toolbar={
|
onSelectCategoriesIds={handleSetSelectedCategoryIds}
|
||||||
<IconButton
|
onCategoriesDelete={() => openModal("delete")}
|
||||||
variant="secondary"
|
|
||||||
color="primary"
|
|
||||||
data-test-id="delete-icon"
|
|
||||||
onClick={() =>
|
|
||||||
openModal("delete", {
|
|
||||||
ids: listElements,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ActionDialog
|
<ActionDialog
|
||||||
confirmButtonState={categoryBulkDeleteOpts.status}
|
confirmButtonState={categoryBulkDeleteOpts.status}
|
||||||
onClose={() =>
|
onClose={() =>
|
||||||
|
@ -189,7 +194,7 @@ export const CategoryList: React.FC<CategoryListProps> = ({ params }) => {
|
||||||
onConfirm={() =>
|
onConfirm={() =>
|
||||||
categoryBulkDelete({
|
categoryBulkDelete({
|
||||||
variables: {
|
variables: {
|
||||||
ids: params.ids,
|
ids: selectedRowIds,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -220,18 +225,20 @@ export const CategoryList: React.FC<CategoryListProps> = ({ params }) => {
|
||||||
/>
|
/>
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</ActionDialog>
|
</ActionDialog>
|
||||||
|
|
||||||
<SaveFilterTabDialog
|
<SaveFilterTabDialog
|
||||||
open={params.action === "save-search"}
|
open={params.action === "save-search"}
|
||||||
confirmButtonState="default"
|
confirmButtonState="default"
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
onSubmit={handleTabSave}
|
onSubmit={onPresetSave}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DeleteFilterTabDialog
|
<DeleteFilterTabDialog
|
||||||
open={params.action === "delete-search"}
|
open={params.action === "delete-search"}
|
||||||
confirmButtonState="default"
|
confirmButtonState="default"
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
onSubmit={handleTabDelete}
|
onSubmit={onPresetDelete}
|
||||||
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
|
tabName={presets[presetIdToDelete - 1]?.name ?? "..."}
|
||||||
/>
|
/>
|
||||||
</PaginatorContext.Provider>
|
</PaginatorContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,16 +20,9 @@ export function getFilterVariables(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const {
|
export const storageUtils = createFilterTabUtils<string>(CATEGORY_FILTERS_KEY);
|
||||||
deleteFilterTab,
|
|
||||||
getFilterTabs,
|
|
||||||
saveFilterTab,
|
|
||||||
} = createFilterTabUtils<CategoryListUrlFilters>(CATEGORY_FILTERS_KEY);
|
|
||||||
|
|
||||||
export const {
|
export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } =
|
||||||
areFiltersApplied,
|
createFilterUtils<CategoryListUrlQueryParams, CategoryListUrlFilters>(
|
||||||
getActiveFilters,
|
|
||||||
getFiltersCurrentTab,
|
|
||||||
} = createFilterUtils<CategoryListUrlQueryParams, CategoryListUrlFilters>(
|
|
||||||
CategoryListUrlFiltersEnum,
|
CategoryListUrlFiltersEnum,
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,6 +14,7 @@ import DataEditor, {
|
||||||
GridSelection,
|
GridSelection,
|
||||||
HeaderClickedEventArgs,
|
HeaderClickedEventArgs,
|
||||||
Item,
|
Item,
|
||||||
|
Theme,
|
||||||
} 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 { GetRowThemeCallback } from "@glideapps/glide-data-grid/dist/ts/data-grid/data-grid-render";
|
||||||
import { Card, CardContent, CircularProgress } from "@material-ui/core";
|
import { Card, CardContent, CircularProgress } from "@material-ui/core";
|
||||||
|
@ -104,6 +105,7 @@ export interface DatagridProps {
|
||||||
columnSelect?: DataEditorProps["columnSelect"];
|
columnSelect?: DataEditorProps["columnSelect"];
|
||||||
showEmptyDatagrid?: boolean;
|
showEmptyDatagrid?: boolean;
|
||||||
rowAnchor?: (item: Item) => string;
|
rowAnchor?: (item: Item) => string;
|
||||||
|
actionButtonPosition?: "left" | "right";
|
||||||
recentlyAddedColumn?: string; // Enables scroll to recently added column
|
recentlyAddedColumn?: string; // Enables scroll to recently added column
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,10 +137,11 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
rowAnchor,
|
rowAnchor,
|
||||||
hasRowHover = false,
|
hasRowHover = false,
|
||||||
onRowSelectionChange,
|
onRowSelectionChange,
|
||||||
|
actionButtonPosition = "left",
|
||||||
recentlyAddedColumn,
|
recentlyAddedColumn,
|
||||||
...datagridProps
|
...datagridProps
|
||||||
}): ReactElement => {
|
}): ReactElement => {
|
||||||
const classes = useStyles();
|
const classes = useStyles({ actionButtonPosition });
|
||||||
const { themeValues } = useTheme();
|
const { themeValues } = useTheme();
|
||||||
const datagridTheme = useDatagridTheme(readonly, readonly);
|
const datagridTheme = useDatagridTheme(readonly, readonly);
|
||||||
const editor = useRef<DataEditorRef | null>(null);
|
const editor = useRef<DataEditorRef | null>(null);
|
||||||
|
@ -266,7 +269,9 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
const handleRowHover = useCallback(
|
const handleRowHover = useCallback(
|
||||||
(args: GridMouseEventArgs) => {
|
(args: GridMouseEventArgs) => {
|
||||||
if (hasRowHover) {
|
if (hasRowHover) {
|
||||||
setHoverRow(args.kind !== "cell" ? undefined : args.location[1]);
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const [_, row] = args.location;
|
||||||
|
setHoverRow(args.kind !== "cell" ? undefined : row);
|
||||||
}
|
}
|
||||||
|
|
||||||
// the code below is responsible for adding native <a> element when hovering over rows in the datagrid
|
// the code below is responsible for adding native <a> element when hovering over rows in the datagrid
|
||||||
|
@ -330,12 +335,11 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const overrideTheme = {
|
const overrideTheme: Partial<Theme> = {
|
||||||
bgCell:
|
bgCell:
|
||||||
themeValues.colors.background.interactiveNeutralSecondaryHovering,
|
themeValues.colors.background.interactiveNeutralSecondaryHovering,
|
||||||
bgCellMedium:
|
bgCellMedium:
|
||||||
themeValues.colors.background.interactiveNeutralSecondaryHovering,
|
themeValues.colors.background.interactiveNeutralSecondaryHovering,
|
||||||
accentLight: undefined as string | undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (readonly) {
|
if (readonly) {
|
||||||
|
@ -476,7 +480,9 @@ export const Datagrid: React.FC<DatagridProps> = ({
|
||||||
<CardContent classes={{ root: classes.cardContentRoot }}>
|
<CardContent classes={{ root: classes.cardContentRoot }}>
|
||||||
{rowsTotal > 0 || showEmptyDatagrid ? (
|
{rowsTotal > 0 || showEmptyDatagrid ? (
|
||||||
<>
|
<>
|
||||||
{selection?.rows && selection?.rows.length > 0 && (
|
{selection?.rows &&
|
||||||
|
selection?.rows.length > 0 &&
|
||||||
|
selectionActionsComponent && (
|
||||||
<div className={classes.actionBtnBar}>
|
<div className={classes.actionBtnBar}>
|
||||||
{selectionActionsComponent}
|
{selectionActionsComponent}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,7 +10,7 @@ interface RowActionsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RowActions = ({ menuItems, disabled }: RowActionsProps) => {
|
export const RowActions = ({ menuItems, disabled }: RowActionsProps) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles({});
|
||||||
const hasSingleMenuItem = menuItems.length === 1;
|
const hasSingleMenuItem = menuItems.length === 1;
|
||||||
const firstMenuItem = menuItems[0];
|
const firstMenuItem = menuItems[0];
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useMemo } from "react";
|
||||||
|
|
||||||
export const cellHeight = 40;
|
export const cellHeight = 40;
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles<{ actionButtonPosition?: "left" | "right" }>(
|
||||||
() => {
|
() => {
|
||||||
const rowActionSelected = {
|
const rowActionSelected = {
|
||||||
background: vars.colors.background.plain,
|
background: vars.colors.background.plain,
|
||||||
|
@ -16,6 +16,8 @@ const useStyles = makeStyles(
|
||||||
return {
|
return {
|
||||||
actionBtnBar: {
|
actionBtnBar: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
left: props => (props.actionButtonPosition === "left" ? 0 : "auto"),
|
||||||
|
right: props => (props.actionButtonPosition === "right" ? 0 : "auto"),
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
background: vars.colors.background.plain,
|
background: vars.colors.background.plain,
|
||||||
borderRadius: vars.borderRadius[4],
|
borderRadius: vars.borderRadius[4],
|
||||||
|
|
|
@ -48,6 +48,7 @@ export const FilterPresetsSelect = ({
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const showUpdateButton =
|
const showUpdateButton =
|
||||||
presetsChanged && savedPresets.length > 0 && activePreset;
|
presetsChanged && savedPresets.length > 0 && activePreset;
|
||||||
|
|
||||||
const showSaveButton = presetsChanged;
|
const showSaveButton = presetsChanged;
|
||||||
|
|
||||||
const getLabel = () => {
|
const getLabel = () => {
|
||||||
|
|
Loading…
Reference in a new issue