feat(pipelines): 编辑和删除功能。

This commit is contained in:
Ivan Li 2021-03-07 17:44:51 +08:00
parent 282366cd72
commit 611341a8ce
7 changed files with 203 additions and 64 deletions

View File

@ -5,6 +5,8 @@ export const LIST_PIPELINES = gql`
pipelines: listPipelines(projectId: $projectId) { pipelines: listPipelines(projectId: $projectId) {
id id
name name
branch
projectId
} }
} }
`; `;

View File

@ -1,22 +1,45 @@
.pipelineList { .pipelineList {
@apply bg-red-200; @apply bg-red-200 w-40;
& > header { & > header {
@apply bg-white flex justify-between items-center; @apply bg-white flex justify-between items-center;
} }
} }
.list {
@apply bg-gray-100 overflow-y-auto max-h-full;
}
.item { .item {
@apply bg-gray-50; @apply bg-white text-gray-700;
@apply border-l-4 border-white transition-colors;
@apply px-2 py-1 my-px;
@apply relative;
small {
@apply text-sm text-gray-500;
svg {
@apply mr-1 h-3 text-gray-300;
}
}
}
.activeItem {
@apply border-red-500;
} }
.addBtn { .addBtn {
@apply bg-red-400 text-white; @apply bg-red-400 text-white;
@apply py-1 px-2 m-2 rounded-lg; @apply py-1 px-2 m-1 rounded-lg mr-auto;
@apply hover:bg-red-500; @apply hover:bg-red-500;
} }
.refetchBtn { .actionBtn {
@apply text-red-400 flex items-center; @apply text-red-400 flex items-center;
@apply w-4 h-4 m-2 rounded-full; @apply w-4 h-4 flex items-center justify-center overflow-hidden text-center m-1;
svg {
}
}
.editPanel {
@apply absolute right-1 top-0 bottom-0;
@apply flex flex-col items-center justify-evenly;
@apply text-red-400 text-xs;
button {
@apply w-3;
}
} }

View File

@ -1,10 +1,13 @@
// This file is automatically generated from your CSS. Any edits will be overwritten. // This file is automatically generated from your CSS. Any edits will be overwritten.
declare namespace PipelineListScssNamespace { declare namespace PipelineListScssNamespace {
export interface IPipelineListScss { export interface IPipelineListScss {
actionBtn: string;
activeItem: string;
addBtn: string; addBtn: string;
editPanel: string;
item: string; item: string;
list: string;
pipelineList: string; pipelineList: string;
refetchBtn: string;
} }
} }

View File

@ -1,16 +1,29 @@
import { useQuery } from '@apollo/client'; import { gql, useQuery, useMutation } from '@apollo/client';
import { makeAutoObservable } from 'mobx'; import { makeAutoObservable } from 'mobx';
import { useLocalObservable, useObserver } from 'mobx-react-lite'; import { useLocalObservable } from 'mobx-react-lite';
import { useMemo } from 'preact/hooks'; import { useCallback, useMemo } from 'preact/hooks';
import { h } from 'preact'; import { h } from 'preact';
import { Pipeline } from '../../generated/graphql'; import { Pipeline } from '../../generated/graphql';
import styles from './pipeline-list.scss'; import styles from './pipeline-list.scss';
import classNames from 'classnames'; import classNames from 'classnames';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faRedoAlt } from '@fortawesome/free-solid-svg-icons'; import {
faCodeBranch,
faRedoAlt,
faEdit,
faEllipsisV,
faTrash
} from '@fortawesome/free-solid-svg-icons';
import { createOverlay } from '../commons/overlay/overlay'; import { createOverlay } from '../commons/overlay/overlay';
import { PipelineEditor } from '../../routes/pipelines/pipeline-editor'; import { PipelineEditor } from '../../routes/pipelines/pipeline-editor';
import { LIST_PIPELINES } from './pipeline-list.constants'; import { LIST_PIPELINES } from './pipeline-list.constants';
import { Observer, observer } from 'mobx-react';
const DELETE_PIPELINE = gql`
mutation DeletePipeline($id: String!) {
deletePipeline(id: $id)
}
`;
interface Props { interface Props {
projectId: string; projectId: string;
@ -19,36 +32,28 @@ interface Props {
class Store { class Store {
pipelines?: Pipeline[]; pipelines?: Pipeline[];
currPipelineId?: string; currPipelineId?: string;
editMode = false;
constructor() { constructor() {
makeAutoObservable(this); makeAutoObservable(this);
} }
refetching = false;
get items() {
return this.pipelines?.map(pipeline => {
const isActive = this.currPipelineId === pipeline.id;
return (
<li
key={pipeline.id}
className={classNames(
{
isActive
},
styles.item
)}
>
<h3>{pipeline.name}</h3>
</li>
);
});
}
setPipelines(pipelines: any[] | undefined) { setPipelines(pipelines: any[] | undefined) {
this.pipelines = pipelines; this.pipelines = pipelines;
} }
setRefetching(val: boolean) {
this.refetching = val;
}
setCurrPipelineId(id: string) {
this.currPipelineId = id;
}
toggleEditMode() {
this.editMode = !this.editMode;
}
} }
export const PipelineList = ({ projectId }: Props) => { export const PipelineList = ({ projectId }: Props) => {
const { data, refetch } = useQuery<{ pipelines: Pipeline[] }>( const { data, refetch, loading } = useQuery<{ pipelines: Pipeline[] }>(
LIST_PIPELINES, LIST_PIPELINES,
{ {
variables: { variables: {
@ -63,25 +68,123 @@ export const PipelineList = ({ projectId }: Props) => {
useMemo(() => store.setPipelines(pipelines), [store, pipelines]); useMemo(() => store.setPipelines(pipelines), [store, pipelines]);
const items = useObserver(() => store.items); const Header = observer(() => {
const addPipeline = () => { const addPipeline = () => {
createOverlay({ createOverlay({
content: <PipelineEditor projectId={projectId} /> content: <PipelineEditor projectId={projectId} />
}); });
}; };
const onRefetchBtnClick = () => {
store.setRefetching(true);
refetch().finally(() => store.setRefetching(false));
};
return ( return (
<section className={styles.pipelineList}>
<header> <header>
<button className={styles.addBtn} onClick={addPipeline}> <button className={styles.addBtn} onClick={addPipeline}>
Add Add
</button> </button>
<button className={styles.refetchBtn} onClick={() => refetch()}> <button className={styles.actionBtn} onClick={onRefetchBtnClick}>
<FontAwesomeIcon icon={faRedoAlt} /> <FontAwesomeIcon
className={classNames({
'animate-spin': store.refetching
})}
icon={faRedoAlt}
/>
</button>
<button
className={styles.actionBtn}
onClick={() => store.toggleEditMode()}
>
<FontAwesomeIcon icon={faEllipsisV} />
</button> </button>
</header> </header>
<ul>{items}</ul> );
});
const items = pipelines?.map(pipeline => (
<Observer key={pipeline.id}>
{(): any => <Item pipeline={pipeline} store={store} />}
</Observer>
));
return (
<section className={styles.pipelineList}>
<Header />
<ul className={styles.list}>{items}</ul>
</section> </section>
); );
}; };
const Item = observer(
({ pipeline, store }: { pipeline: Pipeline; store: Store }) => {
const [remove] = useMutation(DELETE_PIPELINE, {
variables: { id: pipeline.id },
update(cache) {
const cacheData = cache.readQuery<{ pipelines: Pipeline[] }>({
query: LIST_PIPELINES,
variables: { projectId: pipeline.projectId }
});
const pipelines = cacheData?.pipelines?.slice();
if (!pipelines) {
return;
}
const index = pipelines.findIndex(
(item: Pipeline) => item.id === pipeline.id
);
if (index === -1) {
return;
}
pipelines.splice(index, 1);
cache.writeQuery({
query: LIST_PIPELINES,
variables: { projectId: pipeline.projectId },
data: {
pipelines
}
});
}
});
const isActive = store.currPipelineId === pipeline.id;
const openModifyPanel = () => {
createOverlay({
content: (
<PipelineEditor projectId={pipeline.projectId} id={pipeline.id} />
)
});
};
const onRemoveBtnClick = () => {
remove();
};
const EditPanel = observer(() => {
return store.editMode ? (
<div className={styles.editPanel}>
<button onClick={openModifyPanel}>
<FontAwesomeIcon icon={faEdit} />
</button>
<button onClick={onRemoveBtnClick}>
<FontAwesomeIcon icon={faTrash} />
</button>
</div>
) : null;
});
return (
<li
key={pipeline.id}
className={classNames(
{
[styles.activeItem]: isActive
},
styles.item
)}
onClick={() => store.setCurrPipelineId(pipeline.id)}
>
<h3>{pipeline.name}</h3>
<small>
<FontAwesomeIcon icon={faCodeBranch} />
{pipeline.branch}
</small>
<Observer>{(): any => <EditPanel />}</Observer>
</li>
);
}
);

View File

@ -27,7 +27,6 @@ const FIND_PROJECTS = gql`
sshUrl sshUrl
webUrl webUrl
webHookSecret webHookSecret
deletedAt
} }
} }
`; `;

View File

@ -1,17 +1,18 @@
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik, useFormik } from 'formik';
import { h } from 'preact'; import { h } from 'preact';
import { RoutableProps } from 'preact-router'; import { RoutableProps } from 'preact-router';
import styles from './pipeline-editor.scss'; import styles from './pipeline-editor.scss';
import {
CreatePipelineInput,
Pipeline,
UpdatePipelineInput
} from '../../generated/graphql';
import { useOverlay } from '../../components/commons/overlay/overlay'; import { useOverlay } from '../../components/commons/overlay/overlay';
import { gql, useLazyQuery, useMutation } from '@apollo/client'; import { gql, useLazyQuery, useMutation } from '@apollo/client';
import { useMemo } from 'preact/hooks'; import { useMemo } from 'preact/hooks';
import classNames from 'classnames'; import classNames from 'classnames';
import { WorkUnitMetadata, PipelineUnits } from '../../generated/graphql'; import {
CreatePipelineInput,
UpdatePipelineInput,
WorkUnitMetadata,
PipelineUnits,
Pipeline
} from '../../generated/graphql';
import { Message } from '../../components/commons/message/index'; import { Message } from '../../components/commons/message/index';
import { LIST_PIPELINES } from '../../components/pipelines/pipeline-list.constants'; import { LIST_PIPELINES } from '../../components/pipelines/pipeline-list.constants';
@ -41,7 +42,7 @@ const FIND_PIPELINE = gql`
query FindPipeline($id: String!) { query FindPipeline($id: String!) {
pipeline: findPipeline(id: $id) { pipeline: findPipeline(id: $id) {
name name
id branch
projectId projectId
workUnitMetadata { workUnitMetadata {
version version
@ -98,18 +99,28 @@ export const PipelineEditor = ({ projectId, id }: Props) => {
}; };
const isCreate = !id; const isCreate = !id;
const [loadPipeline, { data: pipeline }] = useLazyQuery(FIND_PIPELINE); const [loadPipeline, { data }] = useLazyQuery(FIND_PIPELINE, {
variables: { id }
});
useMemo(() => { useMemo(() => {
if (!isCreate) { if (!isCreate) {
loadPipeline(); loadPipeline();
} }
}, []); }, []);
const formData: FormValues = pipeline ?? { const formData: FormValues = useMemo(() => {
name: 'test', if (data?.pipeline) {
projectId, const fd = JSON.parse(JSON.stringify(data.pipeline));
workUnitMetadata: JSON.stringify(defaultWorkUnitMetadata, null, 2) delete fd?.workUnitMetadata.__typename;
}; fd.workUnitMetadata.units.forEach((item: any) => {
delete item.__typename;
});
fd.workUnitMetadata = JSON.stringify(fd.workUnitMetadata, null, 2);
delete fd.__typename;
return fd;
}
return {};
}, [data, data?.pipeline]);
const [createPipeline] = useMutation<{ pipeline: Pipeline }>( const [createPipeline] = useMutation<{ pipeline: Pipeline }>(
CREATE_PIPELINE, CREATE_PIPELINE,
@ -167,10 +178,9 @@ export const PipelineEditor = ({ projectId, id }: Props) => {
// //
} }
}; };
return ( return (
<section className={styles.editor}> <section className={styles.editor}>
<Formik initialValues={formData} onSubmit={submitForm}> <Formik initialValues={formData} onSubmit={submitForm} enableReinitialize>
<Form className={styles.form}> <Form className={styles.form}>
<label> <label>
<span className={styles.label}></span> <span className={styles.label}></span>

View File

@ -21,7 +21,6 @@ const FIND_PROJECT = gql`
sshUrl sshUrl
webUrl webUrl
webHookSecret webHookSecret
deletedAt
} }
} }
`; `;