diff --git a/cypress/components/common/MultiSelectChipInput.cy.tsx b/cypress/components/common/MultiSelectChipInput.cy.tsx
deleted file mode 100644
index 93345e2b2..000000000
--- a/cypress/components/common/MultiSelectChipInput.cy.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import MultiSelectChipInput from '@/components/input/MultiSelectChipInput';
-import {
- MULTI_SELECT_CHIP_ADD_BUTTON_ID,
- MULTI_SELECT_CHIP_CONTAINER_ID,
- MULTI_SELECT_CHIP_INPUT_ID,
- buildDataCyWrapper,
- buildMultiSelectChipsSelector,
-} from '@/config/selectors';
-
-const ON_SAVE_SPY = 'onSave';
-const getSpyOnSave = () => `@${ON_SAVE_SPY}`;
-const eventHandler = { onSave: (_values: string[]) => {} };
-const EXISTING_VALUES = ['first', 'second', 'third'];
-const NEW_VALUE = 'my new value';
-const LABEL = 'my label';
-
-const getInput = () =>
- cy.get(`${buildDataCyWrapper(MULTI_SELECT_CHIP_INPUT_ID)} input`);
-
-const addANewValue = (newValue: string) => {
- getInput().type(newValue);
- cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_ADD_BUTTON_ID)).click();
-};
-
-const removeAValue = (valueToRemove: string) => {
- const idxOfRemovedValue = EXISTING_VALUES.findIndex(
- (value) => value === valueToRemove,
- );
-
- if (idxOfRemovedValue === -1) {
- throw new Error(`Given value to remove "${valueToRemove}" was not found!`);
- }
-
- cy.get(
- `${buildDataCyWrapper(buildMultiSelectChipsSelector(idxOfRemovedValue))} svg`,
- ).click();
-};
-
-describe('', () => {
- beforeEach(() => {
- cy.spy(eventHandler, 'onSave').as(ON_SAVE_SPY);
- });
-
- describe('Data is empty', () => {
- beforeEach(() => {
- cy.mount(
- ,
- );
- });
-
- it('Chips container should not exist when no data', () => {
- cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_CONTAINER_ID)).should(
- 'not.exist',
- );
- });
-
- it('Add a new value should add a new chip', () => {
- addANewValue(NEW_VALUE);
- cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_CONTAINER_ID))
- .children()
- .should('have.length', 1)
- .should('contain', NEW_VALUE);
- });
-
- it('Add a new value should reset current value', () => {
- addANewValue(NEW_VALUE);
- getInput().should('have.value', '');
- });
-
- it('Add a new empty value should not be possible', () => {
- getInput().should('have.value', '');
- cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_ADD_BUTTON_ID)).should(
- 'be.disabled',
- );
- });
- });
-
- describe('Have some data', () => {
- const valueToRemove = EXISTING_VALUES[1];
-
- beforeEach(() => {
- cy.mount(
- ,
- );
- });
-
- it('Chips container should contains existing chips', () => {
- cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_CONTAINER_ID))
- .children()
- .should('have.length', EXISTING_VALUES.length);
- });
-
- it('Add a new value should add a new chip', () => {
- addANewValue(NEW_VALUE);
- cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_CONTAINER_ID))
- .children()
- .should('have.length', EXISTING_VALUES.length + 1)
- .should('contain', NEW_VALUE);
- });
-
- it('Add a new value should call onSave', () => {
- addANewValue(NEW_VALUE);
- cy.get(getSpyOnSave()).should('be.calledWith', [
- ...EXISTING_VALUES,
- NEW_VALUE,
- ]);
- });
-
- it('Add an existing value should not be possible', () => {
- getInput().type(EXISTING_VALUES[0].toUpperCase());
- cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_ADD_BUTTON_ID)).should(
- 'be.disabled',
- );
- });
-
- it('Add an existing value should not call onSave', () => {
- getInput().type(EXISTING_VALUES[0].toUpperCase());
- cy.get(getSpyOnSave()).should('not.be.called');
- });
-
- it('Remove a value should remove the chip', () => {
- removeAValue(valueToRemove);
- cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_CONTAINER_ID))
- .children()
- .should('have.length', EXISTING_VALUES.length - 1)
- .should('not.contain', valueToRemove);
- });
-
- it('Remove a value should call onSave', () => {
- removeAValue(valueToRemove);
- cy.get(getSpyOnSave()).should(
- 'be.calledWith',
- EXISTING_VALUES.filter((e) => e !== valueToRemove),
- );
- });
- });
-});
diff --git a/cypress/e2e/item/publish/categories.cy.ts b/cypress/e2e/item/publish/categories.cy.ts
deleted file mode 100644
index f0d0f57bf..000000000
--- a/cypress/e2e/item/publish/categories.cy.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-import { Category, CategoryType } from '@graasp/sdk';
-
-import { buildItemPath } from '../../../../src/config/paths';
-import {
- CATEGORIES_ADD_BUTTON_HEADER,
- LIBRARY_SETTINGS_CATEGORIES_ID,
- MUI_CHIP_REMOVE_BTN,
- buildCategoryDropdownParentSelector,
- buildCategorySelectionId,
- buildCategorySelectionOptionId,
- buildDataCyWrapper,
- buildDataTestIdWrapper,
- buildPublishButtonId,
- buildPublishChip,
- buildPublishChipContainer,
-} from '../../../../src/config/selectors';
-import {
- ITEM_WITH_CATEGORIES,
- ITEM_WITH_CATEGORIES_CONTEXT,
- SAMPLE_CATEGORIES,
-} from '../../../fixtures/categories';
-import { PUBLISHED_ITEM } from '../../../fixtures/items';
-import { MEMBERS, SIGNED_OUT_MEMBER } from '../../../fixtures/members';
-
-const CATEGORIES_DATA_CY = buildDataCyWrapper(
- buildPublishChipContainer(LIBRARY_SETTINGS_CATEGORIES_ID),
-);
-
-const openPublishItemTab = (id: string) =>
- cy.get(`#${buildPublishButtonId(id)}`).click();
-
-const toggleOption = (
- id: string,
- categoryType: CategoryType | `${CategoryType}`,
-) => {
- cy.get(`#${buildCategorySelectionId(categoryType)}`).click();
- cy.get(`#${buildCategorySelectionOptionId(categoryType, id)}`).click();
-};
-
-const openCategoriesModal = () => {
- cy.get(buildDataCyWrapper(CATEGORIES_ADD_BUTTON_HEADER)).click();
-};
-
-describe('Categories', () => {
- describe('Item without category', () => {
- beforeEach(() => {
- const item = { ...ITEM_WITH_CATEGORIES, categories: [] as Category[] };
- cy.setUpApi({ items: [item] });
- cy.visit(buildItemPath(item.id));
- openPublishItemTab(item.id);
- });
-
- it('Display item without category', () => {
- // check for not displaying if no categories
- cy.get(CATEGORIES_DATA_CY).should('not.exist');
- });
- });
-
- describe('Item with category', () => {
- const item = ITEM_WITH_CATEGORIES;
-
- beforeEach(() => {
- cy.setUpApi(ITEM_WITH_CATEGORIES_CONTEXT);
- cy.visit(buildItemPath(item.id));
- openPublishItemTab(item.id);
- });
-
- it('Display item category', () => {
- // check for displaying value
- const {
- categories: [{ category }],
- } = item;
- const { name } = SAMPLE_CATEGORIES.find(({ id }) => id === category.id);
- const categoryContent = cy.get(CATEGORIES_DATA_CY);
- categoryContent.contains(name);
- });
-
- describe('Delete a category', () => {
- let id: string;
- let category: Category;
- let categoryType: Category['type'];
-
- beforeEach(() => {
- const {
- categories: [itemCategory],
- } = item;
- ({ category, id } = itemCategory);
- categoryType = SAMPLE_CATEGORIES.find(
- ({ id: cId }) => cId === category.id,
- )?.type;
- });
-
- afterEach(() => {
- cy.wait('@deleteItemCategory').then((data) => {
- const {
- request: { url },
- } = data;
- expect(url.split('/')).contains(id);
- });
- });
-
- it('Using Dropdown in modal', () => {
- openCategoriesModal();
- toggleOption(category.id, categoryType);
- });
-
- it('Using cross on category tag in modal', () => {
- openCategoriesModal();
-
- cy.get(
- buildDataCyWrapper(buildCategoryDropdownParentSelector(categoryType)),
- )
- .find(`[data-tag-index=0] > svg`)
- .click();
- });
-
- it('Using cross on category container', () => {
- cy.get(buildDataCyWrapper(buildPublishChip(category.name)))
- .find(buildDataTestIdWrapper(MUI_CHIP_REMOVE_BTN))
- .click();
- });
- });
-
- it('Add a category', () => {
- openCategoriesModal();
- const { type, id } = SAMPLE_CATEGORIES[1];
- toggleOption(id, type);
-
- cy.wait('@postItemCategory').then((data) => {
- const {
- request: { url },
- } = data;
- expect(url.split('/')).contains(item.id);
- });
- });
- });
-
- // users without permission will not see the sections
- describe('Categories permissions', () => {
- it('User signed out cannot edit category level', () => {
- const item = PUBLISHED_ITEM;
-
- cy.setUpApi({
- items: [item],
- currentMember: SIGNED_OUT_MEMBER,
- });
- cy.visit(buildItemPath(item.id));
-
- // signed out user should not be able to see the publish button
- cy.get(`#${buildPublishButtonId(item.id)}`).should('not.exist');
- cy.get(`#${buildCategorySelectionId(CategoryType.Level)}`).should(
- 'not.exist',
- );
- });
-
- it('Read-only user cannot edit category level', () => {
- const item = PUBLISHED_ITEM;
- cy.setUpApi({
- items: [item],
- currentMember: MEMBERS.BOB,
- });
- cy.visit(buildItemPath(item.id));
-
- // signed out user should not be able to see the publish button
- cy.get(`#${buildPublishButtonId(item.id)}`).should('not.exist');
- cy.get(`#${buildCategorySelectionId(CategoryType.Level)}`).should(
- 'not.exist',
- );
- });
- });
-});
diff --git a/cypress/e2e/item/publish/coEditorSettings.cy.ts b/cypress/e2e/item/publish/coEditorSettings.cy.ts
index 23fd1587a..c0baed957 100644
--- a/cypress/e2e/item/publish/coEditorSettings.cy.ts
+++ b/cypress/e2e/item/publish/coEditorSettings.cy.ts
@@ -9,16 +9,16 @@ import {
buildDataCyWrapper,
buildPublishButtonId,
} from '../../../../src/config/selectors';
-import { ITEM_WITH_CATEGORIES_CONTEXT } from '../../../fixtures/categories';
import { MEMBERS, SIGNED_OUT_MEMBER } from '../../../fixtures/members';
+import { ITEM_WITH_TAGS_CONTEXT } from '../../../fixtures/tags';
import { EDIT_TAG_REQUEST_TIMEOUT } from '../../../support/constants';
const openPublishItemTab = (id: string) => {
cy.get(`#${buildPublishButtonId(id)}`).click();
};
const visitItemPage = () => {
- cy.setUpApi(ITEM_WITH_CATEGORIES_CONTEXT);
- const item = ITEM_WITH_CATEGORIES_CONTEXT.items[0];
+ cy.setUpApi(ITEM_WITH_TAGS_CONTEXT);
+ const item = ITEM_WITH_TAGS_CONTEXT.items[0];
cy.visit(buildItemPath(item.id));
openPublishItemTab(item.id);
};
@@ -35,7 +35,7 @@ describe('Co-editor Setting', () => {
it('Change choice', () => {
visitItemPage();
- const item = ITEM_WITH_CATEGORIES_CONTEXT.items[0];
+ const item = ITEM_WITH_TAGS_CONTEXT.items[0];
const newOptionValue = DISPLAY_CO_EDITORS_OPTIONS.NO.value;
cy.wait('@getPublicationStatus').then(() => {
diff --git a/cypress/e2e/item/publish/tags.cy.ts b/cypress/e2e/item/publish/tags.cy.ts
index b91554063..7c439381b 100644
--- a/cypress/e2e/item/publish/tags.cy.ts
+++ b/cypress/e2e/item/publish/tags.cy.ts
@@ -2,97 +2,134 @@ import {
ItemVisibilityType,
PackedFolderItemFactory,
PermissionLevel,
+ TagCategory,
} from '@graasp/sdk';
import { buildItemPath } from '../../../../src/config/paths';
import {
ITEM_HEADER_ID,
- ITEM_TAGS_OPEN_MODAL_BUTTON_ID,
+ ITEM_TAGS_OPEN_MODAL_BUTTON_CY,
MUI_CHIP_REMOVE_BTN,
- MULTI_SELECT_CHIP_ADD_BUTTON_ID,
- MULTI_SELECT_CHIP_INPUT_ID,
buildCustomizedTagsSelector,
buildDataCyWrapper,
buildDataTestIdWrapper,
+ buildMultiSelectChipInputId,
buildPublishButtonId,
} from '../../../../src/config/selectors';
-import {
- ITEM_WITH_CATEGORIES,
- ITEM_WITH_CATEGORIES_CONTEXT,
-} from '../../../fixtures/categories';
import { PUBLISHED_ITEM_NO_TAGS } from '../../../fixtures/items';
import { MEMBERS, SIGNED_OUT_MEMBER } from '../../../fixtures/members';
+import { SAMPLE_TAGS } from '../../../fixtures/tags';
import { EDIT_TAG_REQUEST_TIMEOUT } from '../../../support/constants';
import { ItemForTest } from '../../../support/types';
+const ITEM_WITH_TAGS = {
+ ...PackedFolderItemFactory(
+ {
+ settings: {
+ displayCoEditors: true,
+ },
+ },
+ { permission: PermissionLevel.Admin },
+ ),
+ tags: SAMPLE_TAGS.slice(0, 2),
+};
+
const openPublishItemTab = (id: string) => {
cy.get(`#${buildPublishButtonId(id)}`).click();
};
const visitItemPage = (item: ItemForTest) => {
- cy.setUpApi(ITEM_WITH_CATEGORIES_CONTEXT);
+ cy.setUpApi({ items: [item] });
cy.visit(buildItemPath(item.id));
openPublishItemTab(item.id);
};
describe('Customized Tags', () => {
it('Display item without tags', () => {
- // check for not displaying if no tags
const item = PUBLISHED_ITEM_NO_TAGS;
cy.setUpApi({ items: [item] });
cy.visit(buildItemPath(item.id));
openPublishItemTab(item.id);
- cy.get(buildDataCyWrapper(buildCustomizedTagsSelector(0))).should(
- 'not.exist',
- );
+ // check display edit button
+ cy.get(buildDataCyWrapper(ITEM_TAGS_OPEN_MODAL_BUTTON_CY))
+ // scroll because description can be long
+ .scrollIntoView()
+ .should('be.visible');
});
it('Display tags', () => {
- const item = ITEM_WITH_CATEGORIES;
+ const item = ITEM_WITH_TAGS;
visitItemPage(item);
- expect(item.settings.tags).to.have.lengthOf.above(0);
- item.settings.tags!.forEach((tag, index) => {
+ expect(item.tags).to.have.lengthOf.above(0);
+ item.tags.forEach((tag) => {
const displayTags = cy.get(
- buildDataCyWrapper(buildCustomizedTagsSelector(index)),
+ buildDataCyWrapper(buildCustomizedTagsSelector(tag.id)),
);
- displayTags.contains(tag);
+ displayTags.contains(tag.name);
});
});
it('Remove tag', () => {
- const item = ITEM_WITH_CATEGORIES;
- const removeIdx = 0;
- const removedTag = item.settings.tags[removeIdx];
+ const item = ITEM_WITH_TAGS;
+ const tagToRemove = ITEM_WITH_TAGS.tags[1];
visitItemPage(item);
- cy.get(buildDataCyWrapper(buildCustomizedTagsSelector(removeIdx)))
+ cy.get(buildDataCyWrapper(buildCustomizedTagsSelector(tagToRemove.id)))
.find(buildDataTestIdWrapper(MUI_CHIP_REMOVE_BTN))
.click();
- cy.wait('@editItem', { timeout: EDIT_TAG_REQUEST_TIMEOUT }).then((data) => {
- const {
- request: { url, body },
- } = data;
- expect(url.split('/')).contains(item.id);
- expect(body.settings.tags).not.contains(removedTag);
- });
+ cy.wait('@removeTag', { timeout: EDIT_TAG_REQUEST_TIMEOUT }).then(
+ (data) => {
+ const {
+ request: { url },
+ } = data;
+ expect(url.split('/')).contains(item.id).contains(tagToRemove.id);
+ },
+ );
});
it('Add tag', () => {
- const item = ITEM_WITH_CATEGORIES;
- const newTag = 'My new tag';
+ cy.intercept(
+ {
+ method: 'Get',
+ url: /\/tags\?search=/,
+ },
+ ({ reply }) =>
+ reply([
+ { name: 'secondary school', category: TagCategory.Level },
+ ...SAMPLE_TAGS,
+ ]),
+ ).as('getTags');
+
+ const item = ITEM_WITH_TAGS;
+ const newTag = { name: 'My new tag', category: TagCategory.Level };
visitItemPage(item);
- cy.get(buildDataCyWrapper(ITEM_TAGS_OPEN_MODAL_BUTTON_ID)).click();
- cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_INPUT_ID)).type(newTag);
- cy.get(buildDataCyWrapper(MULTI_SELECT_CHIP_ADD_BUTTON_ID)).click();
+ cy.get(buildDataCyWrapper(ITEM_TAGS_OPEN_MODAL_BUTTON_CY)).click();
+ cy.get(buildDataCyWrapper(buildMultiSelectChipInputId(TagCategory.Level)))
+ // debounce of 500
+ .type(`${newTag.name}`);
+
+ // should call get tags when typing
+ cy.wait('@getTags').then(({ request: { query } }) => {
+ expect(query.search).to.contain(newTag.name);
+ expect(query.category).to.contain(newTag.category);
+ });
+
+ // display options for opened category
+ cy.get(`li:contains("secondary school")`).should('be.visible');
+
+ cy.get(
+ buildDataCyWrapper(buildMultiSelectChipInputId(TagCategory.Level)),
+ ).type('{Enter}');
- cy.wait('@editItem', { timeout: EDIT_TAG_REQUEST_TIMEOUT }).then((data) => {
+ cy.wait('@addTag', { timeout: EDIT_TAG_REQUEST_TIMEOUT }).then((data) => {
const {
request: { url, body },
} = data;
expect(url.split('/')).contains(item.id);
- expect(body.settings.tags).contains(newTag);
+ expect(body.name).contains(newTag.name);
+ expect(body.category).contains(newTag.category);
});
});
});
diff --git a/cypress/fixtures/categories.ts b/cypress/fixtures/tags.ts
similarity index 54%
rename from cypress/fixtures/categories.ts
rename to cypress/fixtures/tags.ts
index f641c710d..01481663b 100644
--- a/cypress/fixtures/categories.ts
+++ b/cypress/fixtures/tags.ts
@@ -1,82 +1,62 @@
import {
- Category,
- CategoryType,
- ItemCategory,
ItemValidation,
ItemValidationProcess,
ItemValidationStatus,
+ Tag,
+ TagCategory,
} from '@graasp/sdk';
import { ItemForTest } from '../support/types';
import { PUBLISHED_ITEM } from './items';
-import { MEMBERS } from './members';
-export const SAMPLE_CATEGORIES: Category[] = [
+export const SAMPLE_TAGS: Tag[] = [
{
id: 'e873d800-5647-442c-930d-2d677532846a',
- name: 'test_category',
- type: CategoryType.Discipline,
+ name: 'discipline 1',
+ category: TagCategory.Discipline,
+ },
+ {
+ id: '152ef74e-8893-4736-926e-214c17396ed3',
+ name: 'level 1',
+ category: TagCategory.Level,
},
{
id: '352ef74e-8893-4736-926e-214c17396ed3',
- name: 'test_category_2',
- type: CategoryType.Level,
+ name: 'level 2',
+ category: TagCategory.Level,
},
{
id: 'ba7f7e3d-dc75-4070-b892-381fbf4759d9',
- name: 'language-1',
- type: CategoryType.Language,
+ name: 'resource-type-1',
+ category: TagCategory.ResourceType,
},
{
id: 'af7f7e3d-dc75-4070-b892-381fbf4759d5',
- name: 'language-2',
- type: CategoryType.Language,
- },
-];
-
-export const SAMPLE_ITEM_CATEGORIES: ItemCategory[] = [
- {
- id: 'e75e1950-c5b4-4e21-95a2-c7c3bfa4072b',
- item: PUBLISHED_ITEM,
- category: SAMPLE_CATEGORIES[0],
- createdAt: '2021-08-11T12:56:36.834Z',
- creator: MEMBERS.ANNA,
+ name: 'resource-type-2',
+ category: TagCategory.ResourceType,
},
];
-export const SAMPLE_ITEM_LANGUAGE: ItemCategory[] = [
- {
- id: 'e75e1950-c5b4-4e21-95a2-c7c3bfa4072b',
- item: PUBLISHED_ITEM,
- category: SAMPLE_CATEGORIES[2],
- createdAt: '2021-08-11T12:56:36.834Z',
- creator: MEMBERS.ANNA,
- },
-];
-
-export const CUSTOMIZED_TAGS = ['water', 'ice', 'temperature'];
-
-export const ITEM_WITH_CATEGORIES: ItemForTest = {
+export const ITEM_WITH_TAGS: ItemForTest = {
...PUBLISHED_ITEM,
settings: {
- tags: CUSTOMIZED_TAGS,
displayCoEditors: true,
},
// for tests
- categories: SAMPLE_ITEM_CATEGORIES,
+ tags: SAMPLE_TAGS,
};
-export const ITEM_WITH_CATEGORIES_CONTEXT = {
- items: [ITEM_WITH_CATEGORIES],
+export const ITEM_WITH_TAGS_CONTEXT = {
+ items: [ITEM_WITH_TAGS],
itemValidationGroups: [
{
id: '65c57d69-0e59-4569-a422-f330c31c995c',
- item: ITEM_WITH_CATEGORIES,
+ item: ITEM_WITH_TAGS,
createdAt: '2021-08-11T12:56:36.834Z',
itemValidations: [
{
id: 'id1',
- item: ITEM_WITH_CATEGORIES,
+ item: ITEM_WITH_TAGS,
// itemValidationGroup: iVG,
process: ItemValidationProcess.BadWordsDetection,
status: ItemValidationStatus.Success,
@@ -86,7 +66,7 @@ export const ITEM_WITH_CATEGORIES_CONTEXT = {
},
{
id: 'id2',
- item: ITEM_WITH_CATEGORIES,
+ item: ITEM_WITH_TAGS,
// itemValidationGroup: iVG,
process: ItemValidationProcess.ImageChecking,
status: ItemValidationStatus.Success,
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index 25c8fe12c..1061ca443 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -6,13 +6,13 @@ import { ItemLayoutMode } from '@/enums';
import { LAYOUT_MODE_BUTTON_ID } from '../../src/config/selectors';
import { APPS_LIST } from '../fixtures/apps/apps';
-import { SAMPLE_CATEGORIES } from '../fixtures/categories';
import { SAMPLE_MENTIONS } from '../fixtures/chatbox';
import { CURRENT_USER, MEMBERS } from '../fixtures/members';
import './commands/item';
import './commands/navigation';
import {
mockAddFavorite,
+ mockAddTag,
mockAppApiAccessToken,
mockCheckShortLink,
mockClearItemChat,
@@ -21,7 +21,6 @@ import {
mockDeleteAppData,
mockDeleteFavorite,
mockDeleteInvitation,
- mockDeleteItemCategory,
mockDeleteItemLoginSchema,
mockDeleteItemMembershipForItem,
mockDeleteItemThumbnail,
@@ -38,11 +37,9 @@ import {
mockGetAppLink,
mockGetAppListRoute,
mockGetAvatarUrl,
- mockGetCategories,
mockGetChildren,
mockGetCurrentMember,
mockGetItem,
- mockGetItemCategories,
mockGetItemChat,
mockGetItemFavorites,
mockGetItemInvitations,
@@ -64,6 +61,7 @@ import {
mockGetPublishItemInformations,
mockGetPublishItemsForMember,
mockGetShortLinksItem,
+ mockGetTagsByItem,
mockImportH5p,
mockImportZip,
mockMoveItems,
@@ -74,7 +72,6 @@ import {
mockPostAvatar,
mockPostInvitations,
mockPostItem,
- mockPostItemCategory,
mockPostItemChatMessage,
mockPostItemFlag,
mockPostItemLogin,
@@ -88,6 +85,7 @@ import {
mockPutItemLoginSchema,
mockRecycleItems,
mockRejectMembershipRequest,
+ mockRemoveTag,
mockRequestMembership,
mockRestoreItems,
mockSignInRedirection,
@@ -109,7 +107,6 @@ Cypress.Commands.add(
members = Object.values(MEMBERS),
currentMember = CURRENT_USER,
mentions = SAMPLE_MENTIONS,
- categories = SAMPLE_CATEGORIES,
itemValidationGroups = [],
itemPublicationStatus = PublicationStatus.Unpublished,
membershipRequests = [],
@@ -137,10 +134,6 @@ Cypress.Commands.add(
postItemThumbnailError = false,
postAvatarError = false,
importZipError = false,
- getCategoriesError = false,
- getItemCategoriesError = false,
- postItemCategoryError = false,
- deleteItemCategoryError = false,
postInvitationsError = false,
getItemInvitationsError = false,
patchInvitationError = false,
@@ -217,8 +210,6 @@ Cypress.Commands.add(
mockSignOut();
- // mockGetItemLogin(items);
-
mockGetItemLoginSchema(items);
mockGetItemLoginSchemaType(items);
@@ -273,7 +264,6 @@ Cypress.Commands.add(
mockRestoreItems(items, restoreItemsError);
- // mockGetItemThumbnail(items, getItemThumbnailError);
mockGetItemThumbnailUrl(items, getItemThumbnailError);
mockDeleteItemThumbnail(items, getItemThumbnailError);
@@ -286,13 +276,10 @@ Cypress.Commands.add(
mockImportZip(importZipError);
- mockGetCategories(categories, getCategoriesError);
-
- mockGetItemCategories(items, getItemCategoriesError);
-
- mockPostItemCategory(postItemCategoryError);
+ mockGetTagsByItem(items);
- mockDeleteItemCategory(deleteItemCategoryError);
+ mockRemoveTag();
+ mockAddTag();
mockGetItemValidationGroups(itemValidationGroups);
diff --git a/cypress/support/server.ts b/cypress/support/server.ts
index 7aaf12f43..3b74b8035 100644
--- a/cypress/support/server.ts
+++ b/cypress/support/server.ts
@@ -2,7 +2,6 @@ import { API_ROUTES } from '@graasp/query-client';
import {
AccountType,
App,
- Category,
ChatMention,
CompleteMembershipRequest,
DiscriminatedItem,
@@ -76,13 +75,9 @@ const {
buildPostItemChatMessageRoute,
buildClearItemChatRoute,
buildDeleteItemVisibilityRoute,
- buildDeleteItemsRoute,
buildUploadItemThumbnailRoute,
buildUploadAvatarRoute,
buildImportZipRoute,
- buildGetCategoriesRoute,
- buildPostItemCategoryRoute,
- buildDeleteItemCategoryRoute,
buildPostInvitationsRoute,
buildGetItemInvitationsForItemRoute,
buildDeleteInvitationRoute,
@@ -253,7 +248,7 @@ export const mockDeleteItems = (
cy.intercept(
{
method: HttpMethod.Delete,
- url: new RegExp(`${API_HOST}/${buildDeleteItemsRoute([])}`),
+ url: new RegExp(`${API_HOST}/items\\?id\\=`),
},
({ reply }) => {
if (shouldThrowError) {
@@ -1475,82 +1470,38 @@ export const mockPostAvatar = (shouldThrowError: boolean): void => {
).as('uploadAvatar');
};
-export const mockGetCategories = (
- categories: Category[],
- shouldThrowError: boolean,
-): void => {
- cy.intercept(
- {
- method: HttpMethod.Get,
- url: new RegExp(
- `${API_HOST}/${parseStringToRegExp(buildGetCategoriesRoute())}`,
- ),
- },
- ({ reply }) => {
- if (shouldThrowError) {
- reply({ statusCode: StatusCodes.BAD_REQUEST });
- return;
- }
- reply(categories);
- },
- ).as('getCategories');
-};
-
-export const mockGetItemCategories = (
- items: ItemForTest[],
- shouldThrowError: boolean,
-): void => {
+export const mockGetTagsByItem = (items: ItemForTest[]): void => {
cy.intercept(
{
method: HttpMethod.Get,
- url: new RegExp(`${API_HOST}/items/${ID_FORMAT}/categories`),
+ url: new RegExp(`${API_HOST}/items/${ID_FORMAT}/tags`),
},
({ reply, url }) => {
- if (shouldThrowError) {
- return reply({ statusCode: StatusCodes.BAD_REQUEST });
- }
const itemId = url.slice(API_HOST.length).split('/')[2];
- const result = items.find(({ id }) => id === itemId)?.categories || [];
+ const result = items.find(({ id }) => id === itemId)?.tags || [];
return reply(result);
},
- ).as('getItemCategories');
+ ).as('getTagsByItem');
};
-export const mockPostItemCategory = (shouldThrowError: boolean): void => {
+export const mockAddTag = (): void => {
cy.intercept(
{
method: HttpMethod.Post,
- url: new RegExp(`${API_HOST}/${buildPostItemCategoryRoute(ID_FORMAT)}$`),
- },
- ({ reply, body }) => {
- if (shouldThrowError) {
- return reply({ statusCode: StatusCodes.BAD_REQUEST });
- }
-
- return reply(body);
+ url: new RegExp(`${API_HOST}/items/${ID_FORMAT}/tags`),
},
- ).as('postItemCategory');
+ ({ reply }) => reply({ status: StatusCodes.NO_CONTENT }),
+ ).as('addTag');
};
-export const mockDeleteItemCategory = (shouldThrowError: boolean): void => {
+export const mockRemoveTag = (): void => {
cy.intercept(
{
method: HttpMethod.Delete,
- url: new RegExp(
- `${API_HOST}/${buildDeleteItemCategoryRoute({
- itemId: ID_FORMAT,
- itemCategoryId: ID_FORMAT,
- })}$`,
- ),
- },
- ({ reply, body }) => {
- if (shouldThrowError) {
- return reply({ statusCode: StatusCodes.BAD_REQUEST });
- }
-
- return reply(body);
+ url: new RegExp(`${API_HOST}/items/${ID_FORMAT}/tags/${ID_FORMAT}`),
},
- ).as('deleteItemCategory');
+ ({ reply }) => reply({ status: StatusCodes.NO_CONTENT }),
+ ).as('removeTag');
};
export const mockGetItemValidationGroups = (
diff --git a/cypress/support/types.ts b/cypress/support/types.ts
index 2eb1b2858..725cbeb4f 100644
--- a/cypress/support/types.ts
+++ b/cypress/support/types.ts
@@ -1,5 +1,4 @@
import {
- Category,
ChatMention,
ChatMessage,
CompleteMember,
@@ -7,7 +6,6 @@ import {
DiscriminatedItem,
Invitation,
ItemBookmark,
- ItemCategory,
ItemLoginSchema,
ItemMembership,
ItemPublished,
@@ -19,11 +17,12 @@ import {
RecycledItemData,
S3FileItemType,
ShortLink,
+ Tag,
ThumbnailsBySize,
} from '@graasp/sdk';
export type ItemForTest = DiscriminatedItem & {
- categories?: ItemCategory[];
+ tags?: Tag[];
thumbnails?: ThumbnailsBySize;
visibilities?: ItemVisibility[];
itemLoginSchema?: ItemLoginSchema;
@@ -54,7 +53,6 @@ export type ApiConfig = {
members?: MemberForTest[];
currentMember?: MemberForTest;
mentions?: ChatMention[];
- categories?: Category[];
shortLinks?: ShortLink[];
itemId?: DiscriminatedItem['id'];
bookmarkedItems?: ItemBookmark[];
@@ -62,7 +60,6 @@ export type ApiConfig = {
itemPublicationStatus?: PublicationStatus;
publishedItemData?: ItemPublished[];
membershipRequests?: CompleteMembershipRequest[];
- // statuses = SAMPLE_STATUSES,
itemValidationGroups?: ItemValidationGroup[];
deleteItemsError?: boolean;
postItemError?: boolean;
diff --git a/package.json b/package.json
index 3cf3d622d..ff3ba7a76 100644
--- a/package.json
+++ b/package.json
@@ -20,10 +20,10 @@
"@emotion/styled": "11.13.0",
"@graasp/chatbox": "3.3.1",
"@graasp/map": "1.19.0",
- "@graasp/query-client": "5.6.0",
- "@graasp/sdk": "5.3.1",
+ "@graasp/query-client": "5.7.0",
+ "@graasp/sdk": "5.4.0",
"@graasp/stylis-plugin-rtl": "2.2.0",
- "@graasp/translations": "1.42.0",
+ "@graasp/translations": "1.43.0",
"@graasp/ui": "5.4.4",
"@mui/icons-material": "6.1.7",
"@mui/lab": "6.0.0-beta.15",
diff --git a/src/components/hooks/useItemCategories.tsx b/src/components/hooks/useItemCategories.tsx
deleted file mode 100644
index aa2e7f7f1..000000000
--- a/src/components/hooks/useItemCategories.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { Category } from '@graasp/sdk';
-
-import { hooks, mutations } from '@/config/queryClient';
-import { Filter } from '@/types/array';
-
-const { useItemCategories: useCategories } = hooks;
-const { usePostItemCategory, useDeleteItemCategory } = mutations;
-
-type Props = {
- itemId: string;
- filterCategories?: Filter;
-};
-
-type UseItemCategories = {
- isLoading: boolean;
- isMutationLoading: boolean;
- isMutationError: boolean;
- isMutationSuccess: boolean;
- categories?: string[];
- addCategory: (categoryId: string) => void;
- deleteCategory: (categoryId: string) => void;
- deleteCategoryByName: (name: string) => void;
-};
-
-export const useItemCategories = ({
- itemId,
- filterCategories = () => true,
-}: Props): UseItemCategories => {
- const { data: itemCategories, isLoading } = useCategories(itemId);
-
- const filteredCategories = itemCategories?.filter(({ category }) =>
- filterCategories(category),
- );
-
- const categories = filteredCategories?.map(({ category }) => category.name);
-
- const {
- mutate: createItemCategory,
- isPending: isPostLoading,
- isSuccess: isPostSuccess,
- isError: isPostError,
- } = usePostItemCategory();
- const {
- mutate: deleteItemCategory,
- isPending: isDeleteLoading,
- isSuccess: isDeleteSuccess,
- isError: isDeleteError,
- } = useDeleteItemCategory();
-
- const isMutationLoading = isPostLoading || isDeleteLoading;
- const isMutationSuccess = isPostSuccess || isDeleteSuccess;
- const isMutationError = isPostError || isDeleteError;
-
- const deleteCategory = (itemCategoryId: string) =>
- deleteItemCategory({
- itemId,
- itemCategoryId,
- });
-
- const deleteCategoryByName = (categoryName: string) => {
- const removedItemCategory = filteredCategories?.find(
- ({ category }) =>
- category.name.toLowerCase() === categoryName.toLowerCase(),
- );
-
- if (!removedItemCategory) {
- console.error('The given category was not found !', categoryName);
- return;
- }
-
- deleteCategory(removedItemCategory.id);
- };
-
- const addCategory = (categoryId: string) =>
- createItemCategory({ itemId, categoryId });
-
- return {
- isLoading,
- isMutationError,
- isMutationLoading,
- isMutationSuccess,
- categories,
- addCategory,
- deleteCategory,
- deleteCategoryByName,
- };
-};
-
-export default useItemCategories;
diff --git a/src/components/input/MultiSelectChipInput.hook.tsx b/src/components/input/MultiSelectChipInput.hook.tsx
deleted file mode 100644
index 2965c2a0f..000000000
--- a/src/components/input/MultiSelectChipInput.hook.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import { useEffect, useRef, useState } from 'react';
-
-import { useBuilderTranslation } from '@/config/i18n';
-import { BUILDER } from '@/langs/constants';
-import CaseInsensitiveSet from '@/types/set';
-
-const EMPTY_STRING = '';
-const EMPTY_SET = new CaseInsensitiveSet();
-
-type SetOfString = CaseInsensitiveSet;
-
-type Props = {
- data?: string[];
- onChange?: (newValues: string[]) => void;
-};
-
-type UseMultiSelectChipInput = {
- values: string[];
- currentValue: string;
- error: string | undefined;
- hasError: boolean;
- hasData: boolean;
- hasChanged: boolean;
-
- updateValues: (newValues: string[]) => void;
- handleCurrentValueChanged: (newValue: string) => void;
- addValue: () => string[];
- deleteValue: (valueToDelete: string) => string[];
-};
-
-export const useMultiSelectChipInput = ({
- data,
- onChange,
-}: Props): UseMultiSelectChipInput => {
- const { t } = useBuilderTranslation();
- const originalData = useRef(EMPTY_SET);
- const [newData, setNewData] = useState(EMPTY_SET);
- const [currentValue, setCurrentValue] = useState(EMPTY_STRING);
- const [error, setError] = useState();
-
- const hasError = Boolean(error);
- const hasData = newData.size() > 0;
- const hasChanged = !originalData.current.isEqual(newData);
-
- // sync the props with the component's state
- useEffect(() => {
- const newSet = new CaseInsensitiveSet(data);
- setNewData(newSet);
- originalData.current = newSet;
- }, [data]);
-
- const valueIsValid = (
- dataToValidate: string | undefined,
- ): dataToValidate is string => Boolean(dataToValidate);
-
- const valueExist = (newValue: string) => newData.has(newValue);
-
- const validateData = (newValue: string) => {
- if (valueExist(newValue)) {
- setError(t(BUILDER.CHIPS_ALREADY_EXIST, { element: newValue }));
- return false;
- }
- setError(undefined);
- return true;
- };
-
- const notifyOnChange = (newValues: string[]) => onChange?.(newValues);
-
- const addValue = () => {
- if (valueIsValid(currentValue) && !valueExist(currentValue)) {
- const newMapValues = newData.copy([currentValue]);
- setNewData(newMapValues);
- setCurrentValue(EMPTY_STRING);
- notifyOnChange(newMapValues.values());
- return newMapValues.values();
- }
-
- return newData.values();
- };
-
- const deleteValue = (valueToDelete: string) => {
- const newMapValues = newData.copy();
- newMapValues.delete(valueToDelete);
- setNewData(newMapValues);
- notifyOnChange(newMapValues.values());
- return newMapValues.values();
- };
-
- const updateValues = (newValues: string[]) => {
- const newMap = new CaseInsensitiveSet(newValues);
- setNewData(newMap);
- notifyOnChange(newMap.values());
- };
-
- const handleCurrentValueChanged = (newValue: string) => {
- validateData(newValue);
- setCurrentValue(newValue);
- };
-
- return {
- values: newData.values(),
- currentValue,
- error,
- hasError,
- hasData,
- hasChanged,
- updateValues,
- handleCurrentValueChanged,
- addValue,
- deleteValue,
- };
-};
-
-export default useMultiSelectChipInput;
diff --git a/src/components/input/MultiSelectChipInput.tsx b/src/components/input/MultiSelectChipInput.tsx
deleted file mode 100644
index c30ce7d1a..000000000
--- a/src/components/input/MultiSelectChipInput.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import AddIcon from '@mui/icons-material/Add';
-import {
- Autocomplete,
- AutocompleteRenderGetTagProps,
- AutocompleteRenderInputParams,
- Box,
- Chip,
- Fab,
- Stack,
- TextField,
- Typography,
-} from '@mui/material';
-
-import { useBuilderTranslation } from '@/config/i18n';
-import {
- MULTI_SELECT_CHIP_ADD_BUTTON_ID,
- MULTI_SELECT_CHIP_CONTAINER_ID,
- MULTI_SELECT_CHIP_INPUT_ID,
- buildMultiSelectChipsSelector,
-} from '@/config/selectors';
-import { BUILDER } from '@/langs/constants';
-
-import { useMultiSelectChipInput } from './MultiSelectChipInput.hook';
-
-type Props = {
- data?: string[];
- label: string;
- onSave: (newValues: string[]) => void;
-};
-
-export const MultiSelectChipInput = ({
- data,
- label,
- onSave,
-}: Props): JSX.Element | null => {
- const { t } = useBuilderTranslation();
- const {
- values,
- currentValue,
- error,
- hasError,
- updateValues,
- handleCurrentValueChanged,
- addValue,
- } = useMultiSelectChipInput({
- data,
- onChange: onSave,
- });
-
- const renderTags = (
- value: readonly string[],
- getTagProps: AutocompleteRenderGetTagProps,
- ) => (
-
- {value.map((option: string, index: number) => (
-
- ))}
-
- );
-
- const renderInput = (params: AutocompleteRenderInputParams) => (
-
- );
-
- return (
-
-
- updateValues(v)}
- inputValue={currentValue}
- onInputChange={(_e, v) => handleCurrentValueChanged(v)}
- renderTags={renderTags}
- renderInput={renderInput}
- />
-
-
-
-
- {error && (
-
- {error}
-
- )}
-
- );
-};
-
-export default MultiSelectChipInput;
diff --git a/src/components/input/MultiSelectTagChipInput.tsx b/src/components/input/MultiSelectTagChipInput.tsx
new file mode 100644
index 000000000..6a846fd34
--- /dev/null
+++ b/src/components/input/MultiSelectTagChipInput.tsx
@@ -0,0 +1,164 @@
+import {
+ Autocomplete,
+ AutocompleteRenderGetTagProps,
+ AutocompleteRenderInputParams,
+ Box,
+ Button,
+ Chip,
+ Skeleton,
+ Stack,
+ TextField,
+} from '@mui/material';
+
+import { DiscriminatedItem, TagCategory } from '@graasp/sdk';
+
+import { useBuilderTranslation, useEnumsTranslation } from '@/config/i18n';
+import { hooks } from '@/config/queryClient';
+import {
+ MULTI_SELECT_CHIP_CONTAINER_ID,
+ buildMultiSelectChipInputId,
+ buildMultiSelectChipsSelector,
+} from '@/config/selectors';
+
+import useTagsManager from '../item/publish/customizedTags/useTagsManager';
+
+type Props = {
+ itemId: DiscriminatedItem['id'];
+ tagCategory: TagCategory;
+};
+
+export const MultiSelectTagChipInput = ({
+ itemId,
+ tagCategory,
+}: Props): JSX.Element | null => {
+ const { t } = useBuilderTranslation();
+ const { t: translateEnums } = useEnumsTranslation();
+ const {
+ currentValue,
+ error,
+ handleCurrentValueChanged,
+ addValue,
+ deleteValue,
+ resetCurrentValue,
+ debouncedCurrentValue,
+ tagsPerCategory,
+ } = useTagsManager({
+ itemId,
+ });
+ const {
+ data: tags,
+ isFetching,
+ isLoading,
+ } = hooks.useTags({ search: debouncedCurrentValue, category: tagCategory });
+ const renderTags = (
+ value: readonly string[],
+ getTagProps: AutocompleteRenderGetTagProps,
+ ) => (
+
+ {value.map((option: string, index: number) => (
+ {
+ const tagId = tagsPerCategory?.[tagCategory].find(
+ ({ name }) => name === option,
+ );
+ if (tagId) {
+ deleteValue(tagId.id);
+ }
+ }}
+ key={option}
+ />
+ ))}
+
+ );
+
+ const renderInput = (params: AutocompleteRenderInputParams) => (
+ handleCurrentValueChanged(e.target.value, tagCategory)}
+ onKeyDown={(e) => {
+ if (e.code === 'Enter' && 'value' in e.target) {
+ addValue({ name: e.target.value as string, category: tagCategory });
+ }
+ }}
+ />
+ );
+
+ const options =
+ tags
+ ?.filter(({ category }) => category === tagCategory)
+ ?.map(({ name }) => name) ?? [];
+
+ return (
+
+
+
+ addValue({
+ name: currentValue,
+ category: tagCategory,
+ })
+ }
+ >
+ {t('ADD_TAG_OPTION_BUTTON_TEXT', { value: currentValue })}
+
+ )
+ }
+ options={options}
+ value={tagsPerCategory?.[tagCategory]?.map(({ name }) => name) ?? []}
+ onChange={(_e, v) => {
+ if (v.length) {
+ addValue({
+ name: v[v.length - 1],
+ category: tagCategory,
+ });
+ }
+ }}
+ renderTags={renderTags}
+ renderOption={(optionProps, name) => (
+
+ {name}
+
+ )}
+ renderInput={renderInput}
+ disableClearable
+ loading={
+ isFetching || isLoading || debouncedCurrentValue !== currentValue
+ }
+ loadingText={}
+ />
+
+
+ );
+};
diff --git a/src/components/item/publish/CategoriesContainer.tsx b/src/components/item/publish/CategoriesContainer.tsx
deleted file mode 100644
index 9a5185a78..000000000
--- a/src/components/item/publish/CategoriesContainer.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { Typography } from '@mui/material';
-
-import { Category, CategoryType } from '@graasp/sdk';
-
-import { useBuilderTranslation } from '@/config/i18n';
-import { LIBRARY_SETTINGS_CATEGORIES_ID } from '@/config/selectors';
-import { BUILDER } from '@/langs/constants';
-
-import ItemCategoryContainer from './ItemCategoryContainer';
-
-type Props = {
- itemId: string;
-};
-
-const CHIP_COLOR = '#5050d2';
-const SYNC_STATUS_KEY = 'PublishCategories';
-
-export const CategoriesContainer = ({ itemId }: Props): JSX.Element | null => {
- const { t } = useBuilderTranslation();
-
- const filterOutLanguage = ({ type }: Category) =>
- type !== CategoryType.Language;
-
- const title = t(BUILDER.ITEM_CATEGORIES_CONTAINER_TITLE);
- const description = t(BUILDER.ITEM_CATEGORIES_CONTAINER_MISSING_WARNING);
- const emptyMessage = t(BUILDER.ITEM_CATEGORIES_CONTAINER_EMPTY_BUTTON);
-
- const modalTitle = (
-
- {t(BUILDER.ITEM_CATEGORIES_CONTAINER_TITLE)}
-
- );
-
- return (
-
- );
-};
-export default CategoriesContainer;
diff --git a/src/components/item/publish/CategorySelection.tsx b/src/components/item/publish/CategorySelection.tsx
deleted file mode 100644
index 7c7830cfa..000000000
--- a/src/components/item/publish/CategorySelection.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import { SyntheticEvent } from 'react';
-
-import { AutocompleteChangeReason, Stack } from '@mui/material';
-
-import { Category, CategoryType } from '@graasp/sdk';
-import { Loader } from '@graasp/ui';
-
-import groupBy from 'lodash.groupby';
-
-import { useCategoriesTranslation } from '@/config/i18n';
-import { hooks } from '@/config/queryClient';
-import { Filter } from '@/types/array';
-import { sortByName } from '@/utils/item';
-
-import DropdownMenu from './DropdownMenu';
-
-const { useCategories, useItemCategories } = hooks;
-
-const SELECT_OPTION = 'selectOption';
-const REMOVE_OPTION = 'removeOption';
-
-type Props = {
- itemId: string;
- titleContent?: JSX.Element;
- filterCategories?: Filter;
- onCreate: (categoryId: string) => void;
- onDelete: (itemCategoryId: string) => void;
-};
-const CategorySelection = ({
- itemId,
- titleContent,
- filterCategories = () => true,
- onCreate,
- onDelete,
-}: Props): JSX.Element | null => {
- const { t: translateCategories } = useCategoriesTranslation();
- const { data: itemCategories, isLoading: isItemCategoriesLoading } =
- useItemCategories(itemId);
- const { data: allCategories, isLoading: isCategoriesLoading } =
- useCategories();
- const isLoading = isItemCategoriesLoading || isCategoriesLoading;
- const filteredCategories = allCategories?.filter(filterCategories);
- const categoriesByType = groupBy(filteredCategories, (entry) => entry.type);
-
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- if (!Object.values(categoriesByType).length) {
- return null;
- }
-
- const handleChange = (
- _event: SyntheticEvent,
- _values: Category[],
- reason: AutocompleteChangeReason,
- details?: { option: Category },
- ) => {
- if (!itemId) {
- console.error('No item id is defined');
- return;
- }
-
- if (reason === SELECT_OPTION) {
- // post new category
- const newCategoryId = details?.option.id;
- if (newCategoryId) {
- onCreate(newCategoryId);
- } else {
- console.error('Unable to create the category!');
- }
- }
- if (reason === REMOVE_OPTION) {
- const deletedCategoryId = details?.option.id;
- const itemCategoryIdToRemove = itemCategories?.find(
- ({ category }) => category.id === deletedCategoryId,
- )?.id;
- if (itemCategoryIdToRemove) {
- onDelete(itemCategoryIdToRemove);
- } else {
- console.error('Unable to delete the category!');
- }
- }
- };
-
- return (
-
- {titleContent}
- {Object.values(CategoryType)?.map((type) => {
- const values =
- categoriesByType[type]
- ?.map((c: Category) => ({
- ...c,
- name: translateCategories(c.name),
- }))
- ?.sort(sortByName) ?? [];
-
- return (
-
- );
- })}
-
- );
-};
-
-export default CategorySelection;
diff --git a/src/components/item/publish/DropdownMenu.tsx b/src/components/item/publish/DropdownMenu.tsx
deleted file mode 100644
index 977fea79b..000000000
--- a/src/components/item/publish/DropdownMenu.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import { SyntheticEvent } from 'react';
-
-import {
- Autocomplete,
- AutocompleteChangeReason,
- Box,
- TextField,
- Typography,
-} from '@mui/material';
-
-import { Category, CategoryType, ItemCategory } from '@graasp/sdk';
-
-import { useBuilderTranslation } from '../../../config/i18n';
-import {
- buildCategoryDropdownParentSelector,
- buildCategorySelectionId,
- buildCategorySelectionOptionId,
- buildCategorySelectionTitleId,
-} from '../../../config/selectors';
-import { BUILDER } from '../../../langs/constants';
-
-type Props = {
- disabled?: boolean;
- type: CategoryType;
- title: string;
- values: Category[];
- selectedValues?: ItemCategory[];
- handleChange: (
- _event: SyntheticEvent,
- value: Category[],
- reason: AutocompleteChangeReason,
- details?: { option: Category },
- ) => void;
-};
-
-const DropdownMenu = ({
- disabled = false,
- type,
- title,
- handleChange,
- values,
- selectedValues,
-}: Props): JSX.Element | null => {
- const { t: translateBuilder } = useBuilderTranslation();
-
- if (!values.length) {
- return null;
- }
-
- const selected = values.filter(({ id }) =>
- selectedValues?.find(({ category }) => category.id === id),
- );
-
- return (
-
-
- {title}
-
- option.name}
- onChange={handleChange}
- renderInput={(params) => (
-
- )}
- renderOption={(props, option) => (
-
- {option.name}
-
- )}
- />
-
- );
-};
-
-export default DropdownMenu;
diff --git a/src/components/item/publish/ItemCategoryContainer.tsx b/src/components/item/publish/ItemCategoryContainer.tsx
deleted file mode 100644
index 41a7f4992..000000000
--- a/src/components/item/publish/ItemCategoryContainer.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { useEffect } from 'react';
-
-import { Category } from '@graasp/sdk';
-
-import { useDataSyncContext } from '@/components/context/DataSyncContext';
-import { Filter } from '@/types/array';
-
-import useItemCategories from '../../hooks/useItemCategories';
-import useModalStatus from '../../hooks/useModalStatus';
-import CategorySelection from './CategorySelection';
-import PublicationChipContainer from './PublicationChipContainer';
-import PublicationModal from './PublicationModal';
-
-type Props = {
- itemId: string;
- title: string;
- description: string;
- emptyMessage: string;
- modalTitle?: JSX.Element;
- chipColor?: string;
- dataTestId: string;
- dataSyncKey: string;
- filterCategories?: Filter;
-};
-
-export const ItemCategoryContainer = ({
- itemId,
- title,
- description,
- emptyMessage,
- modalTitle,
- chipColor,
- dataTestId,
- dataSyncKey,
- filterCategories,
-}: Props): JSX.Element | null => {
- const { computeStatusFor } = useDataSyncContext();
- const {
- isLoading,
- isMutationLoading,
- isMutationSuccess,
- isMutationError,
- categories,
- addCategory,
- deleteCategory,
- deleteCategoryByName,
- } = useItemCategories({ itemId, filterCategories });
-
- useEffect(
- () =>
- computeStatusFor(dataSyncKey, {
- isLoading: isMutationLoading,
- isSuccess: isMutationSuccess,
- isError: isMutationError,
- }),
- [
- isMutationLoading,
- isMutationSuccess,
- isMutationError,
- computeStatusFor,
- dataSyncKey,
- ],
- );
-
- const { isOpen, openModal, closeModal } = useModalStatus();
-
- return (
- <>
-
- }
- isOpen={isOpen}
- handleOnClose={closeModal}
- />
-
- >
- );
-};
-export default ItemCategoryContainer;
diff --git a/src/components/item/publish/ItemPublishTab.tsx b/src/components/item/publish/ItemPublishTab.tsx
index fb2b4a089..c8ad4a8ed 100644
--- a/src/components/item/publish/ItemPublishTab.tsx
+++ b/src/components/item/publish/ItemPublishTab.tsx
@@ -11,7 +11,6 @@ import {
DataSyncContextProvider,
useDataSyncContext,
} from '@/components/context/DataSyncContext';
-import CategoriesContainer from '@/components/item/publish/CategoriesContainer';
import CoEditorsContainer from '@/components/item/publish/CoEditorsContainer';
import EditItemDescription from '@/components/item/publish/EditItemDescription';
import { LanguageContainer } from '@/components/item/publish/LanguageContainer';
@@ -25,7 +24,7 @@ import { BUILDER } from '@/langs/constants';
import { SomeBreakPoints } from '@/types/breakpoint';
import EditItemName from './EditItemName';
-import CustomizedTags from './customizedTags/CustomizedTags';
+import { PublishCustomizedTags } from './customizedTags/PublishCustomizedTags';
import PublicationButtonSelector from './publicationButtons/PublicationButtonSelector';
type StackOrder = { order?: number | SomeBreakPoints };
@@ -64,25 +63,21 @@ const ItemPublishTab = (): JSX.Element => {
);
}
- const customizedTags = ;
-
const buildPreviewHeader = (): JSX.Element => (
- {!isMobile && customizedTags}
- {isMobile && customizedTags}
);
const buildPreviewContent = (): JSX.Element => (
-
+
diff --git a/src/components/item/publish/PublicationModal.tsx b/src/components/item/publish/PublicationModal.tsx
index a9de2e165..7526d8abc 100644
--- a/src/components/item/publish/PublicationModal.tsx
+++ b/src/components/item/publish/PublicationModal.tsx
@@ -28,7 +28,7 @@ export const PublicationModal = ({
const { t } = useBuilderTranslation();
return (
-