[Apps] Apps marketplace (first draft) (#17562)

https://github.com/user-attachments/assets/c4e63edb-6e98-44bc-841f-ee110ae712d4

How it works
- `manifest.json` files are now committed when apps are published to our
repo
- to display available apps, from the server, we read into our github
repo, using a pod-scoped cache
- feature flagged
- app installation will be behind permission gate MARKETPLACE_APPS

Limitations and what is yet to develop
- content and settings tabs
- installed apps tab
- app installation
- test and potentially fix reading from .manifest.json once [Reshape
manifest
structure](https://github.com/twentyhq/core-team-issues/issues/2183) is
done. additional work is expected on assets notably. (couldnt properly
do it here as manifest.json will only be committed after this pr)
- we only read in community/ folder for now - we may want tochange that
- the cache is rather artisanal for now and scoped by pod - we may want
to change that
This commit is contained in:
Marie
2026-01-30 18:32:32 +01:00
committed by GitHub
parent 36cf388c81
commit 0001c2e7d0
98 changed files with 1713 additions and 280 deletions
@@ -0,0 +1,105 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconFilter, IconSearch } from '@ui/display';
import { IconButton } from '@ui/input/button/components/IconButton';
import { type ChangeEvent, type ReactNode, useState } from 'react';
export type SearchInputProps = {
value: string;
onChange: (value: string) => void;
placeholder?: string;
filterDropdown?: (filterButton: ReactNode) => ReactNode;
autoFocus?: boolean;
disabled?: boolean;
className?: string;
};
const StyledWrapper = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledInputContainer = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-sizing: border-box;
display: flex;
flex: 1;
gap: ${({ theme }) => theme.spacing(1)};
height: 32px;
padding: 0 ${({ theme }) => theme.spacing(2)};
&:focus-within {
border-color: ${({ theme }) => theme.color.blue};
}
`;
const StyledIconContainer = styled.div<{ isFocused: boolean }>`
align-items: center;
color: ${({ theme, isFocused }) =>
isFocused ? theme.font.color.secondary : theme.font.color.light};
display: flex;
justify-content: center;
`;
const StyledInput = styled.input`
background: transparent;
border: none;
color: ${({ theme }) => theme.font.color.primary};
flex: 1;
font-family: ${({ theme }) => theme.font.family};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
outline: none;
width: 100%;
&::placeholder {
color: ${({ theme }) => theme.font.color.light};
font-weight: ${({ theme }) => theme.font.weight.medium};
}
&:disabled {
color: ${({ theme }) => theme.font.color.tertiary};
}
`;
export const SearchInput = ({
value,
onChange,
placeholder,
filterDropdown,
autoFocus,
disabled,
className,
}: SearchInputProps) => {
const theme = useTheme();
const [isFocused, setIsFocused] = useState(false);
const filterButton = <IconButton Icon={IconFilter} variant="secondary" />;
return (
<StyledWrapper className={className}>
<StyledInputContainer>
<StyledIconContainer isFocused={isFocused}>
<IconSearch size={theme.icon.size.md} />
</StyledIconContainer>
<StyledInput
value={value}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
onChange(event.target.value)
}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholder={placeholder}
autoFocus={autoFocus}
disabled={disabled}
/>
</StyledInputContainer>
{filterDropdown && filterDropdown(filterButton)}
</StyledWrapper>
);
};