Skip to content

Commit

Permalink
feat: create modify and export a local live app inside lld (#6452)
Browse files Browse the repository at this point in the history
feat: create and export a local manifest inside lld
  • Loading branch information
RamyEB committed Apr 25, 2024
1 parent b86a1e0 commit 7dab046
Show file tree
Hide file tree
Showing 21 changed files with 1,135 additions and 18 deletions.
5 changes: 5 additions & 0 deletions .changeset/happy-tools-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ledger-live-desktop": patch
---

Create, modify and export a local manifest inside LL
5 changes: 5 additions & 0 deletions .changeset/metal-garlics-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/live-common": patch
---

Add zod Schema for LiveApp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import styled from "styled-components";

export const ChipContainer = styled.div`
display: inline-flex;
cursor: pointer;
width: max-content;
overflow: hidden;
border-radius: 10px;
margin: 0px;
`;

export const Chip = styled.div<{
active: boolean;
}>`
color: ${p =>
p.active ? p.theme.colors.palette.primary.contrastText : p.theme.colors.palette.text.shade20};
background: ${p =>
p.active ? p.theme.colors.palette.primary.main : p.theme.colors.palette.action.disabled};
padding: 0px 8px 2px 8px;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Flex } from "@ledgerhq/react-ui";
import React, { useEffect, useState } from "react";
import Text from "~/renderer/components/Text";
import { Chip, ChipContainer } from "./Chip";
import Cross from "~/renderer/icons/Cross";
import Input from "~/renderer/components/Input";
import FormLiveAppHeader from "./FormLiveAppHeader";
import { DESCRIPTIONS } from "./defaultValues";

type Props = {
fieldName: string;
initialValue: string[];
optional: boolean;
parseCheck: boolean;
path: string;
handleChange: (path: string, value: unknown) => void;
};

function FormLiveAppArray({
fieldName,
initialValue,
optional,
parseCheck,
path,
handleChange,
}: Props) {
const [inputValue, setInputValue] = useState<string>("");
const [selectedValues, setSelectedValues] = useState<string[]>(initialValue);

useEffect(() => {
handleChange(path, selectedValues);
}, [handleChange, path, selectedValues]);

const removeItem = (item: string) => {
setSelectedValues((prev: string[]) => {
return prev.filter(option => option !== item);
});
};

const handleOnEnter = () => {
if (selectedValues.includes(inputValue) || inputValue.trim() === "") return;
setSelectedValues((prev: string[]) => {
const newSelectedvalues = prev;
newSelectedvalues.push(inputValue);
return newSelectedvalues;
});
setInputValue("");
};

return (
<>
<Flex flexDirection={"column"}>
<FormLiveAppHeader
fieldName={fieldName}
description={DESCRIPTIONS[fieldName]}
optional={optional}
/>
<Input
error={!parseCheck}
onEnter={handleOnEnter}
placeholder={optional ? "optional" : "required"}
onChange={setInputValue}
value={inputValue}
/>
<Flex marginTop={2} rowGap={2} flexWrap={"wrap"} columnGap={2} maxHeight={"100%"}>
{selectedValues.map((enumItem, index) => (
<ChipContainer key={enumItem}>
<Chip
style={{
display: "flex",
alignContent: "center",
cursor: "initial",
height: "min-content",
}}
active={true}
key={index}
>
<Text ff="Inter|Medium" fontSize={4}>
{enumItem}
</Text>
<div
onClick={() => removeItem(enumItem)}
style={{
width: "min-content",
height: "max-content",
margin: "auto",
marginLeft: "3px",
cursor: "pointer",
display: "flex",
}}
>
<Cross size={10} />
</div>
</Chip>
</ChipContainer>
))}
</Flex>
</Flex>
</>
);
}

export default FormLiveAppArray;
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { Flex } from "@ledgerhq/react-ui";
import React, { useEffect, useState } from "react";
import Text from "~/renderer/components/Text";
import { Chip, ChipContainer } from "./Chip";
import Cross from "~/renderer/icons/Cross";
import Select from "~/renderer/components/Select";
import FormLiveAppHeader from "./FormLiveAppHeader";
import { DESCRIPTIONS } from "./defaultValues";

type Props = {
fieldName: string;
initialValue: string[];
optional: boolean;
parseCheck: boolean;
options?: string[];
path: string;
handleChange: (path: string, value: unknown) => void;
};
type Option = {
value: string;
label: string;
};

function FormLiveAppArraySelect({
fieldName,
initialValue,
optional,
options,
parseCheck,
path,
handleChange,
}: Props) {
const [selectedValues, setSelectedValues] = useState<string[]>(initialValue);
const [optionsAvailable, setOptionsAvailable] = useState<Option[]>(
options
? options
.filter(option => !initialValue.includes(option))
.map(option => ({ value: option, label: option }))
: [],
);

useEffect(() => {
handleChange(path, selectedValues);
}, [handleChange, path, selectedValues]);

const handleOnChange = (item: Option) => {
setOptionsAvailable((prev: Option[]) => {
return prev.filter(option => option.value != item.value);
});
setSelectedValues((prev: string[]) => {
const newSelectedvalues = [...prev];
newSelectedvalues.push(item.value);
return newSelectedvalues;
});
};

const removeItem = (item: string) => {
setSelectedValues((prev: string[]) => {
return prev.filter(option => option !== item);
});
setOptionsAvailable((prev: Option[]) => {
const newSelectedvalues = [...prev];
newSelectedvalues.push({ value: item, label: item });
return newSelectedvalues;
});
};

const handleOnEnter = (value: string) => {
if (selectedValues.includes(value)) return;

setSelectedValues((prev: string[]) => {
const newSelectedvalues = [...prev];
newSelectedvalues.push(value);
return newSelectedvalues;
});
};

return (
<>
<Flex flexDirection={"column"}>
<FormLiveAppHeader
fieldName={fieldName}
description={DESCRIPTIONS[fieldName]}
optional={optional}
/>
<Select
blurInputOnSelect={true}
onKeyDown={e => {
const target = e.target as HTMLTextAreaElement;
if (e.keyCode === 13 && target.value !== "") {
e.preventDefault();
handleOnEnter(target.value);
target.blur();
}
}}
error={parseCheck ? null : new Error()}
onChange={(option: unknown) => {
handleOnChange(option as Option);
}}
value={null}
noOptionsMessage={() => "No more option available"}
options={optionsAvailable}
/>
<Flex marginTop={2} rowGap={2} flexWrap={"wrap"} columnGap={2} maxHeight={"100%"}>
{selectedValues.map((enumItem, index) => (
<ChipContainer key={enumItem}>
<Chip
style={{
display: "flex",
alignContent: "center",
cursor: "initial",
height: "min-content",
}}
active={true}
key={index}
>
<Text ff="Inter|Medium" fontSize={4}>
{enumItem}
</Text>
<div
onClick={() => removeItem(enumItem)}
style={{
width: "min-content",
height: "max-content",
margin: "auto",
marginLeft: "3px",
cursor: "pointer",
display: "flex",
}}
>
<Cross size={10} />
</div>
</Chip>
</ChipContainer>
))}
</Flex>
</Flex>
</>
);
}

export default FormLiveAppArraySelect;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Flex } from "@ledgerhq/react-ui";
import React from "react";
import Text from "~/renderer/components/Text";

type Props = { fieldName: string; description?: string; optional: boolean };

const FormLiveAppHeader = ({ fieldName, description, optional }: Props) => {
return (
<Flex marginBottom={1} flexDirection={"column"}>
<Text marginLeft={1} ff="Inter|Medium" fontSize={4}>
{`${fieldName} `}
{!optional && <span style={{ color: "red" }}>*</span>}
</Text>
{description && (
<Text color={"grey"} marginLeft={1} ff="Inter|Medium" fontSize={2}>
{description}
</Text>
)}
</Flex>
);
};

export default FormLiveAppHeader;
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Flex } from "@ledgerhq/react-ui";
import React from "react";
import Input from "~/renderer/components/Input";
import Switch from "~/renderer/components/Switch";
import FormLiveAppHeader from "./FormLiveAppHeader";
import { DESCRIPTIONS } from "./defaultValues";

type Props = {
type: string;
fieldName: string;
value: unknown;
optional: boolean;
parseCheck: boolean;
path: string;
autoFocus?: boolean;
disabled?: boolean;
handleChange: (path: string, value: unknown) => void;
};

function FormLiveAppInput({
type,
fieldName,
value,
optional,
parseCheck,
path,
handleChange,
autoFocus = false,
disabled = false,
}: Props) {
return (
<Flex flexDirection={"column"}>
<FormLiveAppHeader
fieldName={fieldName}
description={DESCRIPTIONS[fieldName]}
optional={optional}
/>
{typeof value === "boolean" ? (
<Flex width={"max-content"} marginLeft={1}>
<Switch
isChecked={value}
onChange={value => {
handleChange(path, value);
}}
/>
</Flex>
) : (
<Input
error={!parseCheck}
placeholder={optional ? "optional" : "required"}
disabled={disabled}
autoFocus={autoFocus}
onChange={(value: number | string) => {
if (type === "number") {
!isNaN(Number(value)) && handleChange(path, Number(value));
return;
}
typeof value !== "number" && handleChange(path, value);
}}
value={String(value)}
/>
)}
</Flex>
);
}

export default FormLiveAppInput;
Loading

0 comments on commit 7dab046

Please sign in to comment.