Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions browser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This changelog covers all three packages, as they are (for now) updated as a who
### Atomic Browser

- [#841](https://github.com/atomicdata-dev/atomic-server/issues/841) Add better inputs for `Timestamp` and `Date` datatypes.
- [#842](https://github.com/atomicdata-dev/atomic-server/issues/842) Add media picker for properties with classtype file.

## v0.37.0

Expand Down
1 change: 1 addition & 0 deletions browser/data-browser/src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface ButtonProps
gutter?: boolean;
onClick?: (e: React.MouseEvent) => unknown;
className?: string;
as?: keyof HTMLElementTagNameMap;
}

interface ButtonPropsStyled {
Expand Down
21 changes: 14 additions & 7 deletions browser/data-browser/src/components/Dialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createPortal } from 'react-dom';
import { useHotkeys } from 'react-hotkeys-hook';
import { FaTimes } from 'react-icons/fa';
import { styled, keyframes } from 'styled-components';
import * as CSS from 'csstype';
import { effectTimeout } from '../../helpers/effectTimeout';
import { Button } from '../Button';
import { DropdownContainer } from '../Dropdown/DropdownContainer';
Expand All @@ -26,16 +27,17 @@ export interface InternalDialogProps {
show: boolean;
onClose: (success: boolean) => void;
onClosed: () => void;
width?: CSS.Property.Width;
}

export type WrappedDialogType = React.FC<React.PropsWithChildren<unknown>>;

export enum DialogSlot {
Title = 'title',
Content = 'content',
Actions = 'actions',
}

export const DIALOG_MEDIA_BREAK_POINT = '640px';

const ANIM_MS = 80;
const ANIM_SPEED = `${ANIM_MS}ms`;

Expand Down Expand Up @@ -84,6 +86,7 @@ export function Dialog(props: React.PropsWithChildren<InternalDialogProps>) {
const InnerDialog: React.FC<React.PropsWithChildren<InternalDialogProps>> = ({
children,
show,
width,
onClose,
onClosed,
}) => {
Expand Down Expand Up @@ -150,7 +153,11 @@ const InnerDialog: React.FC<React.PropsWithChildren<InternalDialogProps>> = ({
}, [show, onClosed]);

return (
<StyledDialog ref={dialogRef} onMouseDown={handleOutSideClick}>
<StyledDialog
ref={dialogRef}
onMouseDown={handleOutSideClick}
$width={width}
>
<StyledInnerDialog ref={innerDialogRef}>
<PopoverContainer>
<DropdownContainer>
Expand Down Expand Up @@ -252,7 +259,7 @@ const fadeInBackground = keyframes`
}
`;

const StyledDialog = styled.dialog`
const StyledDialog = styled.dialog<{ $width?: CSS.Property.Width }>`
--animation-speed: 500ms;
box-sizing: border-box;
inset: 0px;
Expand All @@ -263,8 +270,8 @@ const StyledDialog = styled.dialog`
background-color: ${props => props.theme.colors.bg};
border-radius: ${props => props.theme.radius};
border: solid 1px ${props => props.theme.colors.bg2};
max-inline-size: min(90vw, 100ch);
min-inline-size: min(90vw, 60ch);
max-inline-size: min(90vw, ${p => p.$width ?? '100ch'});
min-inline-size: min(90vw, ${p => p.$width ?? '60ch'});
max-block-size: 100vh;
height: fit-content;
max-height: 90vh;
Expand Down Expand Up @@ -310,7 +317,7 @@ const StyledDialog = styled.dialog`
backdrop-filter: blur(0px);
}

@media (max-width: ${props => props.theme.containerWidth}rem) {
@media (max-width: ${DIALOG_MEDIA_BREAK_POINT}) {
max-inline-size: 100%;
max-block-size: 100vh;
}
Expand Down
7 changes: 7 additions & 0 deletions browser/data-browser/src/components/IconButton/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type BaseProps = {
color?: ColorProp;
size?: string;
title: string;
edgeAlign?: 'start' | 'end';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
as?: string | ComponentType<any>;
};
Expand Down Expand Up @@ -78,6 +79,7 @@ IconButtonLink.defaultProps = defaultProps as IconButtonLinkProps;

interface ButtonBaseProps {
size?: string;
edgeAlign?: 'start' | 'end';
}

const IconButtonBase = styled.button<ButtonBaseProps>`
Expand All @@ -94,6 +96,11 @@ const IconButtonBase = styled.button<ButtonBaseProps>`
width: calc(${p => p.size} + var(--button-padding) * 2);
height: calc(${p => p.size} + var(--button-padding) * 2);

margin-inline-start: ${p =>
p.edgeAlign === 'start' ? 'calc(var(--button-padding) * -1)' : '0'};

margin-inline-end: ${p =>
p.edgeAlign === 'end' ? 'calc(var(--button-padding) * -1)' : '0'};
&[disabled] {
opacity: 0.5;
cursor: not-allowed;
Expand Down
16 changes: 13 additions & 3 deletions browser/data-browser/src/components/SideBar/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { styled } from 'styled-components';

import { FaGithub, FaDiscord, FaBook } from 'react-icons/fa';
import { IconButtonLink, IconButtonVariant } from '../IconButton/IconButton';
import { FaRadiation } from 'react-icons/fa6';
import { isDev } from '../../config';

interface AboutItem {
icon: React.ReactNode;
Expand Down Expand Up @@ -30,9 +32,6 @@ const aboutMenuItems: AboutItem[] = [
export function About() {
return (
<>
{/* <SideBarHeader>
<Logo style={{ height: '1.1rem', maxWidth: '100%' }} />
</SideBarHeader> */}
<AboutWrapper>
{aboutMenuItems.map(({ href, icon, helper }) => (
<IconButtonLink
Expand All @@ -48,6 +47,17 @@ export function About() {
{icon}
</IconButtonLink>
))}
{isDev() && (
<IconButtonLink
href='/sandbox'
title='Sandbox, test components in isolation'
size='1.2em'
color='textLight'
variant={IconButtonVariant.Square}
>
<FaRadiation />
</IconButtonLink>
)}
</AboutWrapper>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { styled } from 'styled-components';
import { InnerWrapper } from './components';
import { InnerWrapper } from '../views/FolderPage/GridItem/components';

interface GridItemWithImageProps {
interface ThumbnailProps {
src: string | undefined;
style?: React.CSSProperties | undefined;
}

export function GridItemWithImage({
src,
style,
}: GridItemWithImageProps): JSX.Element {
export function Thumbnail({ src, style }: ThumbnailProps): JSX.Element {
if (src === undefined) {
return <TextWrapper>No preview available</TextWrapper>;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useResource } from '@tomic/react';
import { styled } from 'styled-components';
import { ErrorBoundary } from '../../../views/ErrorPage';
import { FilePreviewThumbnail } from '../../../views/File/FilePreviewThumbnail';

interface FilePickerItemProps {
subject: string;
onClick?: () => void;
}

export function FilePickerItem({
subject,
onClick,
}: FilePickerItemProps): React.JSX.Element {
const resource = useResource(subject);

if (resource.loading) {
return <div>loading</div>;
}

return (
<ErrorBoundary FallBackComponent={ItemError}>
<ItemWrapper onClick={onClick}>
<ItemCard>
<FilePreviewThumbnail resource={resource} />
</ItemCard>
<span>{resource.title}</span>
</ItemWrapper>
</ErrorBoundary>
);
}

const ItemCard = styled.div`
background-color: ${p => p.theme.colors.bg1};
border-radius: ${p => p.theme.radius};
overflow: hidden;
box-shadow: var(--shadow), var(--interaction-shadow);
border: 1px solid ${p => p.theme.colors.bg2};
height: 100%;
width: 100%;
touch-action: none;
pointer-events: none;
user-select: none;
transition: border 0.1s ease-in-out, box-shadow 0.1s ease-in-out;
`;

const ItemWrapper = styled.button`
appearance: none;
text-align: start;
border: none;
padding: 0;
background-color: transparent;
--shadow: 0px 0.7px 1.3px rgba(0, 0, 0, 0.06),
0px 1.8px 3.2px rgba(0, 0, 0, 0.043), 0px 3.4px 6px rgba(0, 0, 0, 0.036),
0px 6px 10.7px rgba(0, 0, 0, 0.03), 0px 11.3px 20.1px rgba(0, 0, 0, 0.024),
0px 27px 48px rgba(0, 0, 0, 0.017);
--interaction-shadow: 0px 0px 0px 0px ${p => p.theme.colors.main};
--card-banner-height: 0px;
display: flex;
gap: 0.5rem;
flex-direction: column;
align-items: center;
outline: none;
text-decoration: none;
color: ${p => p.theme.colors.text1};
width: 100%;
aspect-ratio: 1 / 1;
cursor: pointer;

&:hover ${ItemCard}, &:focus ${ItemCard} {
--interaction-shadow: 0px 0px 0px 1px ${p => p.theme.colors.main};
border: 1px solid ${p => p.theme.colors.main};
}

&:hover,
&:focus {
color: ${p => p.theme.colors.main};
}
`;

interface ItemErrorProps {
error: Error;
}

const ItemError: React.FC<ItemErrorProps> = ({ error }) => {
return <ItemErrorWrapper>{error.message}</ItemErrorWrapper>;
};

const ItemErrorWrapper = styled.div`
color: ${p => p.theme.colors.alert};
text-align: center;
`;
119 changes: 119 additions & 0 deletions browser/data-browser/src/components/forms/FilePicker/FilePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { useEffect, useState } from 'react';
import { Button } from '../../Button';
import { FilePickerDialog } from './FilePickerDialog';
import { SelectedFileBlob, SelectedFileResource } from './SelectedFile';
import { InputProps } from '../ResourceField';
import { FaFileCirclePlus } from 'react-icons/fa6';
import { StoreEvents, useStore, useSubject } from '@tomic/react';
import { useUpload } from '../../../hooks/useUpload';
import { VisuallyHidden } from '../../VisuallyHidden';
import { styled } from 'styled-components';

/**
* Button that opens a dialog that lists all files in the drive and allows the user to upload a new file.
* Handles uploads and makes sure files are uploaded even when the parent resource is not saved yet.
*/
export function FilePicker({
resource,
property,
disabled,
required,
commit,
}: InputProps): React.JSX.Element {
const store = useStore();
const { upload } = useUpload(resource);
const [value, setValue] = useSubject(resource, property.subject, {
validate: false,
commit: commit,
});
const [show, setShow] = useState(false);
const [selectedSubject, setSelectedSubject] = useState<string | undefined>(
value,
);
const [selectedFile, setSelectedFile] = useState<File | undefined>();

const [unsubScheduledUpload, setUnsubScheduledUpload] =
useState<() => void | undefined>();

useEffect(() => {
if (selectedSubject) {
setValue(selectedSubject);
} else if (selectedFile) {
if (resource.new) {
// We can't upload the file yet because its parent has not saved yet so we set the value to a placeholder and then schedule an upload when the resource is saved.
setValue('https://placeholder');
setUnsubScheduledUpload(prevUnsub => {
prevUnsub?.();

const thisUnsub = store.on(
StoreEvents.ResourceSaved,
async savedResource => {
if (savedResource.getSubject() === resource.getSubject()) {
thisUnsub();
const [subject] = await upload([selectedFile]);
await setValue(subject);
resource.save(store);
}
},
);

return thisUnsub;
});
} else {
upload([selectedFile]).then(([subject]) => {
setValue(subject);
});
}
} else {
setValue(undefined);
}
}, [selectedSubject, selectedFile]);

return (
<Wrapper>
<VisuallyHidden>
{value}
<input
aria-hidden
type='text'
value={value ?? ''}
required={required}
disabled={disabled}
/>
</VisuallyHidden>
{!selectedFile && !selectedSubject && (
<Button subtle onClick={() => setShow(true)} disabled={disabled}>
<FaFileCirclePlus />
Select File
</Button>
)}
{selectedSubject && (
<SelectedFileResource
disabled={disabled}
subject={selectedSubject}
onClear={() => setSelectedSubject(undefined)}
/>
)}
{selectedFile && (
<SelectedFileBlob
file={selectedFile}
disabled={disabled}
onClear={() => {
setSelectedFile(undefined);
unsubScheduledUpload?.();
}}
/>
)}
<FilePickerDialog
show={show}
onShowChange={setShow}
onResourcePicked={setSelectedSubject}
onNewFilePicked={setSelectedFile}
/>
</Wrapper>
);
}

const Wrapper = styled.div`
position: relative;
`;
Loading