Filepicker component and logo upload for org (#250)
This commit is contained in:
parent
5d820c7203
commit
8afa900044
|
|
@ -1,5 +1,5 @@
|
||||||
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||||
import { SaveOrgRequest } from "api/OrgApi";
|
import { SaveOrgLogo, SaveOrgRequest } from "api/OrgApi";
|
||||||
|
|
||||||
export const fetchOrg = (orgId: string) => {
|
export const fetchOrg = (orgId: string) => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -66,3 +66,19 @@ export const saveOrg = (orgSettings: SaveOrgRequest) => {
|
||||||
payload: orgSettings,
|
payload: orgSettings,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const uploadOrgLogo = (orgLogo: SaveOrgLogo) => {
|
||||||
|
return {
|
||||||
|
type: ReduxActionTypes.UPLOAD_ORG_LOGO,
|
||||||
|
payload: orgLogo,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteOrgLogo = (id: string) => {
|
||||||
|
return {
|
||||||
|
type: ReduxActionTypes.REMOVE_ORG_LOGO,
|
||||||
|
payload: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,12 @@ export interface SaveOrgRequest {
|
||||||
email?: string;
|
email?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SaveOrgLogo {
|
||||||
|
id: string;
|
||||||
|
logo: File;
|
||||||
|
progress: (progressEvent: ProgressEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateOrgRequest {
|
export interface CreateOrgRequest {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
@ -100,5 +106,26 @@ class OrgApi extends Api {
|
||||||
roleName: null,
|
roleName: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
static saveOrgLogo(request: SaveOrgLogo): AxiosPromise<ApiResponse> {
|
||||||
|
const formData = new FormData();
|
||||||
|
if (request.logo) {
|
||||||
|
formData.append("file", request.logo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Api.post(
|
||||||
|
OrgApi.orgsURL + "/" + request.id + "/logo",
|
||||||
|
formData,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
onUploadProgress: request.progress,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
static deleteOrgLogo(request: { id: string }): AxiosPromise<ApiResponse> {
|
||||||
|
return Api.delete(OrgApi.orgsURL + "/" + request.id + "/logo");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export default OrgApi;
|
export default OrgApi;
|
||||||
|
|
|
||||||
3
app/client/src/assets/icons/ads/upload.svg
Normal file
3
app/client/src/assets/icons/ads/upload.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="42" height="29" viewBox="0 0 42 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.3636 29H28.6364H23.8636V21.2667H27.6818L21 13.5333L14.3182 21.2667H18.1364V29H13.3636H7.63636C3.41918 29 0 25.5374 0 21.2667C0 17.6552 2.457 14.645 5.76164 13.7963C6.12818 6.11707 12.3728 0 20.0455 0C27.6322 0 33.8253 5.9798 34.3159 13.5391C38.598 13.5082 42 17.0191 42 21.2667C42 25.5374 38.5808 29 34.3636 29Z" fill="#9F9F9F"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 486 B |
353
app/client/src/components/ads/FilePicker.tsx
Normal file
353
app/client/src/components/ads/FilePicker.tsx
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import Button, { Category, Size } from "./Button";
|
||||||
|
import axios from "axios";
|
||||||
|
import { ReactComponent as UploadIcon } from "../../assets/icons/ads/upload.svg";
|
||||||
|
import { DndProvider, useDrop, DropTargetMonitor } from "react-dnd";
|
||||||
|
import HTML5Backend, { NativeTypes } from "react-dnd-html5-backend";
|
||||||
|
import Text, { TextType } from "./Text";
|
||||||
|
import { Classes, Variant } from "./common";
|
||||||
|
import { Toaster } from "./Toast";
|
||||||
|
|
||||||
|
const CLOUDINARY_PRESETS_NAME = "";
|
||||||
|
const CLOUDINARY_CLOUD_NAME = "";
|
||||||
|
|
||||||
|
type FilePickerProps = {
|
||||||
|
onFileUploaded?: (fileUrl: string) => void;
|
||||||
|
onFileRemoved?: () => void;
|
||||||
|
fileUploader?: FileUploader;
|
||||||
|
url?: string;
|
||||||
|
logoUploadError?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContainerDiv = styled.div<{
|
||||||
|
isUploaded: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
canDrop: boolean;
|
||||||
|
}>`
|
||||||
|
width: 320px;
|
||||||
|
height: 190px;
|
||||||
|
background-color: ${props => props.theme.colors.filePicker.bg};
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
#fileInput {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-drop-text {
|
||||||
|
margin: ${props => props.theme.spaces[6]}px 0
|
||||||
|
${props => props.theme.spaces[6]}px 0;
|
||||||
|
color: ${props => props.theme.colors.filePicker.color};
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-description {
|
||||||
|
width: 95%;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: ${props => props.theme.spaces[6] + 1}px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-spec {
|
||||||
|
margin-bottom: ${props => props.theme.spaces[2]}px;
|
||||||
|
span {
|
||||||
|
margin-right: ${props => props.theme.spaces[4]}px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
width: 100%;
|
||||||
|
background: ${props => props.theme.colors.filePicker.progress};
|
||||||
|
transition: height 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-inner {
|
||||||
|
background-color: ${props => props.theme.colors.success.light};
|
||||||
|
transition: width 0.4s ease;
|
||||||
|
height: ${props => props.theme.spaces[1]}px;
|
||||||
|
border-radius: ${props => props.theme.spaces[1] - 1}px;
|
||||||
|
width: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-button {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
${props => props.theme.colors.filePicker.shadow.from},
|
||||||
|
${props => props.theme.colors.filePicker.shadow.to}
|
||||||
|
);
|
||||||
|
opacity: 0.6;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
a {
|
||||||
|
width: 110px;
|
||||||
|
margin: ${props => props.theme.spaces[13]}px
|
||||||
|
${props => props.theme.spaces[3]}px ${props => props.theme.spaces[3]}px
|
||||||
|
auto;
|
||||||
|
.${Classes.ICON} {
|
||||||
|
margin-right: ${props => props.theme.spaces[2] - 1}px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.remove-button {
|
||||||
|
display: ${props => (props.isUploaded ? "block" : "none")};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type SetProgress = (percentage: number) => void;
|
||||||
|
export type UploadCallback = (url: string) => void;
|
||||||
|
export type FileUploader = (
|
||||||
|
file: any,
|
||||||
|
setProgress: SetProgress,
|
||||||
|
onUpload: UploadCallback,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export function CloudinaryUploader(
|
||||||
|
file: any,
|
||||||
|
setProgress: SetProgress,
|
||||||
|
onUpload: UploadCallback,
|
||||||
|
) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("upload_preset", CLOUDINARY_PRESETS_NAME);
|
||||||
|
if (file) {
|
||||||
|
formData.append("file", file);
|
||||||
|
}
|
||||||
|
axios
|
||||||
|
.post(
|
||||||
|
`https://api.cloudinary.com/v1_1/${CLOUDINARY_CLOUD_NAME}/image/upload`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
onUploadProgress: function(progressEvent: ProgressEvent) {
|
||||||
|
const uploadPercentage = Math.round(
|
||||||
|
(progressEvent.loaded / progressEvent.total) * 100,
|
||||||
|
);
|
||||||
|
setProgress(uploadPercentage);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(data => {
|
||||||
|
onUpload(data.data.url);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("error in file uploading", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilePickerComponent = (props: FilePickerProps) => {
|
||||||
|
const { logoUploadError } = props;
|
||||||
|
const [fileInfo, setFileInfo] = useState<{ name: string; size: number }>({
|
||||||
|
name: "",
|
||||||
|
size: 0,
|
||||||
|
});
|
||||||
|
const [isUploaded, setIsUploaded] = useState<boolean>(false);
|
||||||
|
const [fileUrl, setFileUrl] = useState("");
|
||||||
|
|
||||||
|
const [{ canDrop, isOver }, drop] = useDrop({
|
||||||
|
accept: [NativeTypes.FILE],
|
||||||
|
drop(item, monitor) {
|
||||||
|
onDrop(monitor);
|
||||||
|
},
|
||||||
|
collect: monitor => ({
|
||||||
|
isOver: monitor.isOver(),
|
||||||
|
canDrop: monitor.canDrop(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const bgRef = useRef<HTMLDivElement>(null);
|
||||||
|
const progressRef = useRef<HTMLDivElement>(null);
|
||||||
|
const fileDescRef = useRef<HTMLDivElement>(null);
|
||||||
|
const fileContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
function ButtonClick(event: React.MouseEvent<HTMLElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(monitor: DropTargetMonitor) {
|
||||||
|
if (monitor) {
|
||||||
|
const files = monitor.getItem().files;
|
||||||
|
if (files) {
|
||||||
|
handleFileUpload(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProgress(uploadPercentage: number) {
|
||||||
|
if (progressRef.current) {
|
||||||
|
progressRef.current.style.width = `${uploadPercentage}%`;
|
||||||
|
}
|
||||||
|
if (uploadPercentage === 100) {
|
||||||
|
setIsUploaded(true);
|
||||||
|
if (fileDescRef.current && bgRef.current) {
|
||||||
|
fileDescRef.current.style.display = "none";
|
||||||
|
bgRef.current.style.opacity = "1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpload(url: string) {
|
||||||
|
props.onFileUploaded && props.onFileUploaded(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileUpload(files: FileList | null) {
|
||||||
|
const file = files && files[0];
|
||||||
|
let fileSize = 0;
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
fileSize = Math.floor(file.size / 1024);
|
||||||
|
setFileInfo({ name: file.name, size: fileSize });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileSize < 250) {
|
||||||
|
if (bgRef.current) {
|
||||||
|
bgRef.current.style.backgroundImage = `url(${URL.createObjectURL(
|
||||||
|
file,
|
||||||
|
)})`;
|
||||||
|
bgRef.current.style.opacity = "0.5";
|
||||||
|
}
|
||||||
|
if (fileDescRef.current) {
|
||||||
|
fileDescRef.current.style.display = "block";
|
||||||
|
}
|
||||||
|
if (fileContainerRef.current) {
|
||||||
|
fileContainerRef.current.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* set form data and send api request */
|
||||||
|
props.fileUploader && props.fileUploader(file, setProgress, onUpload);
|
||||||
|
} else {
|
||||||
|
Toaster.show({
|
||||||
|
text: "File size should be less than 250kb!",
|
||||||
|
variant: Variant.warning,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFile() {
|
||||||
|
if (fileContainerRef.current && bgRef.current) {
|
||||||
|
setFileUrl("");
|
||||||
|
fileContainerRef.current.style.display = "flex";
|
||||||
|
bgRef.current.style.backgroundImage = "url('')";
|
||||||
|
setIsUploaded(false);
|
||||||
|
props.onFileRemoved && props.onFileRemoved();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActive = canDrop && isOver;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.url) {
|
||||||
|
const urlKeys = props.url.split("/");
|
||||||
|
if (urlKeys[urlKeys.length - 1] !== "null") {
|
||||||
|
setFileUrl(props.url);
|
||||||
|
} else {
|
||||||
|
setFileUrl("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [props.url]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fileUrl && !isUploaded) {
|
||||||
|
setIsUploaded(true);
|
||||||
|
if (bgRef.current) {
|
||||||
|
bgRef.current.style.backgroundImage = `url(${fileUrl})`;
|
||||||
|
bgRef.current.style.opacity = "1";
|
||||||
|
}
|
||||||
|
if (fileDescRef.current) {
|
||||||
|
fileDescRef.current.style.display = "none";
|
||||||
|
}
|
||||||
|
if (fileContainerRef.current) {
|
||||||
|
fileContainerRef.current.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [fileUrl, logoUploadError]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContainerDiv
|
||||||
|
isActive={isActive}
|
||||||
|
canDrop={canDrop}
|
||||||
|
isUploaded={isUploaded}
|
||||||
|
ref={drop}
|
||||||
|
>
|
||||||
|
<div ref={bgRef} className="bg-image">
|
||||||
|
<div className="button-wrapper" ref={fileContainerRef}>
|
||||||
|
<UploadIcon />
|
||||||
|
<Text type={TextType.P2} className="drag-drop-text">
|
||||||
|
Drag & Drop files to upload or
|
||||||
|
</Text>
|
||||||
|
<form>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="fileInput"
|
||||||
|
multiple={false}
|
||||||
|
ref={inputRef}
|
||||||
|
accept=".jpeg,.png,.svg"
|
||||||
|
value={""}
|
||||||
|
onChange={el => handleFileUpload(el.target.files)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
text="Browse"
|
||||||
|
category={Category.tertiary}
|
||||||
|
size={Size.medium}
|
||||||
|
onClick={el => ButtonClick(el)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="file-description" ref={fileDescRef} id="fileDesc">
|
||||||
|
<div className="file-spec">
|
||||||
|
<Text type={TextType.H6}>{fileInfo.name}</Text>
|
||||||
|
<Text type={TextType.H6}>{fileInfo.size}KB</Text>
|
||||||
|
</div>
|
||||||
|
<div className="progress-container">
|
||||||
|
<div className="progress-inner" ref={progressRef}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="remove-button">
|
||||||
|
<Button
|
||||||
|
text="remove"
|
||||||
|
icon="delete"
|
||||||
|
size={Size.medium}
|
||||||
|
category={Category.tertiary}
|
||||||
|
onClick={el => removeFile()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ContainerDiv>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FilePicker = (props: FilePickerProps) => {
|
||||||
|
return (
|
||||||
|
<DndProvider backend={HTML5Backend}>
|
||||||
|
<FilePickerComponent {...props} />
|
||||||
|
</DndProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilePicker;
|
||||||
|
|
@ -81,7 +81,7 @@ const boxStyles = (
|
||||||
const StyledInput = styled.input<
|
const StyledInput = styled.input<
|
||||||
TextInputProps & { inputStyle: boxReturnType; isValid: boolean }
|
TextInputProps & { inputStyle: boxReturnType; isValid: boolean }
|
||||||
>`
|
>`
|
||||||
width: ${props => (props.fill ? "100%" : "260px")};
|
width: ${props => (props.fill ? "100%" : "320px")};
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
|
||||||
18
app/client/src/components/stories/FilePicker.stories.tsx
Normal file
18
app/client/src/components/stories/FilePicker.stories.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from "react";
|
||||||
|
import FilePicker, { CloudinaryUploader } from "../ads/FilePicker";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "FilePicker",
|
||||||
|
component: FilePicker,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ShowUploadedFile(data: any) {
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const withDynamicProps = () => (
|
||||||
|
<FilePicker
|
||||||
|
onFileUploaded={data => ShowUploadedFile(data)}
|
||||||
|
fileUploader={CloudinaryUploader}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
@ -690,6 +690,15 @@ type ColorType = {
|
||||||
light: ShadeColor;
|
light: ShadeColor;
|
||||||
dark: ShadeColor;
|
dark: ShadeColor;
|
||||||
};
|
};
|
||||||
|
filePicker: {
|
||||||
|
bg: ShadeColor;
|
||||||
|
color: ShadeColor;
|
||||||
|
progress: ShadeColor;
|
||||||
|
shadow: {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
formFooter: {
|
formFooter: {
|
||||||
cancelBtn: ShadeColor;
|
cancelBtn: ShadeColor;
|
||||||
};
|
};
|
||||||
|
|
@ -838,7 +847,7 @@ export const dark: ColorType = {
|
||||||
border: darkShades[2],
|
border: darkShades[2],
|
||||||
},
|
},
|
||||||
normal: {
|
normal: {
|
||||||
bg: darkShades[0],
|
bg: lightShades[10],
|
||||||
text: darkShades[9],
|
text: darkShades[9],
|
||||||
border: darkShades[0],
|
border: darkShades[0],
|
||||||
},
|
},
|
||||||
|
|
@ -967,6 +976,15 @@ export const dark: ColorType = {
|
||||||
light: darkShades[2],
|
light: darkShades[2],
|
||||||
dark: darkShades[4],
|
dark: darkShades[4],
|
||||||
},
|
},
|
||||||
|
filePicker: {
|
||||||
|
bg: darkShades[1],
|
||||||
|
color: darkShades[7],
|
||||||
|
progress: darkShades[6],
|
||||||
|
shadow: {
|
||||||
|
from: "rgba(21, 17, 17, 0.0001)",
|
||||||
|
to: "rgba(9, 7, 7, 0.883386)",
|
||||||
|
},
|
||||||
|
},
|
||||||
formFooter: {
|
formFooter: {
|
||||||
cancelBtn: darkShades[9],
|
cancelBtn: darkShades[9],
|
||||||
},
|
},
|
||||||
|
|
@ -1244,6 +1262,15 @@ export const light: ColorType = {
|
||||||
light: lightShades[2],
|
light: lightShades[2],
|
||||||
dark: lightShades[4],
|
dark: lightShades[4],
|
||||||
},
|
},
|
||||||
|
filePicker: {
|
||||||
|
bg: lightShades[2],
|
||||||
|
color: lightShades[7],
|
||||||
|
progress: lightShades[6],
|
||||||
|
shadow: {
|
||||||
|
from: "rgba(253, 253, 253, 0.0001)",
|
||||||
|
to: "rgba(250, 250, 250, 0.898847)",
|
||||||
|
},
|
||||||
|
},
|
||||||
formFooter: {
|
formFooter: {
|
||||||
cancelBtn: lightShades[9],
|
cancelBtn: lightShades[9],
|
||||||
},
|
},
|
||||||
|
|
@ -1259,7 +1286,7 @@ export const light: ColorType = {
|
||||||
export const theme: Theme = {
|
export const theme: Theme = {
|
||||||
radii: [0, 4, 8, 10, 20, 50],
|
radii: [0, 4, 8, 10, 20, 50],
|
||||||
fontSizes: [0, 10, 12, 14, 16, 18, 24, 28, 32, 48, 64],
|
fontSizes: [0, 10, 12, 14, 16, 18, 24, 28, 32, 48, 64],
|
||||||
spaces: [0, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 30, 36],
|
spaces: [0, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 30, 36, 38, 40, 42, 44],
|
||||||
fontWeights: [0, 400, 500, 700],
|
fontWeights: [0, 400, 500, 700],
|
||||||
typography: {
|
typography: {
|
||||||
h1: {
|
h1: {
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,8 @@ export const ReduxActionTypes: { [key: string]: string } = {
|
||||||
FETCH_ORGS_INIT: "FETCH_ORGS_INIT",
|
FETCH_ORGS_INIT: "FETCH_ORGS_INIT",
|
||||||
SAVE_ORG_INIT: "SAVE_ORG_INIT",
|
SAVE_ORG_INIT: "SAVE_ORG_INIT",
|
||||||
SAVE_ORG_SUCCESS: "SAVE_ORG_SUCCESS",
|
SAVE_ORG_SUCCESS: "SAVE_ORG_SUCCESS",
|
||||||
|
UPLOAD_ORG_LOGO: "UPLOAD_ORG_LOGO",
|
||||||
|
REMOVE_ORG_LOGO: "REMOVE_ORG_LOGO",
|
||||||
SAVING_ORG_INFO: "SAVING_ORG_INFO",
|
SAVING_ORG_INFO: "SAVING_ORG_INFO",
|
||||||
SET_CURRENT_ORG: "SET_CURRENT_ORG",
|
SET_CURRENT_ORG: "SET_CURRENT_ORG",
|
||||||
SET_CURRENT_ORG_ID: "SET_CURRENT_ORG_ID",
|
SET_CURRENT_ORG_ID: "SET_CURRENT_ORG_ID",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ export type Org = {
|
||||||
name: string;
|
name: string;
|
||||||
website?: string;
|
website?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
logoUrl?: string;
|
||||||
|
uploadProgress?: number;
|
||||||
userPermissions?: string[];
|
userPermissions?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { saveOrg } from "actions/orgActions";
|
import { deleteOrgLogo, saveOrg, uploadOrgLogo } from "actions/orgActions";
|
||||||
import { SaveOrgRequest } from "api/OrgApi";
|
import { SaveOrgRequest } from "api/OrgApi";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import TextInput, {
|
import TextInput, {
|
||||||
|
|
@ -8,11 +8,19 @@ import TextInput, {
|
||||||
notEmptyValidator,
|
notEmptyValidator,
|
||||||
} from "components/ads/TextInput";
|
} from "components/ads/TextInput";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { getCurrentOrg } from "selectors/organizationSelectors";
|
import {
|
||||||
|
getCurrentError,
|
||||||
|
getCurrentOrg,
|
||||||
|
} from "selectors/organizationSelectors";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Text, { TextType } from "components/ads/Text";
|
import Text, { TextType } from "components/ads/Text";
|
||||||
import { Classes } from "@blueprintjs/core";
|
import { Classes } from "@blueprintjs/core";
|
||||||
|
import { getOrgLoadingStates } from "selectors/organizationSelectors";
|
||||||
|
import FilePicker, {
|
||||||
|
SetProgress,
|
||||||
|
UploadCallback,
|
||||||
|
} from "components/ads/FilePicker";
|
||||||
import { getIsFetchingApplications } from "selectors/applicationSelectors";
|
import { getIsFetchingApplications } from "selectors/applicationSelectors";
|
||||||
|
|
||||||
const InputLabelWrapper = styled.div`
|
const InputLabelWrapper = styled.div`
|
||||||
|
|
@ -36,7 +44,13 @@ export const SettingsHeading = styled(Text)`
|
||||||
|
|
||||||
const Loader = styled.div`
|
const Loader = styled.div`
|
||||||
height: 38px;
|
height: 38px;
|
||||||
width: 260px;
|
width: 320px;
|
||||||
|
border-radius: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FilePickerLoader = styled.div`
|
||||||
|
height: 190px;
|
||||||
|
width: 333px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -73,6 +87,36 @@ export function GeneralSettings() {
|
||||||
});
|
});
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
|
const { isFetchingOrg } = useSelector(getOrgLoadingStates);
|
||||||
|
const logoUploadError = useSelector(getCurrentError);
|
||||||
|
|
||||||
|
const FileUploader = (
|
||||||
|
file: File,
|
||||||
|
setProgress: SetProgress,
|
||||||
|
onUpload: UploadCallback,
|
||||||
|
) => {
|
||||||
|
const progress = (progressEvent: ProgressEvent) => {
|
||||||
|
const uploadPercentage = Math.round(
|
||||||
|
(progressEvent.loaded / progressEvent.total) * 100,
|
||||||
|
);
|
||||||
|
if (uploadPercentage === 100) {
|
||||||
|
onUpload(currentOrg.logoUrl || "");
|
||||||
|
}
|
||||||
|
setProgress(uploadPercentage);
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
uploadOrgLogo({
|
||||||
|
id: orgId as string,
|
||||||
|
logo: file,
|
||||||
|
progress: progress,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeleteLogo = () => {
|
||||||
|
dispatch(deleteOrgLogo(orgId));
|
||||||
|
};
|
||||||
const isFetchingApplications = useSelector(getIsFetchingApplications);
|
const isFetchingApplications = useSelector(getIsFetchingApplications);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -96,6 +140,23 @@ export function GeneralSettings() {
|
||||||
)}
|
)}
|
||||||
</SettingWrapper>
|
</SettingWrapper>
|
||||||
|
|
||||||
|
<SettingWrapper>
|
||||||
|
<InputLabelWrapper>
|
||||||
|
<Text type={TextType.H4}>Upload Logo</Text>
|
||||||
|
</InputLabelWrapper>
|
||||||
|
{isFetchingOrg && (
|
||||||
|
<FilePickerLoader className={Classes.SKELETON}></FilePickerLoader>
|
||||||
|
)}
|
||||||
|
{!isFetchingOrg && (
|
||||||
|
<FilePicker
|
||||||
|
url={currentOrg && currentOrg.logoUrl}
|
||||||
|
fileUploader={FileUploader}
|
||||||
|
onFileRemoved={DeleteLogo}
|
||||||
|
logoUploadError={logoUploadError.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SettingWrapper>
|
||||||
|
|
||||||
<SettingWrapper>
|
<SettingWrapper>
|
||||||
<InputLabelWrapper>
|
<InputLabelWrapper>
|
||||||
<Text type={TextType.H4}>Website</Text>
|
<Text type={TextType.H4}>Website</Text>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { call, takeLatest, put, all } from "redux-saga/effects";
|
import { call, takeLatest, put, all, select } from "redux-saga/effects";
|
||||||
import {
|
import {
|
||||||
ReduxActionTypes,
|
ReduxActionTypes,
|
||||||
ReduxAction,
|
ReduxAction,
|
||||||
|
|
@ -22,10 +22,12 @@ import OrgApi, {
|
||||||
DeleteOrgUserRequest,
|
DeleteOrgUserRequest,
|
||||||
ChangeUserRoleRequest,
|
ChangeUserRoleRequest,
|
||||||
FetchAllRolesRequest,
|
FetchAllRolesRequest,
|
||||||
|
SaveOrgLogo,
|
||||||
} from "api/OrgApi";
|
} from "api/OrgApi";
|
||||||
import { ApiResponse } from "api/ApiResponses";
|
import { ApiResponse } from "api/ApiResponses";
|
||||||
import { Toaster } from "components/ads/Toast";
|
import { Toaster } from "components/ads/Toast";
|
||||||
import { Variant } from "components/ads/common";
|
import { Variant } from "components/ads/common";
|
||||||
|
import { getCurrentOrg } from "selectors/organizationSelectors";
|
||||||
|
|
||||||
export function* fetchRolesSaga() {
|
export function* fetchRolesSaga() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -229,6 +231,60 @@ export function* createOrgSaga(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* uploadOrgLogoSaga(action: ReduxAction<SaveOrgLogo>) {
|
||||||
|
try {
|
||||||
|
const request = action.payload;
|
||||||
|
const response: ApiResponse = yield call(OrgApi.saveOrgLogo, request);
|
||||||
|
const isValidResponse = yield validateResponse(response);
|
||||||
|
if (isValidResponse) {
|
||||||
|
const currentOrg = yield select(getCurrentOrg);
|
||||||
|
if (currentOrg && currentOrg.id === request.id) {
|
||||||
|
const updatedOrg = {
|
||||||
|
...currentOrg,
|
||||||
|
logoUrl: response.data.logoUrl,
|
||||||
|
};
|
||||||
|
yield put({
|
||||||
|
type: ReduxActionTypes.SET_CURRENT_ORG,
|
||||||
|
payload: updatedOrg,
|
||||||
|
});
|
||||||
|
Toaster.show({
|
||||||
|
text: "Logo uploaded successfully",
|
||||||
|
variant: Variant.success,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error occured while uploading the logo", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* deleteOrgLogoSaga(action: ReduxAction<{ id: string }>) {
|
||||||
|
try {
|
||||||
|
const request = action.payload;
|
||||||
|
const response: ApiResponse = yield call(OrgApi.deleteOrgLogo, request);
|
||||||
|
const isValidResponse = yield validateResponse(response);
|
||||||
|
if (isValidResponse) {
|
||||||
|
const currentOrg = yield select(getCurrentOrg);
|
||||||
|
if (currentOrg && currentOrg.id === request.id) {
|
||||||
|
const updatedOrg = {
|
||||||
|
...currentOrg,
|
||||||
|
logoUrl: response.data.logoUrl,
|
||||||
|
};
|
||||||
|
yield put({
|
||||||
|
type: ReduxActionTypes.SET_CURRENT_ORG,
|
||||||
|
payload: updatedOrg,
|
||||||
|
});
|
||||||
|
Toaster.show({
|
||||||
|
text: "Logo removed successfully",
|
||||||
|
variant: Variant.success,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error occured while removing the logo", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function* orgSagas() {
|
export default function* orgSagas() {
|
||||||
yield all([
|
yield all([
|
||||||
takeLatest(ReduxActionTypes.FETCH_ORG_ROLES_INIT, fetchRolesSaga),
|
takeLatest(ReduxActionTypes.FETCH_ORG_ROLES_INIT, fetchRolesSaga),
|
||||||
|
|
@ -242,5 +298,7 @@ export default function* orgSagas() {
|
||||||
ReduxActionTypes.CHANGE_ORG_USER_ROLE_INIT,
|
ReduxActionTypes.CHANGE_ORG_USER_ROLE_INIT,
|
||||||
changeOrgUserRoleSaga,
|
changeOrgUserRoleSaga,
|
||||||
),
|
),
|
||||||
|
takeLatest(ReduxActionTypes.UPLOAD_ORG_LOGO, uploadOrgLogoSaga),
|
||||||
|
takeLatest(ReduxActionTypes.REMOVE_ORG_LOGO, deleteOrgLogoSaga),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,3 +58,6 @@ export const getRolesForField = createSelector(getAllRoles, (roles?: any) => {
|
||||||
export const getDefaultRole = createSelector(getRoles, (roles?: OrgRole[]) => {
|
export const getDefaultRole = createSelector(getRoles, (roles?: OrgRole[]) => {
|
||||||
return roles?.find(role => role.isDefault);
|
return roles?.find(role => role.isDefault);
|
||||||
});
|
});
|
||||||
|
export const getCurrentError = (state: AppState) => {
|
||||||
|
return state.ui.errors;
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user