Skip to content
Snippets Groups Projects
Unverified Commit 6fb322ae authored by Alexander Polyankin's avatar Alexander Polyankin Committed by GitHub
Browse files

Add inline collection name and description editing (#23518)

parent a1e23ac7
No related merge requests found
Showing
with 125 additions and 47 deletions
import styled from "@emotion/styled";
import EditableText from "metabase/core/components/EditableText";
export const CaptionContainer = styled.div`
export const CaptionTitleContainer = styled.div`
display: flex;
align-items: center;
gap: 0.5rem;
`;
export const CaptionTitle = styled.h1`
export const CaptionTitle = styled(EditableText)`
font-size: 1.75rem;
font-weight: 900;
word-break: break-word;
word-wrap: anywhere;
overflow-wrap: anywhere;
`;
export const CaptionDescription = styled.div`
font-size: 1rem;
line-height: 1.5rem;
padding-top: 1.15rem;
export interface CaptionDescriptionProps {
isVisible: boolean;
}
export const CaptionDescription = styled(EditableText)<CaptionDescriptionProps>`
opacity: ${props => (props.isVisible ? 1 : 0)};
max-width: 25rem;
transition: opacity 400ms ease 1s;
`;
export const CaptionRoot = styled.div`
&:hover,
&:focus-within {
${CaptionDescription} {
opacity: 1;
transition-delay: 0s;
}
}
`;
import React from "react";
import React, { useCallback } from "react";
import { t } from "ttag";
import { PLUGIN_COLLECTION_COMPONENTS } from "metabase/plugins";
import {
isPersonalCollection,
isRootCollection,
} from "metabase/collections/utils";
import { Collection } from "metabase-types/api";
import {
CaptionContainer,
CaptionTitle,
CaptionDescription,
CaptionRoot,
CaptionTitle,
CaptionTitleContainer,
} from "./CollectionCaption.styled";
export interface CollectionCaptionProps {
collection: Collection;
onChangeName: (collection: Collection, name: string) => void;
onChangeDescription: (
collection: Collection,
description: string | null,
) => void;
}
const CollectionCaption = ({
collection,
onChangeName,
onChangeDescription,
}: CollectionCaptionProps): JSX.Element => {
const isRoot = isRootCollection(collection);
const isPersonal = isPersonalCollection(collection);
const isEditable = !isRoot && !isPersonal && collection.can_write;
const handleChangeName = useCallback(
(name: string) => {
onChangeName(collection, name);
},
[collection, onChangeName],
);
const handleChangeDescription = useCallback(
(description: string) => {
onChangeDescription(collection, description ? description : null);
},
[collection, onChangeDescription],
);
return (
<div>
<CaptionContainer>
<CaptionRoot>
<CaptionTitleContainer>
<PLUGIN_COLLECTION_COMPONENTS.CollectionAuthorityLevelIcon
collection={collection}
size={24}
/>
<CaptionTitle data-testid="collection-name-heading">
{collection.name}
</CaptionTitle>
</CaptionContainer>
{collection.description && (
<CaptionDescription>{collection.description}</CaptionDescription>
<CaptionTitle
key={collection.id}
initialValue={collection.name}
placeholder={t`Add title`}
isDisabled={!isEditable}
data-testid="collection-name-heading"
onChange={handleChangeName}
/>
</CaptionTitleContainer>
{isEditable && (
<CaptionDescription
key={collection.id}
initialValue={collection.description}
placeholder={t`Add description`}
isVisible={Boolean(collection.description)}
isOptional
isMultiline
onChange={handleChangeDescription}
/>
)}
</div>
</CaptionRoot>
);
};
......
......@@ -11,6 +11,11 @@ export interface CollectionHeaderProps {
isAdmin: boolean;
isBookmarked: boolean;
isPersonalCollectionChild: boolean;
onChangeName: (collection: Collection, name: string) => void;
onChangeDescription: (
collection: Collection,
description: string | null,
) => void;
onCreateBookmark: (collection: Collection) => void;
onDeleteBookmark: (collection: Collection) => void;
}
......@@ -20,12 +25,18 @@ const CollectionHeader = ({
isAdmin,
isBookmarked,
isPersonalCollectionChild,
onChangeName,
onChangeDescription,
onCreateBookmark,
onDeleteBookmark,
}: CollectionHeaderProps): JSX.Element => {
return (
<HeaderRoot>
<CollectionCaption collection={collection} />
<CollectionCaption
collection={collection}
onChangeName={onChangeName}
onChangeDescription={onChangeDescription}
/>
<HeaderActions data-testid="collection-menu">
<CollectionTimeline collection={collection} />
<CollectionBookmark
......
import _ from "underscore";
import { connect } from "react-redux";
import { State } from "metabase-types/store";
import Databases from "metabase/entities/databases";
import { getHasDataAccess } from "metabase/new_query/selectors";
import CollectionHeader from "../components/CollectionHeader/CollectionHeader";
const mapStateToProps = (state: State) => ({
canCreateQuestions: getHasDataAccess(state),
});
export default _.compose(
Databases.loadList({
loadingAndErrorWrapper: false,
}),
connect(mapStateToProps),
)(CollectionHeader);
import { connect } from "react-redux";
import Collections from "metabase/entities/collections";
import { Collection } from "metabase-types/api";
import CollectionHeader from "../../components/CollectionHeader";
const mapDispatchToProps = {
onChangeName: (collection: Collection, name: string) =>
Collections.actions.update(collection, { name }),
onChangeDescription: (collection: Collection, description: string | null) =>
Collections.actions.update(collection, { description }),
};
export default connect(null, mapDispatchToProps)(CollectionHeader);
export { default } from "./CollectionHeader";
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
export const EditableTextRoot = styled.div`
export interface EditableTextRootProps {
isDisabled: boolean;
}
export const EditableTextRoot = styled.div<EditableTextRootProps>`
position: relative;
color: ${color("text-dark")};
padding: 0.25rem;
......@@ -9,7 +13,7 @@ export const EditableTextRoot = styled.div`
&:hover,
&:focus-within {
border-color: ${color("border")};
border-color: ${props => (props.isDisabled ? "" : color("border"))};
}
&:after {
......@@ -32,7 +36,7 @@ export const EditableTextArea = styled.textarea`
font-size: inherit;
font-weight: inherit;
line-height: inherit;
cursor: pointer;
cursor: ${props => (props.disabled ? "text" : "pointer")};
border: none;
resize: none;
outline: none;
......
......@@ -19,6 +19,7 @@ export interface EditableTextProps extends EditableTextAttributes {
placeholder?: string;
isOptional?: boolean;
isMultiline?: boolean;
isDisabled?: boolean;
onChange?: (value: string) => void;
"data-testid"?: string;
}
......@@ -29,6 +30,7 @@ const EditableText = forwardRef(function EditableText(
placeholder,
isOptional = false,
isMultiline = false,
isDisabled = false,
onChange,
"data-testid": dataTestId,
...props
......@@ -68,10 +70,16 @@ const EditableText = forwardRef(function EditableText(
);
return (
<EditableTextRoot {...props} ref={ref} data-value={`${displayValue}\u00A0`}>
<EditableTextRoot
{...props}
ref={ref}
isDisabled={isDisabled}
data-value={`${displayValue}\u00A0`}
>
<EditableTextArea
value={inputValue}
placeholder={placeholder}
disabled={isDisabled}
data-testid={dataTestId}
onBlur={handleBlur}
onChange={handleChange}
......
......@@ -27,7 +27,7 @@ function SavedQuestionHeaderButton({ className, question, onSave }) {
<HeaderRoot>
<HeaderTitle
initialValue={question.displayName()}
placeholder={t`A nice title`}
placeholder={t`Add title`}
onChange={onSave}
data-testid="saved-question-header-title"
/>
......
......@@ -50,7 +50,7 @@ export const QuestionInfoSidebar = ({
<ContentSection>
<EditableText
initialValue={description}
placeholder={t`Description`}
placeholder={t`Add description`}
isOptional
isMultiline
onChange={handleSave}
......
......@@ -46,7 +46,10 @@ describe("metabase > scenarios > navbar > new menu", () => {
cy.findByText("Create").click();
});
cy.get("h1").should("have.text", "Test collection");
cy.findByTestId("collection-name-heading").should(
"have.text",
"Test collection",
);
});
it("should suggest questions saved in collections with colon in their name (metabase#14287)", () => {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment