feat: Delete declaration and delete all declarations in advanced style panel context menu (#4948)

## Description

1. added additional way to delete a declaration in addition to cmd+click
2. delete all declarations

## Steps for reproduction

1. click button
3. expect xyz

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
0000)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file
This commit is contained in:
Oleg Isonen authored and GitHub committed 2025-03-05 21:12:18 +01:00
1 parent 5ba9dc38a8
commit 02bcc55299
7 files changed
+77 -25

No files matched your search

@@ -66,7 +66,7 @@ const AdvancedStyleSection = (props: {
export const Section = () => {
const styleMap = useStore($advancedStylesLonghands);
const apiRef = useRef<CssEditorApi>();
const properties = Array.from(styleMap.keys()) as Array<CssProperty>;
const properties = Array.from(styleMap.keys());
const selectedInstanceKey = useStore($selectedInstanceKey);
// Memorizing recent properties by instance id, so that when user switches between instances and comes back
// they are still in-place
@@ -90,16 +90,14 @@ export const Section = () => {
setRecentPropertiesMap(newRecentPropertiesMap);
};
const handleAddProperties = (styleMap: CssStyleMap) => {
const handleAddDeclarations = (styleMap: CssStyleMap) => {
const batch = createBatchUpdate();
for (const [property, value] of styleMap) {
batch.setProperty(property as CssProperty)(value);
batch.setProperty(property)(value);
}
batch.publish({ listed: true });
const insertedProperties = Array.from(
styleMap.keys()
) as Array<CssProperty>;
const insertedProperties = Array.from(styleMap.keys());
updateRecentProperties([...recentProperties, ...insertedProperties]);
};
@@ -114,6 +112,19 @@ export const Section = () => {
);
};
const handleDeleteAllDeclarations = (styleMap: CssStyleMap) => {
const batch = createBatchUpdate();
for (const [property] of styleMap) {
batch.deleteProperty(property);
}
batch.publish();
updateRecentProperties(
recentProperties.filter(
(recentProperty) => styleMap.has(recentProperty) === false
)
);
};
return (
<AdvancedStyleSection
label="Advanced"
@@ -126,7 +137,8 @@ export const Section = () => {
styleMap={styleMap}
onDeleteProperty={handleDeleteProperty}
onSetProperty={setProperty}
onAddProperties={handleAddProperties}
onAddDeclarations={handleAddDeclarations}
onDeleteAllDeclarations={handleDeleteAllDeclarations}
apiRef={apiRef}
recentProperties={recentProperties}
/>
@@ -8,7 +8,6 @@ import {
} from "@webstudio-is/design-system";
import {
generateStyleMap,
hyphenateProperty,
mergeStyles,
toValue,
type CssProperty,
@@ -17,16 +16,20 @@ import {
export const copyAttribute = "data-declaration";
export const CopyPasteMenu = ({
export const CssEditorContextMenu = ({
children,
properties,
styleMap,
onPaste,
onDeleteProperty,
onDeleteAllDeclarations,
}: {
children: ReactNode;
properties: Array<string>;
properties: Array<CssProperty>;
styleMap: CssStyleMap;
onPaste: (cssText: string) => void;
onDeleteProperty: (property: CssProperty) => void;
onDeleteAllDeclarations: (styleMap: CssStyleMap) => void;
}) => {
const lastClickedProperty = useRef<string>();
@@ -34,37 +37,56 @@ export const CopyPasteMenu = ({
navigator.clipboard.readText().then(onPaste);
};
const handleCopyAll = () => {
// Gets all currently visible declarations based on what's in the search or filters.
const getAllDeclarations = () => {
// We want to only copy properties that are currently in front of the user.
// That includes search or any future filters.
const currentStyleMap: CssStyleMap = new Map();
for (const [property, value] of styleMap) {
const isEmpty = toValue(value) === "";
if (properties.includes(property) && isEmpty === false) {
currentStyleMap.set(hyphenateProperty(property), value);
currentStyleMap.set(property, value);
}
}
return currentStyleMap;
};
const css = generateStyleMap(mergeStyles(currentStyleMap));
const handleCopyAll = () => {
const styleMap = getAllDeclarations();
const css = generateStyleMap(mergeStyles(styleMap));
navigator.clipboard.writeText(css);
};
const handleCopy = () => {
const property = lastClickedProperty.current;
const property = lastClickedProperty.current as CssProperty;
if (property === undefined) {
return;
}
const value = styleMap.get(property as CssProperty);
const value = styleMap.get(property);
if (value === undefined) {
return;
}
const style = new Map([[property, value]]);
const css = generateStyleMap(style);
const css = generateStyleMap(new Map([[property, value]]));
navigator.clipboard.writeText(css);
};
const handleDelete = () => {
const property = lastClickedProperty.current as CssProperty;
const value = styleMap.get(property);
if (value === undefined) {
return;
}
onDeleteProperty(property);
};
const handleDeleteAllDeclarations = () => {
const styleMap = getAllDeclarations();
onDeleteAllDeclarations(styleMap);
};
return (
<ContextMenu>
<ContextMenuTrigger
@@ -93,6 +115,12 @@ export const CopyPasteMenu = ({
<ContextMenuItem onSelect={handlePaste}>
Paste declarations
</ContextMenuItem>
<ContextMenuItem destructive onSelect={handleDelete}>
Delete declaration
</ContextMenuItem>
<ContextMenuItem destructive onSelect={handleDeleteAllDeclarations}>
Delete all declarations
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
@@ -55,7 +55,8 @@ export const CssEditor = () => {
styleMap={styleMap}
onDeleteProperty={() => undefined}
onSetProperty={() => () => undefined}
onAddProperties={() => undefined}
onAddDeclarations={() => undefined}
onDeleteAllDeclarations={() => undefined}
/>
);
};
@@ -31,6 +31,7 @@ import {
type CssProperty,
type CssStyleMap,
} from "@webstudio-is/css-engine";
// @todo all style panel stuff needs to be moved to shared and/or decoupled from style panel
import { CssValueInputContainer } from "../../features/style-panel/shared/css-value-input";
import { styleConfigByName } from "../../features/style-panel/shared/configs";
import {
@@ -40,7 +41,7 @@ import {
import { PropertyInfo } from "../../features/style-panel/property-label";
import { ColorPopover } from "../../features/style-panel/shared/color-picker";
import { useClientSupports } from "~/shared/client-supports";
import { CopyPasteMenu, copyAttribute } from "./copy-paste-menu";
import { CssEditorContextMenu, copyAttribute } from "./css-editor-context-menu";
import { AddStyleInput } from "./add-style-input";
import { parseStyleInput } from "./parse-style-input";
import type {
@@ -309,7 +310,8 @@ export type CssEditorApi = { showAddStyleInput: () => void } | undefined;
export const CssEditor = ({
onDeleteProperty,
onSetProperty,
onAddProperties,
onAddDeclarations,
onDeleteAllDeclarations,
styleMap,
apiRef,
showSearch = true,
@@ -317,7 +319,8 @@ export const CssEditor = ({
}: {
onDeleteProperty: DeleteProperty;
onSetProperty: SetProperty;
onAddProperties: (styleMap: CssStyleMap) => void;
onAddDeclarations: (styleMap: CssStyleMap) => void;
onDeleteAllDeclarations: (styleMap: CssStyleMap) => void;
styleMap: CssStyleMap;
apiRef?: RefObject<CssEditorApi>;
showSearch?: boolean;
@@ -357,7 +360,7 @@ export const CssEditor = ({
if (styleMap.size === 0) {
return new Map();
}
onAddProperties(styleMap);
onAddDeclarations(styleMap);
return styleMap;
};
@@ -415,8 +418,10 @@ export const CssEditor = ({
/>
</Box>
)}
<CopyPasteMenu
<CssEditorContextMenu
onPaste={handleInsertStyles}
onDeleteProperty={onDeleteProperty}
onDeleteAllDeclarations={onDeleteAllDeclarations}
styleMap={styleMap}
properties={
searchProperties ?? [...recentProperties, ...currentProperties]
@@ -496,7 +501,7 @@ export const CssEditor = ({
})}
</Flex>
</Flex>
</CopyPasteMenu>
</CssEditorContextMenu>
</>
);
};
+1
View File
@@ -65,6 +65,7 @@
"match-sorter": "^8.0.0",
"react-hot-toast": "^2.5.1",
"token-transformer": "^0.0.28",
"type-fest": "^4.32.0",
"use-debounce": "^10.0.4",
"warn-once": "^0.1.1"
},
@@ -19,6 +19,7 @@ import {
MenuCheckedIcon,
} from "./menu";
export { DropdownMenuArrow } from "./menu";
import type { Simplify } from "type-fest";
export const ContextMenu = ContextMenuPrimitive.Root;
@@ -57,9 +58,10 @@ export const ContextMenuLabel = styled(ContextMenuPrimitive.Label, labelCss);
const StyledMenuItem = styled(ContextMenuPrimitive.Item, menuItemCss, {
defaultVariants: { withIndicator: true },
});
export const ContextMenuItem = forwardRef<
ElementRef<typeof StyledMenuItem>,
ComponentProps<typeof StyledMenuItem> & { icon?: ReactNode }
Simplify<ComponentProps<typeof StyledMenuItem> & { icon?: ReactNode }>
>(({ icon, children, withIndicator, ...props }, forwardedRef) =>
icon ? (
<StyledMenuItem
+3
View File
@@ -1420,6 +1420,9 @@ importers:
token-transformer:
specifier: ^0.0.28
version: 0.0.28
type-fest:
specifier: ^4.32.0
version: 4.32.0
use-debounce:
specifier: ^10.0.4
version: 10.0.4(react@18.3.0-canary-14898b6a9-20240318)