Skip to content

Commit

Permalink
chore(web): add URL field (#674)
Browse files Browse the repository at this point in the history
Co-authored-by: nina992 <[email protected]>
Co-authored-by: KaWaite <[email protected]>
  • Loading branch information
3 people authored Sep 25, 2023
1 parent af7d3b3 commit 8c4b5b3
Show file tree
Hide file tree
Showing 14 changed files with 551 additions and 112 deletions.
11 changes: 11 additions & 0 deletions web/src/beta/components/fields/PropertyFields/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SliderField from "@reearth/beta/components/fields/SliderField";
import SpacingInput, { type SpacingValues } from "@reearth/beta/components/fields/SpacingInput";
import TextInput from "@reearth/beta/components/fields/TextField";
import ToggleField from "@reearth/beta/components/fields/ToggleField";
import URLField from "@reearth/beta/components/fields/URLField";
import type { FlyTo } from "@reearth/beta/lib/core/types";
import type { Camera, LatLng } from "@reearth/beta/utils/value";
import type { Item } from "@reearth/services/api/propertyApi/utils";
Expand Down Expand Up @@ -65,6 +66,16 @@ const PropertyFields: React.FC<Props> = ({ propertyId, item, currentCamera, onFl
onChange={handleChange}
/>
)
) : sf.type === "url" ? (
<URLField
key={sf.id}
name={sf.name}
assetType={sf.ui === "image" ? "image" : sf.ui === "file" ? "file" : undefined}
fileType={sf.ui === "video" || sf.ui === undefined ? "URL" : "asset"}
value={(value as string) ?? ""}
description={sf.description}
onChange={handleChange}
/>
) : sf.type === "spacing" ? (
<SpacingInput
key={sf.id}
Expand Down
36 changes: 36 additions & 0 deletions web/src/beta/components/fields/URLField/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Meta, StoryObj } from "@storybook/react";

import URLField, { Props } from ".";

const meta: Meta<Props> = {
component: URLField,
};

export default meta;
type Story = StoryObj<typeof URLField>;

export const AssetImageType: Story = {
args: {
name: "Asset",
description: "Defaul Asset Uploader",
fileType: "asset",
assetType: "image",
},
};

export const AssetFileType: Story = {
args: {
name: "Asset",
description: "Defaul Asset Uploader",
fileType: "asset",
assetType: "file",
},
};

export const URLType: Story = {
args: {
name: "URL",
description: "Defaul URL Input wrapper",
fileType: "URL",
},
};
176 changes: 176 additions & 0 deletions web/src/beta/components/fields/URLField/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import React, { useCallback, useEffect, useState } from "react";

import Button from "@reearth/beta/components/Button";
import Property from "@reearth/beta/components/fields";
import TextInput from "@reearth/beta/components/fields/common/TextInput";
import useHooks from "@reearth/beta/features/Assets/AssetsQueriesHook/hooks";
import {
FILE_FORMATS,
IMAGE_FORMATS,
VIDEO_FORMATS,
} from "@reearth/beta/features/Assets/constants";
import { Asset } from "@reearth/beta/features/Assets/types";
import { useManageAssets } from "@reearth/beta/features/Assets/useManageAssets/hooks";
import ChooseAssetModal from "@reearth/beta/features/Modals/ChooseAssetModal";
import { checkIfFileType } from "@reearth/beta/utils/util";
import { useT } from "@reearth/services/i18n";
import { useNotification, useWorkspace } from "@reearth/services/state";
import { styled } from "@reearth/services/theme";

export type Props = {
value?: string;
onChange?: (value: string | undefined) => void;
name?: string;
description?: string;
fileType?: "asset" | "URL";
assetType?: "image" | "file";
};

const URLField: React.FC<Props> = ({ name, description, value, fileType, assetType, onChange }) => {
const t = useT();
const [open, setOpen] = useState(false);
const [currentWorkspace] = useWorkspace();
const [, setNotification] = useNotification();
const [currentValue, setCurrentValue] = useState(value);

const handleChange = useCallback(
(inputValue?: string) => {
if (!inputValue) return;

if (
fileType === "asset" &&
!(checkIfFileType(inputValue, FILE_FORMATS) || checkIfFileType(inputValue, IMAGE_FORMATS))
) {
setNotification({
type: "error",
text: t("Wrong file format"),
});
setCurrentValue(undefined);
return;
} else if (fileType === "URL" && !checkIfFileType(inputValue, VIDEO_FORMATS)) {
setNotification({
type: "error",
text: t("wrong Video URL Format"),
});
setCurrentValue(undefined);
return;
}

setCurrentValue(inputValue);
onChange?.(inputValue);
},
[fileType, onChange, setNotification, t],
);

const {
assets,
isLoading,
hasMoreAssets,
searchTerm,
selectedAssets,
selectAsset,
handleGetMoreAssets,
handleFileSelect,
handleSortChange,
handleSearchTerm,
} = useHooks({ workspaceId: currentWorkspace?.id, onAssetSelect: handleChange });

const { localSearchTerm, wrapperRef, onScrollToBottom, handleSearchInputChange, handleSearch } =
useManageAssets({
selectedAssets,
searchTerm,
isLoading,
hasMoreAssets,
onGetMore: handleGetMoreAssets,
onSortChange: handleSortChange,
onSearch: handleSearchTerm,
});

const handleReset = useCallback(() => {
const selectedAsset = assets?.find(a => a.url === currentValue);
if (selectedAsset) {
selectAsset([selectedAsset]);
}
}, [currentValue, assets, selectAsset]);

useEffect(() => {
if (value) {
setCurrentValue(value);
}
}, [value]);

useEffect(() => {
handleReset();
}, [handleReset]);

const handleClose = useCallback(() => {
setOpen(false);
handleReset();
}, [handleReset]);

const handleClick = useCallback(() => setOpen(!open), [open]);

const handleSelect = useCallback(
(asset?: Asset) => {
if (!asset) return;
selectAsset(!selectedAssets.includes(asset) ? [asset] : []);
},
[selectedAssets, selectAsset],
);

return (
<Property name={name} description={description}>
<TextInput value={currentValue} onChange={handleChange} placeholder={t("Not set")} />
{fileType === "asset" && (
<ButtonWrapper>
<AssetButton
icon={assetType === "image" ? "imageStoryBlock" : "file"}
text={t("Choose")}
iconPosition="left"
onClick={handleClick}
/>
<AssetButton
icon="uploadSimple"
text={t("Upload")}
iconPosition="left"
onClick={handleFileSelect}
/>
</ButtonWrapper>
)}
{open && (
<ChooseAssetModal
open={open}
assetType={assetType}
localSearchTerm={localSearchTerm}
selectedAssets={selectedAssets}
wrapperRef={wrapperRef}
assets={assets}
isLoading={isLoading}
hasMoreAssets={hasMoreAssets}
searchTerm={searchTerm}
onClose={handleClose}
handleSearch={handleSearch}
handleSearchInputChange={handleSearchInputChange}
onGetMore={handleGetMoreAssets}
onScrollToBottom={onScrollToBottom}
onSelectAsset={handleSelect}
onSelect={handleChange}
/>
)}
</Property>
);
};

const AssetButton = styled(Button)<{ active?: boolean }>`
cursor: pointer;
padding: 4px;
flex: 1;
`;

const ButtonWrapper = styled.div`
display: flex;
align-items: space-between;
gap: 4px;
`;

export default URLField;
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import { useCallback, useState, useRef } from "react";
import useFileInput from "use-file-input";

import { Asset, SortType } from "@reearth/beta/features/Assets/types";
import { useAssetsFetcher } from "@reearth/services/api";
import { Maybe, AssetSortType as GQLSortType } from "@reearth/services/gql";

export type AssetSortType = "date" | "name" | "size";

export type Asset = {
id: string;
teamId: string;
name: string;
size: number;
url: string;
contentType: string;
};
import { FILE_FORMATS, IMAGE_FORMATS } from "../constants";

const assetsPerPage = 20;

Expand All @@ -22,13 +15,13 @@ const enumTypeMapper: Partial<Record<GQLSortType, string>> = {
[GQLSortType.Size]: "size",
};

function toGQLEnum(val?: AssetSortType) {
function toGQLEnum(val?: SortType) {
if (!val) return;
return (Object.keys(enumTypeMapper) as GQLSortType[]).find(k => enumTypeMapper[k] === val);
}

function pagination(
sort?: { type?: Maybe<AssetSortType>; reverse?: boolean },
sort?: { type?: Maybe<SortType>; reverse?: boolean },
endCursor?: string | null,
) {
const reverseOrder = !sort?.type || sort?.type === "date" ? !sort?.reverse : !!sort?.reverse;
Expand All @@ -41,8 +34,14 @@ function pagination(
};
}

export default ({ workspaceId }: { workspaceId?: string }) => {
const [sort, setSort] = useState<{ type?: AssetSortType; reverse?: boolean }>();
export default ({
workspaceId,
onAssetSelect,
}: {
workspaceId?: string;
onAssetSelect?: (inputValue?: string) => void;
}) => {
const [sort, setSort] = useState<{ type?: SortType; reverse?: boolean }>();
const [searchTerm, setSearchTerm] = useState<string>();
const [selectedAssets, selectAsset] = useState<Asset[]>([]);
const isGettingMore = useRef(false);
Expand All @@ -68,12 +67,15 @@ export default ({ workspaceId }: { workspaceId?: string }) => {
}
}, [endCursor, sort, fetchMore, hasMoreAssets, isGettingMore]);

const createAssets = useCallback(
(files: FileList) => {
const handleAssetsCreate = useCallback(
async (files?: FileList) => {
if (!files) return;
useCreateAssets({ teamId: workspaceId ?? "", file: files });
const result = await useCreateAssets({ teamId: workspaceId ?? "", file: files });
const assetUrl = result?.data[0].data?.createAsset?.asset.url;

onAssetSelect?.(assetUrl);
},
[workspaceId, useCreateAssets],
[workspaceId, useCreateAssets, onAssetSelect],
);

const removeAssets = useCallback(
Expand All @@ -90,7 +92,7 @@ export default ({ workspaceId }: { workspaceId?: string }) => {
(type?: string, reverse?: boolean) => {
if (!type && reverse === undefined) return;
setSort({
type: (type as AssetSortType) ?? sort?.type,
type: (type as SortType) ?? sort?.type,
reverse: !!reverse,
});
},
Expand All @@ -101,6 +103,11 @@ export default ({ workspaceId }: { workspaceId?: string }) => {
setSearchTerm(term);
}, []);

const handleFileSelect = useFileInput(files => handleAssetsCreate?.(files), {
accept: IMAGE_FORMATS + "," + FILE_FORMATS,
multiple: true,
});

return {
assets,
isLoading: loading ?? isRefetching,
Expand All @@ -110,7 +117,7 @@ export default ({ workspaceId }: { workspaceId?: string }) => {
selectedAssets,
selectAsset,
handleGetMoreAssets,
createAssets,
handleFileSelect,
removeAssets,
handleSortChange,
handleSearchTerm,
Expand Down
5 changes: 5 additions & 0 deletions web/src/beta/features/Assets/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const FILE_FORMATS = ".kml,.czml,.topojson,.geojson,.json,.gltf,.glb";

export const IMAGE_FORMATS = ".jpg,.jpeg,.png,.gif,.svg,.tiff,.webp";

export const VIDEO_FORMATS = ".mp4";
10 changes: 10 additions & 0 deletions web/src/beta/features/Assets/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type Asset = {
id: string;
teamId: string;
name: string;
size: number;
url: string;
contentType: string;
};

export type SortType = "date" | "name" | "size";
Loading

0 comments on commit 8c4b5b3

Please sign in to comment.