Add GQL mutation for updating project data in general settings tab (#41)
* Use update project gql client method in UI * Handle save project button based on form change * Fix import order --------- Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
parent
61120ac44a
commit
5310d7c7d0
@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useCombobox } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
|
||||
import {
|
||||
List,
|
||||
@ -9,8 +10,6 @@ import {
|
||||
Typography,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import { Project } from 'gql-client';
|
||||
|
||||
import SearchBar from '../SearchBar';
|
||||
import { useGQLClient } from '../../context/GQLClientContext';
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Project } from 'gql-client';
|
||||
|
||||
import { Typography, Button, Chip } from '@material-tailwind/react';
|
||||
|
||||
@ -8,17 +9,19 @@ import { ProjectDetails } from '../../../types/project';
|
||||
import { relativeTimeMs } from '../../../utils/time';
|
||||
|
||||
interface OverviewProps {
|
||||
project: ProjectDetails;
|
||||
project: Project;
|
||||
organizationProject: ProjectDetails;
|
||||
}
|
||||
|
||||
const OverviewTabPanel = ({ project }: OverviewProps) => {
|
||||
const OverviewTabPanel = ({ project, organizationProject }: OverviewProps) => {
|
||||
// TODO: Fetch current deployment
|
||||
const currentDeploymentTitle = useMemo(() => {
|
||||
const deployment = project.deployments.find((deployment) => {
|
||||
const deployment = organizationProject?.deployments.find((deployment) => {
|
||||
return deployment.isCurrent === true;
|
||||
});
|
||||
|
||||
return deployment?.title;
|
||||
}, []);
|
||||
}, [organizationProject]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-5">
|
||||
@ -28,14 +31,14 @@ const OverviewTabPanel = ({ project }: OverviewProps) => {
|
||||
<div className="grow">
|
||||
<Typography>{project.name}</Typography>
|
||||
<Typography variant="small" color="gray">
|
||||
{project.url}
|
||||
{organizationProject.url}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between p-2 text-sm items-center">
|
||||
<div>
|
||||
^ Domain
|
||||
{!project.domain && (
|
||||
{!organizationProject.domain && (
|
||||
<Chip
|
||||
className="normal-case ml-6 bg-[#FED7AA] text-[#EA580C] inline font-normal"
|
||||
size="sm"
|
||||
@ -44,8 +47,8 @@ const OverviewTabPanel = ({ project }: OverviewProps) => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{project.domain ? (
|
||||
<p>{project.domain}</p>
|
||||
{organizationProject.domain ? (
|
||||
<p>{organizationProject.domain}</p>
|
||||
) : (
|
||||
<Button className="normal-case rounded-full" color="blue" size="sm">
|
||||
Setup
|
||||
@ -54,7 +57,7 @@ const OverviewTabPanel = ({ project }: OverviewProps) => {
|
||||
</div>
|
||||
<div className="flex justify-between p-2 text-sm">
|
||||
<p>^ Source</p>
|
||||
<p>{project.source}</p>
|
||||
<p>{organizationProject.source}</p>
|
||||
</div>
|
||||
<div className="flex justify-between p-2 text-sm">
|
||||
<p>^ Deployment</p>
|
||||
@ -63,7 +66,7 @@ const OverviewTabPanel = ({ project }: OverviewProps) => {
|
||||
<div className="flex justify-between p-2 text-sm">
|
||||
<p>^ Created</p>
|
||||
<p>
|
||||
{relativeTimeMs(project.createdAt)} by ^ {project.createdBy}
|
||||
{relativeTimeMs(project.createdAt)} by ^ {project.owner.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
||||
import { Project } from 'gql-client';
|
||||
|
||||
import OverviewTabPanel from './OverviewTabPanel';
|
||||
import DeploymentsTabPanel from './DeploymentsTabPanel';
|
||||
@ -7,7 +8,9 @@ import { ProjectDetails } from '../../../types/project';
|
||||
import SettingsTabPanel from './SettingsTabPanel';
|
||||
|
||||
interface ProjectTabsProps {
|
||||
project: ProjectDetails;
|
||||
project: Project;
|
||||
organizationProject: ProjectDetails;
|
||||
onUpdate: () => Promise<void>;
|
||||
}
|
||||
|
||||
const Database = () => (
|
||||
@ -28,7 +31,11 @@ const Integrations = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const ProjectTabs = ({ project }: ProjectTabsProps) => {
|
||||
const ProjectTabs = ({
|
||||
project,
|
||||
onUpdate,
|
||||
organizationProject,
|
||||
}: ProjectTabsProps) => {
|
||||
return (
|
||||
<Tabs
|
||||
selectedTabClassName={
|
||||
@ -43,7 +50,10 @@ const ProjectTabs = ({ project }: ProjectTabsProps) => {
|
||||
<Tab className={'p-2 cursor-pointer'}>Settings</Tab>
|
||||
</TabList>
|
||||
<TabPanel>
|
||||
<OverviewTabPanel project={project} />
|
||||
<OverviewTabPanel
|
||||
project={project}
|
||||
organizationProject={organizationProject}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<DeploymentsTabPanel projectId={project.id} />
|
||||
@ -55,7 +65,7 @@ const ProjectTabs = ({ project }: ProjectTabsProps) => {
|
||||
<Integrations />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SettingsTabPanel />
|
||||
<SettingsTabPanel project={project} onUpdate={onUpdate} />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
);
|
||||
|
@ -1,4 +1,6 @@
|
||||
import React, { createElement } from 'react';
|
||||
import { Project } from 'gql-client';
|
||||
|
||||
import {
|
||||
Tabs,
|
||||
TabsHeader,
|
||||
@ -46,7 +48,13 @@ const tabsData = [
|
||||
},
|
||||
];
|
||||
|
||||
const SettingsTabPanel = () => {
|
||||
const SettingsTabPanel = ({
|
||||
project,
|
||||
onUpdate,
|
||||
}: {
|
||||
project: Project;
|
||||
onUpdate: () => Promise<void>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
@ -72,7 +80,10 @@ const SettingsTabPanel = () => {
|
||||
<TabsBody className="col-span-2">
|
||||
{tabsData.map(({ value, component }) => (
|
||||
<TabPanel key={value} value={value} className="p-2">
|
||||
{createElement(component)}
|
||||
{createElement(component, {
|
||||
project: project,
|
||||
onUpdate: onUpdate,
|
||||
})}
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabsBody>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconButton, Input, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { EnvironmentVariable } from 'gql-client';
|
||||
|
||||
import { IconButton, Input, Typography } from '@material-tailwind/react';
|
||||
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
|
||||
const ShowPasswordIcon = ({
|
||||
|
@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Environment, EnvironmentVariable } from 'gql-client';
|
||||
|
||||
import {
|
||||
Typography,
|
||||
@ -12,8 +13,6 @@ import {
|
||||
Chip,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import { Environment, EnvironmentVariable } from 'gql-client';
|
||||
|
||||
import AddEnvironmentVariableRow from './AddEnvironmentVariableRow';
|
||||
import DisplayEnvironmentVariables from './DisplayEnvironmentVariables';
|
||||
import HorizontalLine from '../../../HorizontalLine';
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Link, useOutletContext, useParams } from 'react-router-dom';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Project } from 'gql-client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@ -13,7 +14,7 @@ import {
|
||||
|
||||
import DeleteProjectDialog from './DeleteProjectDialog';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import { ProjectSearchOutletContext } from '../../../../types/project';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
|
||||
const TEAMS = ['Airfoil'];
|
||||
const DEFAULT_SELECT_TEAM = undefined;
|
||||
@ -32,13 +33,14 @@ const CopyIcon = ({ value }: { value: string }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const GeneralTabPanel = () => {
|
||||
const { id } = useParams();
|
||||
const { projects } = useOutletContext<ProjectSearchOutletContext>();
|
||||
|
||||
const currentProject = useMemo(() => {
|
||||
return projects.find((project: any) => project.id === id);
|
||||
}, [id]);
|
||||
const GeneralTabPanel = ({
|
||||
project,
|
||||
onUpdate,
|
||||
}: {
|
||||
project: Project;
|
||||
onUpdate: () => Promise<void>;
|
||||
}) => {
|
||||
const client = useGQLClient();
|
||||
|
||||
const {
|
||||
handleSubmit: handleTransfer,
|
||||
@ -58,18 +60,35 @@ const GeneralTabPanel = () => {
|
||||
const handleDeleteProjectDialog = () =>
|
||||
setOpenDeleteDialog(!openDeleteDialog);
|
||||
|
||||
const { handleSubmit, register } = useForm({
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
formState: updateProjectFormState,
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
appName: currentProject?.name,
|
||||
description: currentProject?.description,
|
||||
appName: project.name,
|
||||
description: project.description,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset({ appName: project.name, description: project.description });
|
||||
}, [project]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentProject && (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(() => {})}>
|
||||
<form
|
||||
onSubmit={handleSubmit(async ({ appName, description }) => {
|
||||
const { updateProject } = await client.updateProject(project.id, {
|
||||
name: appName,
|
||||
description,
|
||||
});
|
||||
if (updateProject) {
|
||||
await onUpdate();
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Typography variant="h6">Project info</Typography>
|
||||
<Typography variant="small" className="font-medium text-gray-800">
|
||||
App name
|
||||
@ -96,20 +115,26 @@ const GeneralTabPanel = () => {
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
variant="outlined"
|
||||
value={currentProject.id}
|
||||
value={project.id}
|
||||
size="md"
|
||||
disabled
|
||||
icon={<CopyIcon value={currentProject.id} />}
|
||||
icon={<CopyIcon value={project.id} />}
|
||||
/>
|
||||
<Button type="submit" variant="gradient" size="sm" className="mt-1">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="gradient"
|
||||
size="sm"
|
||||
className="mt-1"
|
||||
disabled={!updateProjectFormState.isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
<div className="mb-1">
|
||||
<Typography variant="h6">Transfer project</Typography>
|
||||
<Typography variant="small">
|
||||
Transfer this app to your personal account or a team you are a
|
||||
member of.
|
||||
Transfer this app to your personal account or a team you are a member
|
||||
of.
|
||||
<Link to="" className="text-blue-500">
|
||||
Learn more
|
||||
</Link>
|
||||
@ -167,8 +192,8 @@ const GeneralTabPanel = () => {
|
||||
<div className="mb-1">
|
||||
<Typography variant="h6">Delete project</Typography>
|
||||
<Typography variant="small">
|
||||
The project will be permanently deleted, including its deployments
|
||||
and domains. This action is irreversible and can not be undone.
|
||||
The project will be permanently deleted, including its deployments and
|
||||
domains. This action is irreversible and can not be undone.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="gradient"
|
||||
@ -185,8 +210,6 @@ const GeneralTabPanel = () => {
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Environment } from 'gql-client';
|
||||
|
||||
import HorizontalLine from '../components/HorizontalLine';
|
||||
import { IconButton, Typography } from '@material-tailwind/react';
|
||||
|
||||
import HorizontalLine from '../components/HorizontalLine';
|
||||
import ProjectSearchBar from '../components/projects/ProjectSearchBar';
|
||||
import { useGQLClient } from '../context/GQLClientContext';
|
||||
import { ProjectDetails } from '../types/project';
|
||||
|
@ -1,27 +1,49 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate, useOutletContext, useParams } from 'react-router-dom';
|
||||
import { Project as ProjectType } from 'gql-client';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import HorizontalLine from '../../components/HorizontalLine';
|
||||
import ProjectTabs from '../../components/projects/project/ProjectTabs';
|
||||
import { useGQLClient } from '../../context/GQLClientContext';
|
||||
import { ProjectSearchOutletContext } from '../../types/project';
|
||||
|
||||
const Project = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const client = useGQLClient();
|
||||
|
||||
const { projects } = useOutletContext<ProjectSearchOutletContext>();
|
||||
const [project, setProject] = useState<ProjectType | null>(null);
|
||||
|
||||
const project = useMemo(() => {
|
||||
return projects.find((project) => {
|
||||
const fetchProject = useCallback(async (id: string | undefined) => {
|
||||
if (id) {
|
||||
const { project } = await client.getProject(id);
|
||||
setProject(project);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProject(id);
|
||||
}, [id]);
|
||||
|
||||
const onUpdate = async () => {
|
||||
await fetchProject(id);
|
||||
};
|
||||
|
||||
// TODO: Remove organization projects
|
||||
const { projects: organizationProjects } =
|
||||
useOutletContext<ProjectSearchOutletContext>();
|
||||
|
||||
const organizationProject = useMemo(() => {
|
||||
return organizationProjects.find((project) => {
|
||||
return project.id === id;
|
||||
});
|
||||
}, [id, projects]);
|
||||
}, [id, organizationProjects]);
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
{project ? (
|
||||
{project && organizationProject ? (
|
||||
<>
|
||||
<div className="flex p-4 gap-4 items-center">
|
||||
<Button
|
||||
@ -32,7 +54,7 @@ const Project = () => {
|
||||
{'<'}
|
||||
</Button>
|
||||
<Typography variant="h3" className="grow">
|
||||
{project?.title}
|
||||
{project?.name}
|
||||
</Typography>
|
||||
<Button className="rounded-full" variant="outlined">
|
||||
Open Repo
|
||||
@ -43,7 +65,11 @@ const Project = () => {
|
||||
</div>
|
||||
<HorizontalLine />
|
||||
<div className="p-4">
|
||||
<ProjectTabs project={project} />
|
||||
<ProjectTabs
|
||||
project={project}
|
||||
organizationProject={organizationProject}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
|
||||
|
||||
import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables } from './queries';
|
||||
import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse } from './types';
|
||||
import { removeMember, addEnvironmentVariables, updateDeploymentToProd } from './mutations';
|
||||
import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject } from './queries';
|
||||
import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput } from './types';
|
||||
import { removeMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation } from './mutations';
|
||||
|
||||
export interface GraphQLConfig {
|
||||
gqlEndpoint: string;
|
||||
@ -39,6 +39,17 @@ export class GQLClient {
|
||||
return data;
|
||||
}
|
||||
|
||||
async getProject (projectId: string) : Promise<GetProjectResponse> {
|
||||
const { data } = await this.client.query({
|
||||
query: getProject,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getOrganizations () : Promise<GetOrganizationsResponse> {
|
||||
const { data } = await this.client.query({
|
||||
query: getOrganizations
|
||||
@ -124,4 +135,16 @@ export class GQLClient {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async updateProject (projectId: string, updateProject: UpdateProjectInput): Promise<UpdateProjectResponse> {
|
||||
const { data } = await this.client.mutate({
|
||||
mutation: updateProjectMutation,
|
||||
variables: {
|
||||
projectId,
|
||||
updateProject
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
@ -17,3 +17,9 @@ mutation ($deploymentId: String!) {
|
||||
updateDeploymentToProd(deploymentId: $deploymentId)
|
||||
}
|
||||
`;
|
||||
|
||||
export const updateProjectMutation = gql`
|
||||
mutation ($projectId: String!, $updateProject: UpdateProjectInput) {
|
||||
updateProject(projectId: $projectId, updateProject: $updateProject)
|
||||
}
|
||||
`;
|
||||
|
@ -12,6 +12,28 @@ query {
|
||||
}
|
||||
`;
|
||||
|
||||
export const getProject = gql`
|
||||
query ($projectId: String!) {
|
||||
project(projectId: $projectId) {
|
||||
createdAt
|
||||
description
|
||||
id
|
||||
name
|
||||
template
|
||||
updatedAt
|
||||
prodBranch
|
||||
framework
|
||||
repository
|
||||
webhooks
|
||||
owner {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getOrganizations = gql`
|
||||
query {
|
||||
organizations {
|
||||
|
@ -152,6 +152,10 @@ export type GetUserResponse = {
|
||||
user: User
|
||||
}
|
||||
|
||||
export type GetProjectResponse = {
|
||||
project: Project | null
|
||||
}
|
||||
|
||||
export type SearchProjectsResponse = {
|
||||
searchProjects: Project[]
|
||||
}
|
||||
@ -169,3 +173,12 @@ export type AddEnvironmentVariableInput = {
|
||||
export type UpdateDeploymentToProdResponse = {
|
||||
updateDeploymentToProd: boolean;
|
||||
}
|
||||
|
||||
export type UpdateProjectResponse = {
|
||||
updateProject: boolean;
|
||||
}
|
||||
|
||||
export type UpdateProjectInput = {
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user