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

Handle renaming conflicts #195

Merged
merged 13 commits into from
Jan 8, 2025
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
8 changes: 6 additions & 2 deletions src/actions/patchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { AppSetting } from "../models/settings";
import { DataRefRecord } from "../models/dataref";
import { DataFileRecord } from "../models/datafile";
import { PatcherExportRecord } from "../models/patcher";
import { cloneJSON, InvalidMIDIFormatError, parseMIDIMappingDisplayValue, UnknownMIDIFormatError } from "../lib/util";
import { cloneJSON, getUniqueName, InvalidMIDIFormatError, parseMIDIMappingDisplayValue, UnknownMIDIFormatError } from "../lib/util";
import { MIDIMetaMappingType } from "../lib/constants";

export enum PatcherActionType {
Expand Down Expand Up @@ -355,9 +355,13 @@ export const loadPresetOnRemoteInstance = (instance: PatcherInstanceRecord, pres
}
};

export const savePresetToRemoteInstance = (instance: PatcherInstanceRecord, name: string): AppThunk =>
export const savePresetToRemoteInstance = (instance: PatcherInstanceRecord, givenName: string, ensureUniqueName: boolean = true): AppThunk =>
(dispatch) => {
try {
const name = ensureUniqueName
? getUniqueName(givenName, instance.presets.valueSeq().map(p => p.name).toArray())
: givenName;

const message = {
address: `${instance.path}/presets/save`,
args: [
Expand Down
32 changes: 22 additions & 10 deletions src/actions/sets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { updateSetMetaOnRemoteFromNodes } from "./meta";
import { NodeType } from "../models/graph";
import { getNodes } from "../selectors/graph";
import { ParameterRecord } from "../models/parameter";
import { getPatcherInstance, getPatcherInstanceParamtersSortedByInstanceIdAndIndex } from "../selectors/patchers";
import { getPatcherInstance, getPatcherInstanceParametersSortedByInstanceIdAndIndex } from "../selectors/patchers";
import { OSCQueryRNBOSetView, OSCQueryRNBOSetViewListState } from "../lib/types";
import { getGraphSetView, getGraphSetViews } from "../selectors/sets";
import { getGraphPresets, getGraphSets, getGraphSetView, getGraphSetViews } from "../selectors/sets";
import { clamp, getUniqueName, instanceAndParamIndicesToSetViewEntry } from "../lib/util";
import { setInstanceWaitingForMidiMappingOnRemote } from "./patchers";

Expand Down Expand Up @@ -157,9 +157,15 @@ export const loadGraphSetOnRemote = (set: GraphSetRecord): AppThunk =>
}
};

export const saveGraphSetOnRemote = (name: string): AppThunk =>
(dispatch) => {
export const saveGraphSetOnRemote = (givenName: string, ensureUniqueName: boolean = true): AppThunk =>
(dispatch, getState) => {
try {

const graphSets = getGraphSets(getState());
const name = ensureUniqueName
? getUniqueName(givenName, graphSets.valueSeq().map(s => s.name).toArray())
: givenName;

const message = {
address: "/rnbo/inst/control/sets/save",
args: [
Expand Down Expand Up @@ -247,9 +253,15 @@ export const loadSetPresetOnRemote = (preset: PresetRecord): AppThunk =>
}
};

export const saveSetPresetToRemote = (name: string): AppThunk =>
(dispatch) => {
export const saveSetPresetToRemote = (givenName: string, ensureUniqueName: boolean = true): AppThunk =>
(dispatch, getState) => {
try {

const setPresets = getGraphPresets(getState());
const name = ensureUniqueName
? getUniqueName(givenName, setPresets.valueSeq().map(p => p.name).toArray())
: givenName;

const message = {
address: "/rnbo/inst/control/sets/presets/save",
args: [
Expand Down Expand Up @@ -338,7 +350,7 @@ export const createSetViewOnRemote = (givenName: string): AppThunk =>
(dispatch, getState) => {
try {
const state = getState();
const params = getPatcherInstanceParamtersSortedByInstanceIdAndIndex(state);
const params = getPatcherInstanceParametersSortedByInstanceIdAndIndex(state);
const existingViews = getGraphSetViews(state);
const name = getUniqueName(givenName, existingViews.valueSeq().map(v => v.name).toArray());

Expand Down Expand Up @@ -532,7 +544,7 @@ export const removeParameterFromSetView = (setView: GraphSetViewRecord, param: P
}
};

export const removeAllParamtersFromSetView = (setView: GraphSetViewRecord): AppThunk =>
export const removeAllParametersFromSetView = (setView: GraphSetViewRecord): AppThunk =>
(dispatch) => {
try {
const message = {
Expand Down Expand Up @@ -572,12 +584,12 @@ export const addParameterToSetView = (setView: GraphSetViewRecord, param: Parame
}
};

export const addAllParamtersToSetView = (setView: GraphSetViewRecord): AppThunk =>
export const addAllParametersToSetView = (setView: GraphSetViewRecord): AppThunk =>
(dispatch, getState) => {
try {
const state = getState();
const params = setView.params.withMutations(list => {
getPatcherInstanceParamtersSortedByInstanceIdAndIndex(state)
getPatcherInstanceParametersSortedByInstanceIdAndIndex(state)
.forEach(param => {
if (!setView.paramIds.has(param.setViewId)) {
list.push({ instanceId: param.instanceId, paramIndex: param.index });
Expand Down
23 changes: 20 additions & 3 deletions src/components/presets/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export type PresetDrawerProps = {
onClose: () => any;
onDeletePreset: (preset: PresetRecord) => any;
onLoadPreset: (preset: PresetRecord) => any;
onSavePreset: (name: string) => any;
onCreatePreset: (name: string) => any;
onSavePreset: (preset: PresetRecord) => any;
onRenamePreset: (preset: PresetRecord, name: string) => any;
onSetInitialPreset?: (set: PresetRecord) => any;
presets: Seq.Indexed<PresetRecord>;
Expand All @@ -23,6 +24,7 @@ export type PresetDrawerProps = {
const PresetDrawer: FunctionComponent<PresetDrawerProps> = memo(function WrappedPresetDrawer({
open,
onClose,
onCreatePreset,
onDeletePreset,
onLoadPreset,
onSavePreset,
Expand All @@ -46,19 +48,34 @@ const PresetDrawer: FunctionComponent<PresetDrawerProps> = memo(function Wrapped
});
}, [onDeletePreset]);

const validateUniquePresetName = useCallback((name: string): boolean => {
return !presets.find(p => p.name === name);
}, [presets]);

return (
<Drawer
opened={ open }
onClose={ onClose }
position="right"
title={ <Group gap="xs"><IconElement path={ mdiCamera }/> Presets</Group> }
>
<SavePresetForm onSave={ onSavePreset } />
<SavePresetForm onSave={ onCreatePreset } />
<Divider mt="lg" />
<DrawerSectionTitle>Saved Presets</DrawerSectionTitle>
<Stack gap="sm">
{
presets.map(preset => <PresetItem key={ preset.id } preset={ preset } onLoad={ onLoadPreset } onDelete={ onTriggerDeletePreset } onRename = { onRenamePreset } onSetInitial = { onSetInitialPreset }/> )
presets.map(preset => (
<PresetItem
key={ preset.id }
preset={ preset }
onLoad={ onLoadPreset }
onDelete={ onTriggerDeletePreset }
onRename={ onRenamePreset }
onSave={ onSavePreset }
onSetInitial = { onSetInitialPreset }
validateUniqueName={ validateUniquePresetName }
/>
))
}
</Stack>
</Drawer>
Expand Down
43 changes: 34 additions & 9 deletions src/components/presets/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,26 @@ import classes from "./presets.module.css";
import { PresetRecord } from "../../models/preset";
import { keyEventIsValidForName, replaceInvalidNameChars } from "../../lib/util";
import { IconElement } from "../elements/icon";
import { mdiCheck, mdiClose, mdiDotsVertical, mdiHistory, mdiPencil, mdiStar, mdiTrashCan } from "@mdi/js";
import { mdiCheck, mdiClose, mdiContentSave, mdiDotsVertical, mdiHistory, mdiPencil, mdiStar, mdiTrashCan } from "@mdi/js";

export type PresetItemProps = {
preset: PresetRecord;
onDelete: (set: PresetRecord) => any;
onLoad: (set: PresetRecord) => any;
onRename: (set: PresetRecord, name: string) => any;
onSetInitial?: (set: PresetRecord) => any;
onDelete: (preset: PresetRecord) => any;
onLoad: (preset: PresetRecord) => any;
onRename: (preset: PresetRecord, name: string) => any;
onSave: (preset: PresetRecord) => any;
onSetInitial?: (preset: PresetRecord) => any;
validateUniqueName: (name: string) => boolean;
};

export const PresetItem: FunctionComponent<PresetItemProps> = memo(function WrappedPresetItem({
preset,
onDelete,
onLoad,
onRename,
onSetInitial
onSave,
onSetInitial,
validateUniqueName
}: PresetItemProps) {

const [isEditing, setIsEditing] = useState<boolean>(false);
Expand All @@ -38,18 +42,28 @@ export const PresetItem: FunctionComponent<PresetItemProps> = memo(function Wrap
onSetInitial(preset);
}, [preset, onSetInitial]);

const onSavePreset = useCallback((_e: MouseEvent<HTMLButtonElement>) => {
onSave(preset);
}, [onSave, preset]);

const onLoadPreset = useCallback((_e: MouseEvent<HTMLButtonElement>) => {
onLoad(preset);
}, [onLoad, preset]);

const onRenamePreset = useCallback((e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!name?.length) {
inputRef.current?.focus();
const trimmedName = name.trim();
if (preset.name === trimmedName) {
setIsEditing(false);
} else if (!trimmedName?.length) {
setError("Please provide a valid preset name");
} else if (!validateUniqueName(trimmedName)) {
setError(`A preset with the name "${trimmedName}" already exists`);
} else {
onRename(preset, name);
onRename(preset, trimmedName);
}
}, [name, onRename, preset, setError]);
}, [name, onRename, preset, setError, inputRef, setIsEditing, validateUniqueName]);

const onDeletePreset = useCallback((_e: MouseEvent<HTMLButtonElement>) => {
onDelete(preset);
Expand All @@ -72,6 +86,15 @@ export const PresetItem: FunctionComponent<PresetItemProps> = memo(function Wrap
}
}, [toggleEditing]);

useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
if (!isEditing) {
setError(undefined);
}
}, [isEditing, inputRef, setError]);

useEffect(() => {
setName(preset.name);
setIsEditing(false);
Expand Down Expand Up @@ -138,8 +161,10 @@ export const PresetItem: FunctionComponent<PresetItemProps> = memo(function Wrap
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Preset Actions</Menu.Label>
<Menu.Item leftSection={ <IconElement path={ mdiContentSave } /> } onClick={ onSavePreset } >Overwrite</Menu.Item>
<Menu.Item leftSection={ <IconElement path={ mdiPencil } /> } onClick={ toggleEditing } >Rename</Menu.Item>
{ onSetInitial && <Menu.Item leftSection={ <IconElement path={ mdiStar } /> } onClick={ onSetInitialPreset } >Load on Startup</Menu.Item> }
<Menu.Divider />
<Menu.Item color="red" leftSection={ <IconElement path={ mdiTrashCan } /> } onClick={ onDeletePreset } >Delete</Menu.Item>
</Menu.Dropdown>
</Menu>
Expand Down
5 changes: 3 additions & 2 deletions src/components/presets/save.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ export const SavePresetForm: FunctionComponent<SavePresetFormProps> = memo(funct

const onSavePreset = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
if (!name?.length) {
const trimmedName = name.trim();
if (!trimmedName?.length) {
setError("Please provide a valid preset name");
} else {
setError(undefined);
onSave(name);
onSave(trimmedName);
setName("");
}
};
Expand Down
7 changes: 4 additions & 3 deletions src/components/setViews/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ export const CreateSetViewForm: FC<CreateSetViewFormProps> = memo(function Wrapp
if (error && e.target.value?.length) setError(undefined);
};

const onSavePreset = (e: FormEvent<HTMLFormElement>): void => {
const onSaveSetView = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
if (!name?.length) {
const trimmedName = name.trim();
if (!trimmedName?.length) {
setError("Please provide a valid SetView name");
} else {
setError(undefined);
Expand All @@ -36,7 +37,7 @@ export const CreateSetViewForm: FC<CreateSetViewFormProps> = memo(function Wrapp
}, []);

return (
<form onSubmit={ onSavePreset } >
<form onSubmit={ onSaveSetView } >
<Group gap="xs" align="flex-end">
<TextInput
label="Create SetView"
Expand Down
5 changes: 5 additions & 0 deletions src/components/setViews/drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ const SetViewDrawer: FC<CreateSetViewModalProps> = ({
});
}, [onDeleteSetView]);

const validateUniqueSetViewName = useCallback((name: string): boolean => {
return !setViews.find(v => v.name === name);
}, [setViews]);

return (

<Drawer
Expand All @@ -70,6 +74,7 @@ const SetViewDrawer: FC<CreateSetViewModalProps> = ({
onDelete={ onTriggerDeleteSetView }
onRename={ onRenameSetView }
onLoad={ onLoadSetView }
validateUniqueName={ validateUniqueSetViewName }
/>
))
}
Expand Down
25 changes: 21 additions & 4 deletions src/components/setViews/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ export type GraphSetViewItemProps = {
onLoad: (set: GraphSetViewRecord) => any;
onRename: (set: GraphSetViewRecord, name: string) => any;
setView: GraphSetViewRecord;
validateUniqueName: (name: string) => boolean;
};

export const GraphSetViewItem: FC<GraphSetViewItemProps> = memo(function WrappedGraphSetViewItem({
isActive,
onDelete,
onLoad,
onRename,
setView
setView,
validateUniqueName
}: GraphSetViewItemProps) {

const [isEditing, setIsEditing] = useState<boolean>(false);
Expand All @@ -40,12 +42,18 @@ export const GraphSetViewItem: FC<GraphSetViewItemProps> = memo(function Wrapped

const onRenameSetView = useCallback((e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!name?.length) {
inputRef.current?.focus();
const trimmedName = name.trim();
if (setView.name === trimmedName) {
setIsEditing(false);
} else if (!trimmedName?.length) {
setError("Please provide a valid SetView name");
} else if (!validateUniqueName(trimmedName)) {
setError(`A SetView with the name "${trimmedName}" already exists`);
} else {
onRename(setView, name);
onRename(setView, trimmedName);
}
}, [name, onRename, setView, setError]);
}, [name, onRename, setView, setError, setIsEditing, validateUniqueName]);

const onDeleteSetView = useCallback((_e: MouseEvent<HTMLButtonElement>) => {
onDelete(setView);
Expand Down Expand Up @@ -73,6 +81,15 @@ export const GraphSetViewItem: FC<GraphSetViewItemProps> = memo(function Wrapped
setIsEditing(false);
}, [setView, setName, setIsEditing]);

useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
if (!isEditing) {
setError(undefined);
}
}, [isEditing, inputRef, setError]);

return isEditing ? (
<form onSubmit={ onRenameSetView } >
<Group align="flex-start">
Expand Down
6 changes: 3 additions & 3 deletions src/components/setViews/paramModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { PatcherInstanceRecord } from "../../models/instance";
import { OrderedSet as ImmuOrderedSet, Seq } from "immutable";
import { ParameterRecord } from "../../models/parameter";
import { ResponsiveButton } from "../elements/responsiveButton";
import { addAllParamtersToSetView, addParameterToSetView, removeAllParamtersFromSetView, removeParameterFromSetView } from "../../actions/sets";
import { addAllParametersToSetView, addParameterToSetView, removeAllParametersFromSetView, removeParameterFromSetView } from "../../actions/sets";
import classes from "./setviews.module.css";
import { modals } from "@mantine/modals";

Expand Down Expand Up @@ -112,7 +112,7 @@ export const SetViewParameterModal: FC<SetViewParameterModalProps> = memo(functi
</Text>
),
labels: { confirm: "Add", cancel: "Cancel" },
onConfirm: () => dispatch(addAllParamtersToSetView(setView))
onConfirm: () => dispatch(addAllParametersToSetView(setView))
});

}, [dispatch, setView]);
Expand All @@ -128,7 +128,7 @@ export const SetViewParameterModal: FC<SetViewParameterModalProps> = memo(functi
),
labels: { confirm: "Remove", cancel: "Cancel" },
confirmProps: { color: "red" },
onConfirm: () => dispatch(removeAllParamtersFromSetView(setView))
onConfirm: () => dispatch(removeAllParametersFromSetView(setView))
});
}, [dispatch, setView]);

Expand Down
Loading
Loading