Skip to content

Commit

Permalink
Page builder blocks v2 UI list page blocks (#2496)
Browse files Browse the repository at this point in the history
* feat: add Page Blocks page to Page Builder UI app

* fix: fix code style issues

* feat: add empty state to Page Blocks list

* fix: fix code style comments

* fix: change label value

* feat: delete/edit Page Block functinality

* fix: fix i18n namespace names for Page Block feature pages
  • Loading branch information
neatbyte-vnobis authored Jul 15, 2022
1 parent cf75eb3 commit 01aefbf
Show file tree
Hide file tree
Showing 11 changed files with 778 additions and 7 deletions.
5 changes: 5 additions & 0 deletions packages/app-page-builder/src/PageBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ const PageBuilderMenu: React.FC = () => {
label={"Categories"}
path="/page-builder/block-categories"
/>
<Menu
name="pageBuilder.blocks.pageBlocks"
label={"Blocks"}
path="/page-builder/page-blocks"
/>
</HasPermission>
</Menu>
</Menu>
Expand Down
19 changes: 19 additions & 0 deletions packages/app-page-builder/src/admin/plugins/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Menus from "../views/Menus/Menus";
import Pages from "../views/Pages/Pages";
import Editor from "../views/Pages/Editor";
import BlockCategories from "../views/BlockCategories/BlockCategories";
import PageBlocks from "../views/PageBlocks/PageBlocks";

const ROLE_PB_CATEGORY = "pb.category";
const ROLE_PB_MENUS = "pb.menu";
Expand Down Expand Up @@ -111,6 +112,24 @@ const plugins: RoutePlugin[] = [
)}
/>
)
},
{
name: "route-pb-page-blocks",
type: "route",
route: (
<Route
exact
path="/page-builder/page-blocks"
render={() => (
<SecureRoute permission={ROLE_PB_BLOCK}>
<AdminLayout>
<Helmet title={"Blocks"} />
<PageBlocks />
</AdminLayout>
</SecureRoute>
)}
/>
)
}
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { ReactComponent as AddIcon } from "@webiny/app-admin/assets/icons/add-18
import { ReactComponent as FilterIcon } from "@webiny/app-admin/assets/icons/filter-24px.svg";
import { PageBuilderSecurityPermission, PbBlockCategory } from "~/types";

const t = i18n.ns("app-page-builder/admin/categories/data-list");
const t = i18n.ns("app-page-builder/admin/block-categories/data-list");

interface CreatableItem {
createdBy?: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import isEmpty from "lodash/isEmpty";
import EmptyView from "@webiny/app-admin/components/EmptyView";
import { ReactComponent as AddIcon } from "@webiny/app-admin/assets/icons/add-18px.svg";

const t = i18n.ns("app-page-builder/admin/categories/form");
const t = i18n.ns("app-page-builder/admin/block-categories/form");

const ButtonWrapper = styled("div")({
display: "flex",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import gql from "graphql-tag";
import { PbBlockCategory, PbErrorResponse } from "~/types";

const BASE_FIELDS = `
export const PAGE_BLOCK_CATEGORY_BASE_FIELDS = `
slug
name
createdOn
Expand All @@ -16,7 +16,7 @@ export const LIST_BLOCK_CATEGORIES = gql`
pageBuilder {
listBlockCategories {
data {
${BASE_FIELDS}
${PAGE_BLOCK_CATEGORY_BASE_FIELDS}
}
error {
data
Expand Down Expand Up @@ -47,7 +47,7 @@ export const GET_BLOCK_CATEGORY = gql`
pageBuilder {
getBlockCategory(slug: $slug){
data {
${BASE_FIELDS}
${PAGE_BLOCK_CATEGORY_BASE_FIELDS}
}
error {
code
Expand Down Expand Up @@ -81,7 +81,7 @@ export const CREATE_BLOCK_CATEGORY = gql`
pageBuilder {
blockCategory: createBlockCategory(data: $data) {
data {
${BASE_FIELDS}
${PAGE_BLOCK_CATEGORY_BASE_FIELDS}
}
error {
code
Expand Down Expand Up @@ -117,7 +117,7 @@ export const UPDATE_BLOCK_CATEGORY = gql`
pageBuilder {
blockCategory: updateBlockCategory(slug: $slug, data: $data) {
data {
${BASE_FIELDS}
${PAGE_BLOCK_CATEGORY_BASE_FIELDS}
}
error {
code
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import React, { useCallback, useMemo, useState } from "react";
import { i18n } from "@webiny/app/i18n";
import { useRouter } from "@webiny/react-router";
import { useQuery } from "@apollo/react-hooks";
import orderBy from "lodash/orderBy";

import {
DataList,
DataListModalOverlay,
DataListModalOverlayAction,
ScrollList,
ListItem,
ListItemText,
ListItemTextSecondary
} from "@webiny/ui/List";
import { Cell, Grid } from "@webiny/ui/Grid";
import { Select } from "@webiny/ui/Select";
import SearchUI from "@webiny/app-admin/components/SearchUI";
import { ReactComponent as FilterIcon } from "@webiny/app-admin/assets/icons/filter-24px.svg";

import { PbBlockCategory, PbPageBlock } from "~/types";
import { LIST_PAGE_BLOCKS_AND_CATEGORIES } from "./graphql";

const t = i18n.ns("app-page-builder/admin/page-blocks/by-categories-data-list");

interface Sorter {
label: string;
sort: string;
}
const SORTERS: Sorter[] = [
{
label: t`Newest to oldest`,
sort: "createdOn_DESC"
},
{
label: t`Oldest to newest`,
sort: "createdOn_ASC"
},
{
label: t`Name A-Z`,
sort: "name_ASC"
},
{
label: t`Name Z-A`,
sort: "name_DESC"
}
];

const BlocksByCategoriesDataList = () => {
const [filter, setFilter] = useState<string>("");
const [sort, setSort] = useState<string>(SORTERS[0].sort);
const { history } = useRouter();
const listQuery = useQuery(LIST_PAGE_BLOCKS_AND_CATEGORIES);

const blockCategoriesData: PbBlockCategory[] =
listQuery?.data?.pageBuilder?.listBlockCategories?.data || [];
const pageBlocksData: PbPageBlock[] = listQuery?.data?.pageBuilder?.listPageBlocks?.data || [];

const filterData = useCallback(
({ slug, name }) => {
return slug.toLowerCase().includes(filter) || name.toLowerCase().includes(filter);
},
[filter]
);

const sortData = useCallback(
categories => {
if (!sort) {
return categories;
}
const [field, order] = sort.split("_");
return orderBy(categories, field, order.toLowerCase() as "asc" | "desc");
},
[sort]
);

const selectedBlocksCategory = new URLSearchParams(location.search).get("category");
const loading = [listQuery].find(item => item.loading);

const blockCategoriesDataListModalOverlay = useMemo(
() => (
<DataListModalOverlay>
<Grid>
<Cell span={12}>
<Select
value={sort}
onChange={setSort}
label={t`Sort by`}
description={"Sort block categories by"}
>
{SORTERS.map(({ label, sort: value }) => {
return (
<option key={label} value={value}>
{label}
</option>
);
})}
</Select>
</Cell>
</Grid>
</DataListModalOverlay>
),
[sort]
);

const filteredBlockCategoriesData: PbBlockCategory[] =
filter === "" ? blockCategoriesData : blockCategoriesData.filter(filterData);
const categoryList: PbBlockCategory[] = sortData(filteredBlockCategoriesData);

return (
<DataList
title={t`Blocks`}
loading={Boolean(loading)}
data={categoryList}
search={
<SearchUI
value={filter}
onChange={setFilter}
inputPlaceholder={t`Search categories`}
/>
}
modalOverlay={blockCategoriesDataListModalOverlay}
modalOverlayAction={
<DataListModalOverlayAction
icon={<FilterIcon />}
data-testid={"default-data-list.filter"}
/>
}
refresh={() => {
if (!listQuery.refetch) {
return;
}
listQuery.refetch();
}}
>
{({ data }: { data: PbBlockCategory[] }) => (
<ScrollList data-testid="default-data-list">
{data.map(item => {
const numberOfBlocks = pageBlocksData.filter(
pageBlock => pageBlock.blockCategory === item.slug
).length;
return (
<ListItem
key={item.slug}
selected={item.slug === selectedBlocksCategory}
>
<ListItemText
onClick={() =>
history.push(
`/page-builder/page-blocks?category=${item.slug}`
)
}
>
{item.name}
<ListItemTextSecondary>{`${numberOfBlocks} ${
numberOfBlocks === 1 ? "block" : "blocks"
} in the category`}</ListItemTextSecondary>
</ListItemText>
</ListItem>
);
})}
</ScrollList>
)}
</DataList>
);
};

export default BlocksByCategoriesDataList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useMemo, useCallback } from "react";
import { SplitView, LeftPanel, RightPanel } from "@webiny/app-admin/components/SplitView";
import { useSecurity } from "@webiny/app-security";

import { PageBuilderSecurityPermission } from "~/types";
import BlocksByCategoriesDataList from "./BlocksByCategoriesDataList";
import PageBlocksDataList from "./PageBlocksDataList";

export interface CreatableItem {
createdBy?: {
id?: string;
};
}

const PageBlocks: React.FC = () => {
const { identity, getPermission } = useSecurity();
const pbPageBlockPermission = useMemo((): PageBuilderSecurityPermission | null => {
return getPermission("pb.block");
}, [identity]);

const canEdit = useCallback((item: CreatableItem): boolean => {
if (!pbPageBlockPermission) {
return false;
}
if (pbPageBlockPermission.own) {
const identityId = identity ? identity.id || identity.login : null;
return item.createdBy?.id === identityId;
}
if (typeof pbPageBlockPermission.rwd === "string") {
return pbPageBlockPermission.rwd.includes("w");
}

return true;
}, []);

const canDelete = useCallback((item: CreatableItem): boolean => {
if (!pbPageBlockPermission) {
return false;
}
if (pbPageBlockPermission.own) {
const identityId = identity ? identity.id || identity.login : null;
return item.createdBy?.id === identityId;
}
if (typeof pbPageBlockPermission.rwd === "string") {
return pbPageBlockPermission.rwd.includes("d");
}

return true;
}, []);

return (
<SplitView>
<LeftPanel>
<BlocksByCategoriesDataList />
</LeftPanel>
<RightPanel>
<PageBlocksDataList canEdit={canEdit} canDelete={canDelete} />
</RightPanel>
</SplitView>
);
};

export default PageBlocks;
Loading

0 comments on commit 01aefbf

Please sign in to comment.