Skip to content

Commit

Permalink
feat!: add documentation to projects
Browse files Browse the repository at this point in the history
  • Loading branch information
olloz26 authored and ciyer committed Jan 10, 2025
1 parent 625c102 commit a465c67
Show file tree
Hide file tree
Showing 16 changed files with 386 additions and 22 deletions.
4 changes: 4 additions & 0 deletions client/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"apiversion",
"ascii",
"asciimath",
"autoformat",
"autosave",
"autosaved",
"autosaves",
Expand All @@ -106,6 +107,7 @@
"bool",
"booleans",
"borderless",
"bulleted",
"calc",
"cancellable",
"cancelled",
Expand Down Expand Up @@ -255,13 +257,15 @@
"stdout",
"stockimages",
"storages",
"strikethrough",
"swiper",
"tada",
"telepresence",
"textarea",
"thead",
"toastify",
"toggler",
"tokenizer",
"tolerations",
"toml",
"tooltip",
Expand Down
98 changes: 97 additions & 1 deletion client/src/components/buttons/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import cx from "classnames";
import { Fragment, ReactNode, useRef, useState } from "react";
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import {
ArrowRight,
ChevronDown,
Expand All @@ -42,6 +42,7 @@ import {
Col,
DropdownMenu,
DropdownToggle,
Tooltip,
UncontrolledDropdown,
UncontrolledTooltip,
} from "reactstrap";
Expand Down Expand Up @@ -444,6 +445,100 @@ function EditButtonLink({
);
}

interface EditSaveButtonProps {
"data-cy"?: string;
disabled?: boolean;
toggle: () => void;
tooltipMessage?: string | null;
checksBeforeSave?: () => boolean;
checksBeforeSaveTooltipMessage?: () => string | null;
}
function EditSaveButton({
"data-cy": dataCy,
disabled,
toggle,
tooltipMessage = null,
checksBeforeSave = () => false,
checksBeforeSaveTooltipMessage = () => null,
}: EditSaveButtonProps) {
const [localDisabled, setLocalDisabled] = useState(disabled);
const ref = useRef(null);
const saveButtonRef = useRef(null);
const [editMode, setEditMode] = useState(false);
const [checksBeforeSaveTooltip, setChecksBeforeSaveTooltip] = useState(false);

useEffect(() => {
setLocalDisabled(disabled);
}, [disabled]);

return (
<>
<span ref={ref}>
{!editMode && localDisabled ? (
<Button color="outline-primary" disabled size="sm">
<Pencil className="bi" />
</Button>
) : editMode ? (
<ButtonGroup>
<Button
innerRef={saveButtonRef}
disabled={localDisabled}
color="outline-primary"
size="sm"
data-cy={dataCy}
onClick={(event) => {
if (checksBeforeSave()) {
setEditMode(false);
(event.target as HTMLInputElement).form?.requestSubmit();
} else {
setChecksBeforeSaveTooltip(true);
}
}}
>
Save
</Button>
{checksBeforeSaveTooltip && localDisabled ? (
<Tooltip target={saveButtonRef} isOpen={true}>
{checksBeforeSaveTooltipMessage()}
</Tooltip>
) : tooltipMessage ? (
<UncontrolledTooltip target={ref} delay={{ show: 500, hide: 50 }}>
{tooltipMessage}
</UncontrolledTooltip>
) : (
<></>
)}
<Button
color="outline-primary"
size="sm"
data-cy={dataCy}
onClick={() => {
setEditMode(false);
setLocalDisabled(false);
toggle();
}}
>
Discard
</Button>
</ButtonGroup>
) : (
<Button
color="outline-primary"
size="sm"
data-cy={dataCy}
onClick={() => {
setEditMode(true);
toggle();
}}
>
<Pencil className="bi" />
</Button>
)}
</span>
</>
);
}

export function PlusRoundButton({
"data-cy": dataCy,
handler,
Expand Down Expand Up @@ -478,6 +573,7 @@ export {
ButtonWithMenu,
CardButton,
EditButtonLink,
EditSaveButton,
GoBackButton,
InlineSubmitButton,
RefreshButton,
Expand Down
38 changes: 38 additions & 0 deletions client/src/components/form-field/LazyCkEditorRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*!
* Copyright 2023 - Swiss Data Science Center (SDSC)
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
* Eidgenössische Technische Hochschule Zürich (ETHZ).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { lazy, Suspense } from "react";
import { Loader } from "../Loader";

const CkEditor = lazy(() => import("./CkEditor"));

export function LazyCkEditorRenderer(props: { name: string; data: string }) {
return (
<Suspense fallback={<Loader />}>
<CkEditor
id={props.name}
name={props.name}
data={props.data}
disabled={true}
invalid={false}
outputType={"html"}
setInputs={() => {}}
/>
</Suspense>
);
}
5 changes: 5 additions & 0 deletions client/src/components/form-field/TextAreaInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ interface TextAreaInputProps<T extends FieldValues> {
name: string;
register: UseFormRegisterReturn;
required?: boolean;
wordCount?: (stats: {
exact: boolean;
characters: number;
words: number;
}) => void;
}

function TextAreaInput<T extends FieldValues>(props: TextAreaInputProps<T>) {
Expand Down
8 changes: 8 additions & 0 deletions client/src/components/form-field/ckEditor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.ck.ck-editor__main > .ck-editor__editable:not(.ck-focused) {
border: 0px;
}
.ck.ck-editor__top
.ck-sticky-panel:not(.ck-focused)
.ck-sticky-panel__content:not(.ck-focused) {
border: 0px;
}
4 changes: 3 additions & 1 deletion client/src/components/formlabels/FormLabels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ interface InputLabelProps extends LabelProps {
}

const InputLabel = ({ text, isRequired = false }: InputLabelProps) => {
return (
return text ? (
<Label>
{text} <RequiredLabel isRequired={isRequired} />
</Label>
) : (
<></>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default function ProjectPageContainer() {
useGetNamespacesByNamespaceProjectsAndSlugQuery({
namespace: namespace ?? "",
slug: slug ?? "",
withDocumentation: true,
});

if (isLoading) return <Loader className="align-self-center" />;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*!
* Copyright 2024 - Swiss Data Science Center (SDSC)
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
* Eidgenössische Technische Hochschule Zürich (ETHZ).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import cx from "classnames";
import { useCallback, useState } from "react";

import { FileEarmarkText } from "react-bootstrap-icons";
import { Card, CardBody, CardHeader, ListGroup } from "reactstrap";

import { EditSaveButton } from "../../../../components/buttons/Button";

import { Project } from "../../../projectsV2/api/projectV2.api";
import { usePatchProjectsByProjectIdMutation } from "../../../projectsV2/api/projectV2.enhanced-api";
import TextAreaInput from "../../../../components/form-field/TextAreaInput.tsx";
import { useForm } from "react-hook-form";
import { LazyCkEditorRenderer } from "../../../../components/form-field/LazyCkEditorRenderer.tsx";

interface DocumentationForm {
description: string;
}

export default function Documentation({ project }: { project: Project }) {
const [updateProject] = usePatchProjectsByProjectIdMutation();
const [description, setDescription] = useState(project.documentation || "");

const { control, handleSubmit, setValue, getValues, register } =
useForm<DocumentationForm>();
const onSubmit = useCallback(
(data: DocumentationForm) => {
setDescription(data.description);
setShowEditor(false);
updateProject({
"If-Match": project.etag ? project.etag : "",
projectId: project.id,
projectPatch: { documentation: data.description },
});
},
[project.etag, project.id, updateProject]
);

const [showEditor, setShowEditor] = useState(false);
const toggle = () => {
setShowEditor(!showEditor);
setValue("description", description);
};

const markdownCharacterLimit = 5000;
const aboutCharacterLimit =
Math.floor(((2 / 3) * markdownCharacterLimit) / 10) * 10;
const [characterLimit, setCharacterLimit] = useState(aboutCharacterLimit);
const [character, setCharacter] = useState(0);
const [disabledSaveButton, setDisabledSaveButton] = useState(false);

const wordCount = (stats: {
exact: boolean;
characters: number;
words: number;
}) => {
stats.exact
? setCharacterLimit(markdownCharacterLimit)
: setCharacterLimit(aboutCharacterLimit);
setCharacter(stats.characters);
};

const descriptionField = register("description");
{
const descriptionFieldTmp = descriptionField.onChange;
descriptionField.onChange = (value) => {
setDisabledSaveButton(false);
return descriptionFieldTmp(value);
};
}

return (
<Card data-cy="project-documentation-card">
<form className="form-rk-pink" onSubmit={handleSubmit(onSubmit)}>
<CardHeader>
<div
className={cx(
"align-items-center",
"d-flex",
"flex-wrap",
"justify-content-between"
)}
>
<h4 className={cx("m-0")}>
<FileEarmarkText className={cx("me-1", "bi")} />
Documentation
</h4>
<span>
{showEditor ? (
<span style={{ verticalAlign: "middle" }}>
{character} of
{characterLimit == aboutCharacterLimit ? " about " : " "}
{characterLimit} characters &nbsp;
</span>
) : (
<></>
)}
<EditSaveButton
data-cy="project-documentation-edit"
toggle={toggle}
disabled={disabledSaveButton}
// tooltip="Save or discard your changes."
checksBeforeSave={() => {
if (
getValues("description").length <= markdownCharacterLimit
) {
return true;
}
setDisabledSaveButton(true);
return false;
}}
checksBeforeSaveTooltipMessage={() =>
`Documentation is too long.\n The document can not be longer\nthan ${markdownCharacterLimit} characters.`
}
/>
</span>
</div>
</CardHeader>
<CardBody>
{showEditor ? (
<ListGroup flush>
<TextAreaInput<DocumentationForm>
control={control}
getValue={() => getValues("description")}
name="description"
label="Description"
register={descriptionField}
wordCount={wordCount}
/>
</ListGroup>
) : (
<ListGroup flush>
<div className="pb-2"></div>
<LazyCkEditorRenderer name="description" data={description} />
</ListGroup>
)}
</CardBody>
</form>
</Card>
);
}
Loading

0 comments on commit a465c67

Please sign in to comment.