Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: copy path button #181

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
74 changes: 74 additions & 0 deletions app/components/CopyPathButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ClipboardIcon, CogIcon } from "@heroicons/react/outline";
import { useCallback, useState } from "react";
import { CopyText } from "./CopyText";
import { Body } from "./Primitives/Body";
import { Dialog, DialogTrigger, DialogContent } from "./UI/Dialog";
import { PathComponent } from '@jsonhero/path'
import { getPathValue, languages } from "~/utilities/programmingLanguages";
import classnames from "~/utilities/classnames";
import { CopyPathPreferences } from "./CopyPathPreferences";
import { usePreferences } from "./PreferencesProvider";

export type CopyPathButtonProps = {
heroPathComponents: PathComponent[];
className?: string;
};

export function CopyPathButton({ heroPathComponents, className }: CopyPathButtonProps) {
const [copied, setCopied] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false)
const [preferences] = usePreferences()
const [variableName, setVariableName] = useState("");
const [useOptChaining, setUseOptionalChaining] = useState(false);

const onCopied = useCallback(() => {
setCopied(true);
const timeout = setTimeout(() => {
setCopied(false);
}, 1500);
}, [heroPathComponents]);
return (
<>
<CopyText className={className} value={getPathValue(preferences?.language || languages.javascript, variableName, useOptChaining, heroPathComponents)} onCopied={onCopied}>
{copied ? (
<Body>Copied!</Body>
) : (
<div className="flex items-center">
<ClipboardIcon className="h-4 w-4 mr-[2px]" />
<Body>Copy Path</Body>
</div>
)}
</CopyText>
<Dialog open={settingsOpen}>
<DialogTrigger
className="focus:outline-none focus-visible:outline-none"
onClick={() => setSettingsOpen(true)}
>
<div className={`${className}`}>
<div className="flex items-center">
<CogIcon className="h-4 w-4 -mr-[2px] mt-px"/>
<Body>&nbsp;</Body>
</div>
</div>
</DialogTrigger>
<DialogContent
onOverlayClick={() => setSettingsOpen(false)}
className={classnames(
"fixed z-50",
"w-[95vw] max-w-2xl rounded-lg",
"top-0 left-[50%] -translate-x-[50%]",
"mt-[60px]",
"bg-white border-[1px] border-slate-500 dark:border-slate-700 dark:bg-slate-800"
)}
>
<CopyPathPreferences
variableName={variableName}
onVariableNameChange={setVariableName}
useOptChaining={useOptChaining}
onUseOptionalChainingChange={setUseOptionalChaining}
/>
</DialogContent>
</Dialog>
</>
);
}
76 changes: 76 additions & 0 deletions app/components/CopyPathPreferences.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { languages, canUseOptChaining } from "~/utilities/programmingLanguages";
import { usePreferences } from "./PreferencesProvider";
import { useState } from "react";

export type CopyPathPreferencesProps = {
variableName: string;
onVariableNameChange: (value: string) => void;
useOptChaining: boolean;
onUseOptionalChainingChange: (value: boolean) => void;
};

export function CopyPathPreferences({
variableName,
onVariableNameChange,
useOptChaining,
onUseOptionalChainingChange,
}: CopyPathPreferencesProps) {
const [preferences, setPreferences] = usePreferences();

return (
<div className="p-4 flex flex-col space-y-4">
<h1 className="text-2xl text-gray-800 dark:text-white">
Copy Path Preferences
</h1>
<div className="grid grid-cols-2 gap-4">
<label htmlFor="language" className="text-gray-800 dark:text-white">
Language
</label>
<select
className="py-1 px-2 rounded-md bg-gray-300 dark:bg-gray-800 text-gray-700 dark:text-gray-400 hover:bg-gray-400 hover:dark:bg-gray-700"
defaultValue={preferences.language}
onChange={(e) =>
setPreferences({
...preferences,
language: e.target.value as languages,
})
}
>
{Object.values(languages).map((language) => (
<option key={language} value={language}>
{language}
</option>
))}
</select>
<label
htmlFor="variable-name"
className="text-gray-800 dark:text-white"
>
Variable Name
</label>
<input
type="text"
className="py-1 px-2 rounded-md bg-gray-300 dark:bg-gray-800 text-gray-700 dark:text-gray-400 hover:bg-gray-400 hover:dark:bg-gray-700"
defaultValue={variableName}
onChange={(e) => onVariableNameChange(e.target.value)}
/>
{canUseOptChaining[preferences.language || languages.javascript] && (
<>
<label
htmlFor="use-optional-chaining"
className="text-gray-800 dark:text-white"
>
Use Optional Chaining
</label>
<input
type="checkbox"
className="rounded-md bg-gray-300 dark:bg-gray-800 text-gray-700 dark:text-gray-400 hover:bg-gray-400 hover:dark:bg-gray-700"
checked={useOptChaining}
onChange={(e) => onUseOptionalChainingChange(e.target.checked)}
/>
</>
)}
</div>
</div>
);
}
30 changes: 23 additions & 7 deletions app/components/InfoHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { concatenated, getHierarchicalTypes } from "~/utilities/dataType";
import { formatRawValue } from "~/utilities/formatter";
import { isNullable } from "~/utilities/nullable";
import { CopyTextButton } from "./CopyTextButton";
import { CopyPathButton } from "./CopyPathButton";
import { Body } from "./Primitives/Body";
import { LargeMono } from "./Primitives/LargeMono";
import { Title } from "./Primitives/Title";
Expand Down Expand Up @@ -45,7 +46,8 @@ export function InfoHeader({ relatedPaths }: InfoHeaderProps) {
return isNullable(relatedPaths, json);
}, [relatedPaths, json]);

const [hovering, setHovering] = useState(false);
const [hoveringKey, setHoveringKey] = useState(false);
const [hoveringValue, setHoveringValue] = useState(false);
console.warn(selectedInfo);

const newPath = formattedSelectedInfo.replace(/^#/, "$").replace(/\//g, ".");
Expand All @@ -56,10 +58,24 @@ export function InfoHeader({ relatedPaths }: InfoHeaderProps) {

return (
<div className="mb-4 pb-4">
<div className="flex items-center">
<Title className="flex-1 mr-2 overflow-hidden overflow-ellipsis break-words text-slate-700 transition dark:text-slate-200">
<div
className="relative flex items-center"
onMouseEnter={() => setHoveringKey(true)}
onMouseLeave={() => setHoveringKey(false)}
>
<Title className={`flex-1 mr-2 overflow-hidden overflow-ellipsis break-words text-slate-700 transition dark:text-slate-200 ${
hoveringKey ? "bg-slate-100 dark:bg-slate-700" : "bg-transparent"
}`}>
{ selectedName ?? "nothing" }
</Title>
<div className={`absolute top-0 right-8 flex justify-end h-full w-fit transition ${
hoveringKey ? "opacity-100" : "opacity-0"
}`}>
<CopyPathButton
className="bg-slate-200 hover:bg-slate-300 h-fit mr-1 px-2 py-0.5 rounded-sm transition hover:cursor-pointer dark:text-white dark:bg-slate-600 dark:hover:bg-slate-500"
heroPathComponents={selectedHeroPath.components}
/>
</div>
<div>
<ValueIcon
monochrome
Expand All @@ -70,13 +86,13 @@ export function InfoHeader({ relatedPaths }: InfoHeaderProps) {
</div>
<div
className="relative w-full h-full"
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
onMouseEnter={() => setHoveringValue(true)}
onMouseLeave={() => setHoveringValue(false)}
>
{isSelectedLeafNode && (
<LargeMono
className={`z-10 py-1 mb-1 text-slate-800 overflow-ellipsis break-words transition rounded-sm dark:text-slate-300 ${
hovering ? "bg-slate-100 dark:bg-slate-700" : "bg-transparent"
hoveringValue ? "bg-slate-100 dark:bg-slate-700" : "bg-transparent"
}`}
>
{selectedNode.name === "$ref" && checkPathExists(json, newPath) ? (
Expand All @@ -90,7 +106,7 @@ export function InfoHeader({ relatedPaths }: InfoHeaderProps) {
)}
<div
className={`absolute top-1 right-0 flex justify-end h-full w-fit transition ${
hovering ? "opacity-100" : "opacity-0"
hoveringValue ? "opacity-100" : "opacity-0"
}`}
>
<CopyTextButton
Expand Down
13 changes: 9 additions & 4 deletions app/components/PreferencesProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import {createContext, Dispatch, ReactNode, SetStateAction, useContext, useEffect, useState} from 'react';
import { languages } from '~/utilities/programmingLanguages';

interface Preferences {
indent: number;
language: languages;
}

const PreferencesDefaults: Preferences = {
indent: 2,
language: languages.javascript
};

type PartialPreferences = Partial<Preferences>;

type PreferencesContextType = [
Preferences | undefined,
Dispatch<SetStateAction<Preferences | undefined>>
PartialPreferences,
Dispatch<SetStateAction<PartialPreferences>>
];

const PreferencesContext = createContext<PreferencesContextType | undefined>(undefined);
Expand All @@ -30,7 +35,7 @@ export function PreferencesProvider({
}: {
children: ReactNode;
}) {
const [preferences, setPreferences] = useState<Preferences>();
const [preferences, setPreferences] = useState<PartialPreferences>(PreferencesDefaults);

useEffect(() => {
const preferences = loadPreferences();
Expand All @@ -39,7 +44,7 @@ export function PreferencesProvider({

useEffect(() => {
if (preferences === undefined) return;
savePreferences(preferences);
savePreferences(preferences as Preferences);
}, [preferences]);

return (
Expand Down
25 changes: 25 additions & 0 deletions app/utilities/programmingLanguages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { PathComponent } from "@jsonhero/path"

export enum languages {
javascript = "javascript",
python = "python"
}

export const canUseOptChaining = {
[languages.javascript]: true,
[languages.python]: false,
}

export const defaultLangauge = languages.javascript

export const getPathValue = (language: languages, variableName: string, useOptChaining: boolean, paths: PathComponent[]): string => {
const path_base = variableName
switch(language) {
case languages.python:
return `${path_base}${paths.slice(1).map(p => p.isArray ? `[${p.toString()}]` : `["${p.toString()}"]`).join("")}`
case languages.javascript:
default:
return `${path_base}${paths.slice(1).map(p => p.isArray ? `${useOptChaining ? "?[" : "["}${p.toString()}]` : `${useOptChaining ? "?." : "."}${p.toString()}`).join("")}`
}
}