diff --git a/.eslintignore b/.eslintignore index b47577a..3a04567 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,4 @@ preact.config.js src/generated/**/* postcss.js *.config.js -tests/**/* +tests/**/* \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 8fecf35..ba3abc4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,7 +38,8 @@ module.exports = { files: ['*.js', '*.ts', '*.tsx'], rules: { '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/interface-name-prefix': 'off' + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/no-non-null-assertion': 'off' } }, ] diff --git a/.vscode/settings.json b/.vscode/settings.json index 763f00a..09a697c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,8 @@ "editor.quickSuggestions": { "strings": true, "other": true, - } + }, + "cSpell.words": [ + "Formik" + ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f5ce389..2757784 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8886,6 +8886,27 @@ "mime-types": "^2.1.12" } }, + "formik": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.2.6.tgz", + "integrity": "sha512-Kxk2zQRafy56zhLmrzcbryUpMBvT0tal5IvcifK5+4YNGelKsnrODFJ0sZQRMQboblWNym4lAW3bt+tf2vApSA==", + "requires": { + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.14", + "lodash-es": "^4.17.14", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^1.10.0" + }, + "dependencies": { + "deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==" + } + } + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -12448,6 +12469,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, + "lodash-es": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.20.tgz", + "integrity": "sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA==" + }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", @@ -17943,6 +17969,11 @@ } } }, + "react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "react-is": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", @@ -20643,6 +20674,11 @@ "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "dev": true }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "tinydate": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tinydate/-/tinydate-1.3.0.tgz", diff --git a/package.json b/package.json index ec87a8b..b5e0f9c 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@apollo/client": "^3.3.7", "@tailwindcss/postcss7-compat": "^2.0.2", "autoprefixer": "^9.8.6", + "formik": "^2.2.6", "graphql": "^15.5.0", "mobx": "^6.1.4", "mobx-react": "^7.1.0", diff --git a/src/app.store.ts b/src/app.store.ts new file mode 100644 index 0000000..d537ec7 --- /dev/null +++ b/src/app.store.ts @@ -0,0 +1,21 @@ +import { action, makeObservable, observable } from 'mobx'; +import { ComponentChild } from 'preact'; + +export class AppStore { + @observable.ref nav: ComponentChild; + @observable.ref main: ComponentChild; + constructor() { + makeObservable(this); + } + + @action + setNav(component: ComponentChild) { + this.nav = component; + } + @action + setMain(component: ComponentChild) { + this.main = component; + } +} + +export const appStore = new AppStore(); diff --git a/src/components/app.tsx b/src/components/app.tsx index 9d14fcb..d8b130b 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -1,26 +1,8 @@ -import { - ApolloClient, - ApolloProvider, - gql, - InMemoryCache, - useQuery -} from '@apollo/client'; +import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'; import { FunctionalComponent, FunctionComponent, h } from 'preact'; -import { globalState } from '../global.state'; import { ProjectPanel } from './projects/project-panel'; -import * as styles from './app.scss'; - -const LIST_PROJECT = gql` - query listProject { - findProjects { - id - name - sshUrl - comment - webUrl - } - } -`; +import styles from './app.scss'; +import { OverlayContainer } from './commons/overlay/overlay'; const client = new ApolloClient({ uri: '/api/graphql', @@ -32,18 +14,15 @@ const App: FunctionalComponent = () => {
+
); }; const Content: FunctionComponent = () => { - const { loading, data } = useQuery(LIST_PROJECT); - globalState.projects = data?.findProjects; - console.log(globalState.projects); - const Loading: FunctionComponent = () =>
Loading...
; const Board: FunctionComponent = () => ; - return loading ? : ; + return ; }; export default App; diff --git a/src/components/commons/overlay/overlay.scss b/src/components/commons/overlay/overlay.scss new file mode 100644 index 0000000..96056ed --- /dev/null +++ b/src/components/commons/overlay/overlay.scss @@ -0,0 +1,6 @@ +.mask { + @apply opacity-50 bg-white fixed top-0 bottom-0 left-0 right-0; +} +.body { + @apply fixed top-0 bottom-0 left-0 right-0; +} diff --git a/src/components/commons/overlay/overlay.scss.d.ts b/src/components/commons/overlay/overlay.scss.d.ts new file mode 100644 index 0000000..a0348e3 --- /dev/null +++ b/src/components/commons/overlay/overlay.scss.d.ts @@ -0,0 +1,11 @@ +// This file is automatically generated from your CSS. Any edits will be overwritten. +declare namespace OverlayScssNamespace { + export interface IOverlayScss { + body: string; + mask: string; + } +} + +declare const OverlayScssModule: OverlayScssNamespace.IOverlayScss; + +export = OverlayScssModule; diff --git a/src/components/commons/overlay/overlay.store.ts b/src/components/commons/overlay/overlay.store.ts new file mode 100644 index 0000000..5ee48f9 --- /dev/null +++ b/src/components/commons/overlay/overlay.store.ts @@ -0,0 +1,26 @@ +import { makeAutoObservable, observable } from 'mobx'; +import { ComponentChild } from 'preact'; + +export class OverlayStore { + @observable.ref overlays: [string, ComponentChild][] = []; + + constructor() { + makeAutoObservable(this, { + overlays: observable.shallow + }); + } + + addOverlay(id: string, overlay: ComponentChild) { + this.overlays = [...this.overlays, [id, overlay]]; + } + + removeOverlay(id: string) { + const index = this.overlays.findIndex(it => it[0] === id); + if (index === -1) { + return; + } + this.overlays.splice(index, 1); + } +} + +export const overlayStore = new OverlayStore(); diff --git a/src/components/commons/overlay/overlay.tsx b/src/components/commons/overlay/overlay.tsx new file mode 100644 index 0000000..f417399 --- /dev/null +++ b/src/components/commons/overlay/overlay.tsx @@ -0,0 +1,78 @@ +import { ComponentChild, createContext, Fragment, h } from 'preact'; +import styles from './overlay.scss'; + +import { observer } from 'mobx-react'; +import { overlayStore } from './overlay.store'; +import { useContext, useState } from 'preact/hooks'; +import { createPortal } from 'preact/compat'; + +interface Props { + content: ComponentChild; + overlayId?: string; + onClose?: () => void; + onOk?: () => void; + onCancel?: () => void; +} + +interface OverlayContext { + close: () => void; +} + +const rootElement = document.createElement('div'); +rootElement.classList.add('overlay-root'); +document.body.appendChild(rootElement); + +export const createOverlay = (props: Props) => { + return new Promise(resolve => { + const id = Math.random() + .toString(36) + .substr(2, 6); + props.overlayId = props.overlayId ?? id; + const overlay = ( + resolve(undefined)}> + ); + overlayStore.addOverlay(props.overlayId, overlay); + }); +}; + +export const overlayContext = createContext({ + close: () => undefined +}); + +const OverlayProvider = overlayContext.Provider; + +export const useOverlay = () => { + return useContext(overlayContext); +}; + +export const Overlay = (props: Props) => { + const [isVisible, setIsVisible] = useState(true); + const close = () => { + setIsVisible(false); + overlayStore.removeOverlay(props.overlayId!); + props.onClose?.(); + }; + const controller = { + close + }; + return createPortal( + isVisible ? ( + + +
+
{props.content}
+
+
+ ) : ( +
+ ), + rootElement + ); +}; + +export const OverlayContainer = observer(() => { + const list = overlayStore.overlays.map(item => ( +
{item[1]}
+ )); + return
{list}
; +}); diff --git a/src/components/projects/project-editor.scss b/src/components/projects/project-editor.scss new file mode 100644 index 0000000..80462db --- /dev/null +++ b/src/components/projects/project-editor.scss @@ -0,0 +1,34 @@ +.editor { + @apply bg-white shadow-lg p-4 rounded-lg text-gray-800 + top-1/4 left-1/2 absolute + transform -translate-x-1/2 -translate-y-1/2 + w-5/6; +} +.form { + @apply bg-white; + & > * { + @apply block; + } + label { + @apply my-4 relative flex; + } + .label { + @apply text-gray-700 w-20 inline-block text-right; + + &::after { + content: ':'; + } + } + .controller { + @apply border-b border-gray-300 flex-auto; + } + .submitBtn { + @apply px-2 py-1 rounded-full bg-red-400 text-white; + } + .cancelBtn { + @apply px-2 py-1 rounded-full bg-gray-100 text-gray-700 ml-2; + } + .footer { + @apply text-right; + } +} diff --git a/src/components/projects/project-editor.scss.d.ts b/src/components/projects/project-editor.scss.d.ts new file mode 100644 index 0000000..21468e0 --- /dev/null +++ b/src/components/projects/project-editor.scss.d.ts @@ -0,0 +1,16 @@ +// This file is automatically generated from your CSS. Any edits will be overwritten. +declare namespace ProjectEditorScssNamespace { + export interface IProjectEditorScss { + cancelBtn: string; + controller: string; + editor: string; + footer: string; + form: string; + label: string; + submitBtn: string; + } +} + +declare const ProjectEditorScssModule: ProjectEditorScssNamespace.IProjectEditorScss; + +export = ProjectEditorScssModule; diff --git a/src/components/projects/project-editor.tsx b/src/components/projects/project-editor.tsx new file mode 100644 index 0000000..0921433 --- /dev/null +++ b/src/components/projects/project-editor.tsx @@ -0,0 +1,108 @@ +import { gql, useApolloClient, useMutation } from '@apollo/client'; +import { Field, Form, Formik } from 'formik'; +import { h } from 'preact'; +import { useState } from 'preact/compat'; +import { CreateProjectInput, Project } from '../../generated/graphql'; +import { useOverlay } from '../commons/overlay/overlay'; +import styles from './project-editor.scss'; + +interface Props { + id?: string; + isCreate: boolean; +} + +const CREATE_PROJECT = gql` + mutation CreateProject($project: CreateProjectInput!) { + createProject(project: $project) { + id + name + comment + sshUrl + webUrl + webHookSecret + deletedAt + } + } +`; + +export const ProjectEditor = (props: Props) => { + useApolloClient(); + const isCreate = props.id === undefined; + + const [createProject] = useMutation(CREATE_PROJECT); + + const { close } = useOverlay(); + const cancel = (ev: MouseEvent) => { + ev.preventDefault(); + close(); + }; + + const [project] = useState({ + name: '', + comment: '', + sshUrl: '', + webHookSecret: '' + }); + const submitForm = async (values: CreateProjectInput) => { + try { + if (props.id === undefined) { + await createProject({ + variables: { project: values } + }); + } + close(); + } finally { + // + } + }; + + return ( +
+ +
+ + + + + +
+ + +
+
+
+
+ ); +}; diff --git a/src/components/projects/project-panel.scss b/src/components/projects/project-panel.scss index 73cfed1..7ddd8f8 100644 --- a/src/components/projects/project-panel.scss +++ b/src/components/projects/project-panel.scss @@ -27,3 +27,14 @@ @apply text-red-100; } } + +.operations { + @apply p-2 bg-gray-50; + + button { + @apply shadow hover:shadow-md py-1 px-2 rounded-lg transition-all; + &.primaryBtn { + @apply bg-red-400 text-white hover:bg-red-500; + } + } +} diff --git a/src/components/projects/project-panel.scss.d.ts b/src/components/projects/project-panel.scss.d.ts index 22f6ea3..a17c296 100644 --- a/src/components/projects/project-panel.scss.d.ts +++ b/src/components/projects/project-panel.scss.d.ts @@ -4,7 +4,9 @@ declare namespace ProjectPanelScssNamespace { item: string; itemActive: string; list: string; + operations: string; panel: string; + primaryBtn: string; } } diff --git a/src/components/projects/project-panel.tsx b/src/components/projects/project-panel.tsx index 6fda746..604d19a 100644 --- a/src/components/projects/project-panel.tsx +++ b/src/components/projects/project-panel.tsx @@ -1,37 +1,86 @@ -import { observer } from 'mobx-react'; -import { FunctionComponent, h } from 'preact'; +import { gql, useQuery } from '@apollo/client'; +import { useLocalStore } from 'mobx-react'; +import { h } from 'preact'; +import { forwardRef } from 'preact/compat'; +import { useImperativeHandle, useRef } from 'preact/hooks'; +import { appStore } from '../../app.store'; import { Project } from '../../generated/graphql'; -import { GlobalState, globalState } from '../../global.state'; +import { createOverlay } from '../commons/overlay/overlay'; +import { ProjectEditor } from './project-editor'; import * as styles from './project-panel.scss'; +const FIND_PROJECTS = gql` + query FindProjects { + projects: findProjects { + id + name + comment + sshUrl + webUrl + webHookSecret + deletedAt + } + } +`; +interface ListRef { + refetch: () => void; +} export function ProjectPanel() { + const listRef = useRef(); + const addProject = () => { + createOverlay({ + content: + }).then(() => { + listRef.current.refetch(); + }); + }; + return (

Fennec

- +
+ +
+
); } -const List: FunctionComponent<{ globalState: GlobalState }> = observer( - ({ globalState }) => { - const setCurrProejct = (project: Project) => { - globalState.setCurrentProject(project); - }; - console.log('change'); +const List = forwardRef((_, ref) => { + const { data, refetch } = useQuery<{ + projects: Project[]; + }>(FIND_PROJECTS); - const list = globalState.projects?.map(item => ( -
  • setCurrProejct(item)} - > -

    {item.name}

    - {item.comment} -
  • - )); - return
      {list}
    ; - } -); + const state = useLocalStore<{ currentProject?: Project }>(() => ({ + currentProject: undefined + })); + + const setCurrProject = (project: Project) => { + state.currentProject = project; + appStore.setMain(JSON.stringify(project)); + }; + + useImperativeHandle(ref, () => ({ + refetch + })); + + const list = data?.projects?.map(item => ( +
  • setCurrProject(item)} + > +

    {item.name}

    + {item.comment} +
  • + )); + return
      {list}
    ; +}); diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx index 1661e38..3916ef0 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -1,12 +1,8 @@ import { gql } from '@apollo/client'; export type Maybe = T | null; -export type Exact = { - [K in keyof T]: T[K]; -}; -export type MakeOptional = Omit & - { [SubKey in K]?: Maybe }; -export type MakeMaybe = Omit & - { [SubKey in K]: Maybe }; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -34,6 +30,7 @@ export type Project = { deletedAt?: Maybe; }; + export type Query = { __typename?: 'Query'; hello: Hello; @@ -41,6 +38,7 @@ export type Query = { findProject: Project; }; + export type QueryFindProjectArgs = { id: Scalars['String']; }; @@ -52,15 +50,18 @@ export type Mutation = { deleteProject: Scalars['Float']; }; + export type MutationCreateProjectArgs = { project: CreateProjectInput; }; + export type MutationModifyProjectArgs = { project: UpdateProjectInput; id: Scalars['String']; }; + export type MutationDeleteProjectArgs = { id: Scalars['String']; };