Move workspace menu, better env mgmt, QoL

This commit is contained in:
Gregory Schier
2023-10-29 09:45:16 -07:00
parent b59ea4991c
commit a87ca6af47
19 changed files with 214 additions and 104 deletions

View File

@@ -4,12 +4,13 @@ import { Icon } from './Icon';
interface Props {
checked: boolean;
title: string;
onChange: (checked: boolean) => void;
disabled?: boolean;
className?: string;
}
export function Checkbox({ checked, onChange, className, disabled }: Props) {
export function Checkbox({ checked, onChange, className, disabled, title }: Props) {
const handleClick = useCallback(() => {
onChange(!checked);
}, [onChange, checked]);
@@ -20,6 +21,7 @@ export function Checkbox({ checked, onChange, className, disabled }: Props) {
aria-checked={checked ? 'true' : 'false'}
disabled={disabled}
onClick={handleClick}
title={title}
className={classNames(
className,
'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',

View File

@@ -11,7 +11,7 @@ export interface DialogProps {
children: ReactNode;
open: boolean;
onClose: () => void;
title: ReactNode;
title?: ReactNode;
description?: ReactNode;
className?: string;
size?: 'sm' | 'md' | 'full' | 'dynamic';
@@ -63,9 +63,13 @@ export function Dialog({
size === 'dynamic' && 'min-w-[30vw] max-w-[80vw]',
)}
>
<Heading className="text-xl font-semibold w-full" id={titleId}>
{title}
</Heading>
{title ? (
<Heading className="text-xl font-semibold w-full" id={titleId}>
{title}
</Heading>
) : (
<span />
)}
{description && <p id={descriptionId}>{description}</p>}
<div className="h-full w-full">{children}</div>
{/*Put close at the end so that it's the last thing to be tabbed to*/}

View File

@@ -41,6 +41,7 @@ export interface EditorProps {
wrapLines?: boolean;
format?: (v: string) => string;
autocomplete?: GenericCompletionConfig;
autocompleteVariables?: boolean;
actions?: ReactNode;
}
@@ -64,12 +65,14 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
singleLine,
format,
autocomplete,
autocompleteVariables,
actions,
wrapLines,
}: EditorProps,
ref,
) {
const environment = useActiveEnvironment();
const e = useActiveEnvironment();
const environment = autocompleteVariables ? e : null;
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
useImperativeHandle(ref, () => cm.current?.view);

View File

@@ -37,7 +37,6 @@ import { text } from './text/extension';
import { twig } from './twig/extension';
import { url } from './url/extension';
import type { Environment } from '../../../lib/models';
import { EditorView } from 'codemirror';
export const myHighlightStyle = HighlightStyle.define([
{

View File

@@ -8,7 +8,7 @@ import { IconButton } from './IconButton';
import { HStack, VStack } from './Stacks';
export type InputProps = Omit<HTMLAttributes<HTMLInputElement>, 'onChange' | 'onFocus'> &
Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete' | 'forceUpdateKey' | 'autoFocus' | 'autoSelect'> & {
Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete' | 'forceUpdateKey' | 'autoFocus' | 'autoSelect' | 'autocompleteVariables'> & {
name: string;
type?: 'text' | 'password';
label: string;

View File

@@ -10,6 +10,7 @@ import { Icon } from './Icon';
import { IconButton } from './IconButton';
import type { InputProps } from './Input';
import { Input } from './Input';
import type { EditorView } from 'codemirror';
export type PairEditorProps = {
pairs: Pair[];
@@ -20,6 +21,8 @@ export type PairEditorProps = {
valuePlaceholder?: string;
nameAutocomplete?: GenericCompletionConfig;
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
nameAutocompleteVariables?: boolean;
valueAutocompleteVariables?: boolean;
nameValidate?: InputProps['validate'];
valueValidate?: InputProps['validate'];
};
@@ -37,17 +40,20 @@ type PairContainer = {
};
export const PairEditor = memo(function PairEditor({
pairs: originalPairs,
className,
forceUpdateKey,
nameAutocomplete,
valueAutocomplete,
nameAutocompleteVariables,
namePlaceholder,
valuePlaceholder,
nameValidate,
valueValidate,
className,
onChange,
pairs: originalPairs,
valueAutocomplete,
valueAutocompleteVariables,
valuePlaceholder,
valueValidate,
}: PairEditorProps) {
const [forceFocusPairId, setForceFocusPairId] = useState<string | null>(null);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [pairs, setPairs] = useState<PairContainer[]>(() => {
// Remove empty headers on initial render
@@ -105,6 +111,15 @@ export const PairEditor = memo(function PairEditor({
[hoveredIndex, setPairsAndSave],
);
const handleSubmitRow = useCallback(
(pair: PairContainer) => {
const index = pairs.findIndex((p) => p.id === pair.id);
const id = pairs[index + 1]?.id ?? null;
setForceFocusPairId(id);
},
[pairs],
);
const handleChange = useCallback(
(pair: PairContainer) =>
setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))),
@@ -152,6 +167,9 @@ export const PairEditor = memo(function PairEditor({
pairContainer={p}
className="py-1"
isLast={isLast}
nameAutocompleteVariables={nameAutocompleteVariables}
valueAutocompleteVariables={valueAutocompleteVariables}
forceFocusPairId={forceFocusPairId}
forceUpdateKey={forceUpdateKey}
nameAutocomplete={nameAutocomplete}
valueAutocomplete={valueAutocomplete}
@@ -159,6 +177,7 @@ export const PairEditor = memo(function PairEditor({
valuePlaceholder={valuePlaceholder}
nameValidate={nameValidate}
valueValidate={valueValidate}
onSubmit={handleSubmitRow}
onChange={handleChange}
onFocus={handleFocus}
onDelete={isLast ? undefined : handleDelete}
@@ -179,16 +198,20 @@ enum ItemTypes {
type FormRowProps = {
className?: string;
pairContainer: PairContainer;
forceFocusPairId?: string | null;
onMove: (id: string, side: 'above' | 'below') => void;
onEnd: (id: string) => void;
onChange: (pair: PairContainer) => void;
onDelete?: (pair: PairContainer) => void;
onFocus?: (pair: PairContainer) => void;
onSubmit?: (pair: PairContainer) => void;
isLast?: boolean;
} & Pick<
PairEditorProps,
| 'nameAutocomplete'
| 'valueAutocomplete'
| 'nameAutocompleteVariables'
| 'valueAutocompleteVariables'
| 'namePlaceholder'
| 'valuePlaceholder'
| 'nameValidate'
@@ -198,23 +221,34 @@ type FormRowProps = {
const FormRow = memo(function FormRow({
className,
pairContainer,
forceFocusPairId,
forceUpdateKey,
isLast,
nameAutocomplete,
namePlaceholder,
nameAutocompleteVariables,
valueAutocompleteVariables,
nameValidate,
onChange,
onDelete,
onEnd,
onFocus,
onMove,
onEnd,
isLast,
forceUpdateKey,
nameAutocomplete,
onSubmit,
pairContainer,
valueAutocomplete,
namePlaceholder,
valuePlaceholder,
nameValidate,
valueValidate,
}: FormRowProps) {
const { id } = pairContainer;
const ref = useRef<HTMLDivElement>(null);
const nameInputRef = useRef<EditorView>(null);
useEffect(() => {
if (forceFocusPairId === pairContainer.id) {
nameInputRef.current?.focus();
}
}, [forceFocusPairId, pairContainer.id]);
const handleChangeEnabled = useMemo(
() => (enabled: boolean) => onChange({ id, pair: { ...pairContainer.pair, enabled } }),
@@ -237,7 +271,7 @@ const FormRow = memo(function FormRow({
const [, connectDrop] = useDrop<PairContainer>(
{
accept: ItemTypes.ROW,
hover: (item, monitor) => {
hover: (_, monitor) => {
if (!ref.current) return;
const hoverBoundingRect = ref.current?.getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
@@ -285,12 +319,18 @@ const FormRow = memo(function FormRow({
<span className="w-3" />
)}
<Checkbox
title={pairContainer.pair.enabled ? 'disable entry' : 'Enable item'}
disabled={isLast}
checked={isLast ? false : !!pairContainer.pair.enabled}
className={classNames('mr-2', isLast && '!opacity-disabled')}
onChange={handleChangeEnabled}
/>
<div
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
onSubmit?.(pairContainer);
}}
className={classNames(
'grid items-center',
'@xs:gap-2 @xs:!grid-rows-1 @xs:!grid-cols-[minmax(0,1fr)_minmax(0,1fr)]',
@@ -298,11 +338,12 @@ const FormRow = memo(function FormRow({
)}
>
<Input
ref={nameInputRef}
hideLabel
useTemplating
size="sm"
require={!isLast && !!pairContainer.pair.enabled && !!pairContainer.pair.value}
validate={nameValidate}
useTemplating
forceUpdateKey={forceUpdateKey}
containerClassName={classNames(isLast && 'border-dashed')}
defaultValue={pairContainer.pair.name}
@@ -312,9 +353,11 @@ const FormRow = memo(function FormRow({
onFocus={handleFocus}
placeholder={namePlaceholder ?? 'name'}
autocomplete={nameAutocomplete}
autocompleteVariables={nameAutocompleteVariables}
/>
<Input
hideLabel
useTemplating
size="sm"
containerClassName={classNames(isLast && 'border-dashed')}
validate={valueValidate}
@@ -325,10 +368,10 @@ const FormRow = memo(function FormRow({
onChange={handleChangeValue}
onFocus={handleFocus}
placeholder={valuePlaceholder ?? 'value'}
useTemplating
autocomplete={valueAutocomplete?.(pairContainer.pair.name)}
autocompleteVariables={valueAutocompleteVariables}
/>
</div>
</form>
<IconButton
aria-hidden={!onDelete}
disabled={!onDelete}