implementing api for multiple delete

This commit is contained in:
Martina 2020-09-17 20:40:10 -07:00
parent aaab6fcb2c
commit da5ccb8abc
10 changed files with 556 additions and 152 deletions

View File

@ -206,6 +206,13 @@ export const deleteBucketItem = async (data) => {
});
};
export const deleteBucketItems = async (data) => {
return await returnJSON(`/api/data/remove-multiple`, {
...DEFAULT_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const getSerializedSlate = async (data) => {
return await returnJSON(`/api/slates/get-serialized`, {
...DEFAULT_OPTIONS,

View File

@ -1,3 +1,6 @@
import React from "react";
import * as SVG from "~/common/svg";
export const values = {
version: "1.0.0",
sds: "0.1.0",
@ -79,3 +82,27 @@ export const theme = {
export const gateways = {
ipfs: "https://slate.textile.io/ipfs",
};
export function FileTypeIcon(props) {
if (props.type && props.type.startsWith("image/")) {
return <SVG.Image {...props} />;
}
if (props.type && props.type.startsWith("video/")) {
return <SVG.Video {...props} />;
}
if (props.type && props.type.startsWith("audio/")) {
return <SVG.Sound {...props} />;
}
if (props.type && props.type.startsWith("application/epub")) {
return <SVG.Book {...props} />;
}
if (props.type && props.type.startsWith("application/pdf")) {
return <SVG.Document {...props} />;
}
return <SVG.Document {...props} />;
}

View File

@ -593,6 +593,25 @@ export const Slate = (props) => (
</svg>
);
export const Trash = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
style={props.style}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 6H5H21" />
<path d="M8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M19 6V20C19 20.5304 18.7893 21.0391 18.4142 21.4142C18.0391 21.7893 17.5304 22 17 22H7C6.46957 22 5.96086 21.7893 5.58579 21.4142C5.21071 21.0391 5 20.5304 5 20V6H19Z" />
<path d="M10 11V17" />
<path d="M14 11V17" />
</svg>
);
export const Folder = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@ -11,29 +11,22 @@ import { PopoverNavigation } from "~/components/system/components/PopoverNavigat
import { LoaderSpinner } from "~/components/system/components/Loaders";
import { dispatchCustomEvent } from "~/common/custom-events";
import { generateLayout } from "~/components/core/Slate";
import { CheckBox } from "~/components/system/components/CheckBox";
import { Table } from "~/components/core/Table";
import SlateMediaObject from "~/components/core/SlateMediaObject";
import SlateMediaObjectPreview from "~/components/core/SlateMediaObjectPreview";
const COLUMNS_SCHEMA = [
{
key: "name",
name: <span style={{ fontSize: "0.9rem" }}>Name</span>,
width: "100%",
},
{
key: "size",
name: <span style={{ fontSize: "0.9rem" }}>Size</span>,
width: "104px",
},
{
key: "more",
name: <div />,
width: "64px",
},
];
const STYLES_CONTAINER_HOVER = css`
display: flex;
:hover {
color: ${Constants.system.brand};
}
`;
const STYLES_LINK = css`
display: inline;
cursor: pointer;
transition: 200ms ease all;
font-size: 0.9rem;
@ -41,10 +34,6 @@ const STYLES_LINK = css`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
:hover {
color: ${Constants.system.brand};
}
`;
const STYLES_VALUE = css`
@ -55,18 +44,8 @@ const STYLES_VALUE = css`
white-space: nowrap;
`;
const STYLES_TABLE_VALUE = {
fontFamily: Constants.font.medium,
padding: "0px 24px",
};
const STYLES_TABLE_CONTAINER = css`
border: 1px solid rgba(229, 229, 229, 0.75);
`;
const STYLES_ICON_BOX = css`
display: flex;
justify-content: flex-end;
display: inline-flex;
align-items: center;
padding: 8px;
cursor: pointer;
@ -85,6 +64,7 @@ const STYLES_COPY_INPUT = css`
const STYLES_IMAGE_GRID = css`
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
margin: 0 -27px;
`;
@ -109,6 +89,7 @@ export default class DataView extends React.Component {
state = {
menu: null,
checked: {},
loading: {},
};
@ -385,6 +366,13 @@ export default class DataView extends React.Component {
return null;
}
dispatchCustomEvent({
name: "create-alert",
detail: {
alert: { message: "File successfully deleted!", status: "INFO" },
},
});
await this.props.onRehydrate();
await this._handleUpdate();
this._handleLoading({ cid });
@ -419,16 +407,73 @@ export default class DataView extends React.Component {
);
}
const columns = COLUMNS_SCHEMA;
const columns = [
{
key: "checkbox",
name: <span />,
width: "24px",
},
{
key: "name",
name: <div style={{ fontSize: "0.9rem", padding: "18px 0" }}>Name</div>,
width: "100%",
},
{
key: "size",
name: <div style={{ fontSize: "0.9rem", padding: "18px 0" }}>Size</div>,
width: "104px",
},
{
key: "more",
name: <span />,
width: "48px",
},
];
const rows = this.props.items.map((each, index) => {
const cid = each.ipfs.replace("/ipfs/", "");
const isOnNetwork = each.networks && each.networks.includes("FILECOIN");
return {
...each,
checkbox: this.props.onCheckBox ? (
<div
style={{
margin: "12px 0",
opacity:
Object.keys(this.props.checked).length > 0
? 1
: this.state.hover === index
? 1
: 0,
}}
>
<CheckBox
name={`checkbox-${this.props.startIndex + index}`}
value={
!!this.props.checked[
`checkbox-${this.props.startIndex + index}`
]
}
onChange={this.props.onCheckBox}
boxStyle={{ height: 16, width: 16 }}
style={{ position: "relative", right: 3 }}
/>
</div>
) : (
<div />
),
name: (
<div css={STYLES_LINK} onClick={() => this._handleSelect(index)}>
{each.file || each.name}
<div
css={STYLES_CONTAINER_HOVER}
onClick={() => this._handleSelect(index)}
>
<div
css={STYLES_ICON_BOX}
style={{ paddingLeft: 0, paddingRight: 18 }}
>
<Constants.FileTypeIcon type={each.type} height="24px" />
</div>
<div css={STYLES_LINK}>{each.file || each.name}</div>
</div>
),
size: <div css={STYLES_VALUE}>{Strings.bytesToSize(each.size)}</div>,
@ -496,19 +541,13 @@ export default class DataView extends React.Component {
};
return (
<div css={STYLES_TABLE_CONTAINER}>
<System.Table
topRowStyle={{
...STYLES_TABLE_VALUE,
backgroundColor: Constants.system.foreground,
fontFamily: Constants.font.semiBold,
padding: "12px 24px",
}}
<React.Fragment>
<Table
data={data}
noColor
rowStyle={STYLES_TABLE_VALUE}
onAction={this.props.onAction}
onNavigateTo={this.props.onNavigateTo}
rowStyle={{ padding: "10px 16px" }}
topRowStyle={{ padding: "0px 16px" }}
onMouseEnter={(i) => this.setState({ hover: i })}
onMouseLeave={() => this.setState({ hover: null })}
/>
<input
ref={(c) => {
@ -518,7 +557,7 @@ export default class DataView extends React.Component {
value={this.state.copyValue}
css={STYLES_COPY_INPUT}
/>
</div>
</React.Fragment>
);
}
}

View File

@ -51,75 +51,21 @@ export default class SlateMediaObjectPreview extends React.Component {
? this.props.title.substring(0, this.props.charCap) + "..."
: this.props.title;
if (this.props.type && this.props.type.startsWith("image/")) {
return <img css={STYLES_IMAGE} style={this.props.imageStyle} src={url} />;
}
let element = (
<Constants.FileTypeIcon type={this.props.type} height="24px" />
);
return (
<article css={STYLES_ENTITY} style={this.props.style}>
<div>
<SVG.Document height="24px" />
</div>
<div>{element}</div>
{this.props.title && !this.props.small ? (
<div css={STYLES_TITLE}>{title}</div>
) : null}
</article>
);
if (this.props.type && this.props.type.startsWith("video/")) {
element = (
<article css={STYLES_ENTITY} style={this.props.style}>
<div>
<SVG.Video height="24px" />
</div>
{this.props.title && !this.props.small ? (
<div css={STYLES_TITLE}>{title}</div>
) : null}
</article>
);
}
if (this.props.type && this.props.type.startsWith("audio/")) {
element = (
<article css={STYLES_ENTITY} style={this.props.style}>
<div>
<SVG.Sound height="24px" />
</div>
{this.props.title && !this.props.small ? (
<div css={STYLES_TITLE}>{title}</div>
) : null}
</article>
);
}
if (this.props.type && this.props.type.startsWith("application/epub")) {
element = (
<article css={STYLES_ENTITY} style={this.props.style}>
<div>
<SVG.Book height="24px" />
</div>
{this.props.title && !this.props.small ? (
<div css={STYLES_TITLE}>{title}</div>
) : null}
</article>
);
}
if (this.props.type && this.props.type.startsWith("application/pdf")) {
element = (
<article css={STYLES_ENTITY} style={this.props.style}>
<div>
<SVG.Document height="24px" />
</div>
{this.props.title && !this.props.small ? (
<div css={STYLES_TITLE}>{title}</div>
) : null}
</article>
);
}
if (this.props.type && this.props.type.startsWith("image/")) {
element = (
<img css={STYLES_IMAGE} style={this.props.imageStyle} src={url} />
);
}
return element;
}
}

View File

@ -16,6 +16,7 @@ const STYLES_IMAGE_ROW = css`
flex-direction: row;
flex-wrap: wrap;
height: 160px;
justify-content: space-between;
overflow: hidden;
@media (max-width: ${Constants.sizes.mobile}px) {
@ -26,7 +27,7 @@ const STYLES_IMAGE_ROW = css`
const STYLES_ITEM_BOX = css`
width: 160px;
height: 160px;
margin: 0px 18px;
margin: 0px 12px;
display: flex;
align-items: center;
justify-content: center;

131
components/core/Table.js Normal file
View File

@ -0,0 +1,131 @@
import * as React from "react";
import * as Constants from "~/common/constants";
import * as Strings from "~/common/strings";
import * as SubSystem from "~/components/system/components/fragments/TableComponents";
import { css } from "@emotion/react";
import { P } from "~/components/system/components/Typography";
import * as SVG from "~/common/svg";
const TABLE_COLUMN_WIDTH_DEFAULTS = {
1: "100%",
2: "50%",
3: "33.333%",
4: "25%",
5: "20%",
6: "16.666%",
7: "14.28%",
8: "12.5%",
};
const STYLES_CONTAINER = css`
border: 1px solid rgba(229, 229, 229, 0.75);
`;
const STYLES_TABLE_ROW = css`
position: relative;
box-sizing: border-box;
border-bottom: 1px solid rgba(229, 229, 229, 0.75);
display: flex;
align-items: center;
width: 100%;
transition: 200ms ease all;
:last-child {
border: 0;
}
`;
const STYLES_TABLE_TOP_ROW = css`
box-sizing: border-box;
font-family: ${Constants.font.semiBold};
border-bottom: 1px solid rgba(229, 229, 229, 0.75);
display: flex;
width: 100%;
align-items: center;
background-color: ${Constants.system.foreground};
`;
export class Table extends React.Component {
render() {
const { data } = this.props;
const ac = {};
if (!data || !data.rows || data.rows.length === 0) {
return <P style={{ padding: 24 }}>No data.</P>;
}
for (let x = 0; x < data.columns.length; x++) {
ac[data.columns[x].key] = {
...data.columns[x],
index: x,
};
}
const width = TABLE_COLUMN_WIDTH_DEFAULTS[data.columns.length];
return (
<div css={STYLES_CONTAINER} onMouseLeave={this.props.onMouseLeave}>
{this.props.noLabel ? null : (
<div css={STYLES_TABLE_TOP_ROW} style={this.props.topRowStyle}>
{data.columns.map((c, cIndex) => {
let localWidth = c.width ? c.width : width;
let flexShrink = c.width && c.width !== "100%" ? "0" : null;
if (cIndex === 0 && !c.width) {
localWidth = "100%";
}
return (
<div
key={`table-top-${c.key}-${cIndex}`}
style={{
width: localWidth,
flexShrink,
}}
>
{c.name || ""}
</div>
);
})}
</div>
)}
{data.rows.map((r, i) => {
return (
<div
key={`${r.id}-${i}`}
css={STYLES_TABLE_ROW}
style={this.props.rowStyle}
onMouseEnter={() => this.props.onMouseEnter(i)}
>
{Object.keys(ac).map((each, cIndex) => {
const field = ac[each];
const content = r[each];
let localWidth = field.width ? field.width : width;
let flexShrink =
field.width && field.width !== "100%" ? "0" : null;
if (cIndex === 0 && !field.width) {
localWidth = "100%";
}
return (
<div
key={`${each}-${i}-${cIndex}`}
style={{
width: localWidth,
flexShrink,
...this.props.contentStyle,
}}
>
{content}
</div>
);
})}
</div>
);
})}
</div>
);
}
}

View File

@ -25,8 +25,8 @@ const STYLES_CHECKBOX_FIGURE = css`
justify-content: center;
pointer-events: none;
flex-shrink: 0;
height: 32px;
width: 32px;
height: 24px;
width: 24px;
margin: 0;
padding: 0;
`;
@ -74,7 +74,7 @@ export class CheckBox extends React.Component {
render() {
return (
<label css={STYLES_CHECKBOX} style={this.props.style}>
<figure css={STYLES_CHECKBOX_FIGURE}>
<figure css={STYLES_CHECKBOX_FIGURE} style={this.props.boxStyle}>
{this.props.value ? <SVG.CheckBox height="20px" /> : null}
</figure>
<input

View File

@ -0,0 +1,142 @@
import * as Data from "~/node_common/data";
import * as Utilities from "~/node_common/utilities";
import * as Strings from "~/common/strings";
import { read } from "fs";
const generateLayout = (items) => {
if (!items) {
return [];
}
if (!items.length) {
return [];
}
return items.map((item, i) => {
var y = Math.ceil(Math.random() * 4) + 1;
return {
x: (i * 2) % 10,
y: 0,
w: 2,
h: 2,
minW: 2,
minH: 2,
// NOTE(jim): Library quirk thats required.
i: i.toString(),
};
});
};
export default async (req, res) => {
if (!req.body.data || !req.body.data.cids || !req.body.data.cids.length) {
return res
.status(500)
.send({ decorator: "SERVER_REMOVE_DATA_NO_CID", error: true });
}
const id = Utilities.getIdFromCookie(req);
if (!id) {
return res
.status(403)
.send({ decorator: "SERVER_REMOVE_DATA_NOT_ALLOWED", error: true });
}
const user = await Data.getUserById({
id,
});
const {
buckets,
bucketKey,
bucketName,
} = await Utilities.getBucketAPIFromUserToken(user.data.tokens.api);
const r = await buckets.list();
const items = await buckets.listIpfsPath(r[0].path);
let entities = [];
for (let i = 0; i < items.itemsList.length; i++) {
if (req.body.data.cids.includes(items.itemsList[i].cid)) {
entities.push(items.itemsList[i]);
if (entities.length === items.itemsList.length) break;
}
}
if (!entities.length) {
return res
.status(500)
.send({ decorator: "SERVER_REMOVE_DATA_NO_CID", error: true });
}
// remove from your bucket
for (let entity of entities) {
try {
// NOTE(jim):
// We use name instead of path because the second argument is for
// a subpath, not the full path.
bucketRemoval = await buckets.removePath(bucketKey, entity.name);
} catch (e) {
console.log(e);
continue;
}
}
// NOTE(jim):
// Goes through all of your slates and removes all data references.
const slates = await Data.getSlatesByUserId({ userId: id });
for (let i = 0; i < slates.length; i++) {
let slate = slates[i];
let removal = false;
let objects = slate.data.objects.filter((o) => {
for (let cid of req.body.data.cids) {
if (o.url.includes(cid)) {
removal = true;
return false;
}
}
return true;
});
if (removal) {
let layouts = await Data.updateSlateById({
id: slate.id,
updated_at: new Date(),
data: {
...slate.data,
objects,
layouts: { lg: generateLayout(objects) },
},
});
}
}
// NOTE(jim):
// Removes the file reference from your library
const response = await Data.updateUserById({
id: user.id,
data: {
...user.data,
library: [
{
...user.data.library[0],
children: user.data.library[0].children.filter((o) => {
for (let cid of req.body.data.cids) {
if (o.ipfs.includes(cid)) {
return false;
}
}
return true;
}),
},
],
},
});
return res.status(200).send({
decorator: "SERVER_REMOVE_DATA",
success: true,
bucketItems: items.itemsList,
});
};

View File

@ -6,7 +6,11 @@ import * as Constants from "~/common/constants";
import { css } from "@emotion/react";
import { TabGroup } from "~/components/core/TabGroup";
import { ButtonPrimary } from "~/components/system/components/Buttons";
import {
ButtonPrimary,
ButtonWarning,
} from "~/components/system/components/Buttons";
import { dispatchCustomEvent } from "~/common/custom-events";
import ScenePage from "~/components/core/ScenePage";
import DataView from "~/components/core/DataView";
@ -33,8 +37,18 @@ const STYLES_HEADER_LINE = css`
margin-bottom: 42px;
`;
const STYLES_ARROWS = css`
text-align: end;
const STYLES_ACTION_ROW = css`
display: flex;
align-items: center;
`;
const STYLES_LEFT = css`
width: 100%;
min-width: 10%;
`;
const STYLES_RIGHT = css`
flex-shrink: 0;
`;
const STYLES_ICON_ELEMENT = css`
@ -73,6 +87,7 @@ export default class SceneFilesFolder extends React.Component {
state = {
view: "grid",
startIndex: 0,
checked: {},
};
loop = async () => {
@ -134,6 +149,62 @@ export default class SceneFilesFolder extends React.Component {
}
};
_handleCheckBox = (e) => {
let checked = this.state.checked;
if (e.target.value === false) {
delete checked[e.target.name];
this.setState({ checked });
return;
}
this.setState({
checked: { ...this.state.checked, [e.target.name]: true },
});
};
_handleDeleteFiles = async (e) => {
const message = `Are you sure you want to delete these ${
Object.keys(this.state.checked).length
} files? They will be deleted from your slates as well`;
if (!window.confirm(message)) {
return;
}
let cids = Object.keys(this.state.checked).map((id) => {
let index = parseInt(id.replace("checkbox-", ""));
return this.props.viewer.library[0].children[index].ipfs.replace(
"/ipfs/",
""
);
});
console.log(cids);
const response = await Actions.deleteBucketItems({ cids });
if (!response) {
dispatchCustomEvent({
name: "create-alert",
detail: {
alert: {
message:
"We're having trouble connecting right now. Please try again later",
},
},
});
return null;
}
if (response.error) {
dispatchCustomEvent({
name: "create-alert",
detail: { alert: { decorator: response.decorator } },
});
return null;
}
dispatchCustomEvent({
name: "create-alert",
detail: {
alert: { message: "Files successfully deleted!", status: "INFO" },
},
});
};
render() {
return (
<ScenePage>
@ -166,13 +237,13 @@ export default class SceneFilesFolder extends React.Component {
<div
css={STYLES_ICON_BOX}
onClick={() => {
this.setState({ view: "list" });
this.setState({ view: "grid" });
}}
>
<SVG.ListView
<SVG.GridView
style={{
color:
this.state.view === "list"
this.state.view === "grid"
? Constants.system.black
: "rgba(0,0,0,0.25)",
}}
@ -182,13 +253,13 @@ export default class SceneFilesFolder extends React.Component {
<div
css={STYLES_ICON_BOX}
onClick={() => {
this.setState({ view: "grid" });
this.setState({ view: "list" });
}}
>
<SVG.GridView
<SVG.ListView
style={{
color:
this.state.view === "grid"
this.state.view === "list"
? Constants.system.black
: "rgba(0,0,0,0.25)",
}}
@ -204,36 +275,57 @@ export default class SceneFilesFolder extends React.Component {
this.state.startIndex,
this.state.startIndex + VIEW_LIMIT
)}
onAction={this.props.onAction}
startIndex={this.state.startIndex}
checked={this.state.checked}
onCheckBox={this._handleCheckBox}
onRehydrate={this.props.onRehydrate}
/>
<div css={STYLES_ARROWS}>
<span
css={STYLES_ICON_ELEMENT}
style={
this.state.startIndex - VIEW_LIMIT >= 0
? null
: { cursor: "not-allowed", color: Constants.system.border }
}
onClick={() => this._increment(-1)}
>
<SVG.NavigationArrow
height="24px"
style={{ transform: `rotate(180deg)` }}
/>
</span>
<span
css={STYLES_ICON_ELEMENT}
style={
this.state.startIndex + VIEW_LIMIT <
this.props.viewer.library[0].children.length
? null
: { cursor: "not-allowed", color: Constants.system.border }
}
onClick={() => this._increment(1)}
>
<SVG.NavigationArrow height="24px" />
</span>
<div css={STYLES_ACTION_ROW}>
<div css={STYLES_LEFT}>
{Object.keys(this.state.checked).length ? (
<ButtonWarning
style={{ width: 160 }}
onClick={this._handleDeleteFiles}
>
Delete {Object.keys(this.state.checked).length} file
{Object.keys(this.state.checked).length > 1 ? "s" : ""}
</ButtonWarning>
) : null}
</div>
<div css={STYLES_RIGHT}>
<span
css={STYLES_ICON_ELEMENT}
style={
this.state.startIndex - VIEW_LIMIT >= 0
? null
: {
cursor: "not-allowed",
color: Constants.system.border,
}
}
onClick={() => this._increment(-1)}
>
<SVG.NavigationArrow
height="24px"
style={{ transform: `rotate(180deg)` }}
/>
</span>
<span
css={STYLES_ICON_ELEMENT}
style={
this.state.startIndex + VIEW_LIMIT <
this.props.viewer.library[0].children.length
? null
: {
cursor: "not-allowed",
color: Constants.system.border,
}
}
onClick={() => this._increment(1)}
>
<SVG.NavigationArrow height="24px" />
</span>
</div>
</div>
</React.Fragment>
) : (