mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-04 00:18:40 +01:00
Change requests - add multiple reviewers (#2448)
This PR adds implements the frontend and migrations part of multiple reviewers. 2 UI parts: 1. Configuration to add the count of required approvals 2. Handle multiple approvers in review page.
This commit is contained in:
parent
f2dde9d63a
commit
9176ffae1e
@ -100,6 +100,9 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
changeRequest.state === 'In review' &&
|
changeRequest.state === 'In review' &&
|
||||||
!isAdmin;
|
!isAdmin;
|
||||||
|
|
||||||
|
const hasApprovedAlready = changeRequest.approvals.some(
|
||||||
|
approval => approval.createdBy.id === user?.id
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ChangeRequestHeader changeRequest={changeRequest} />
|
<ChangeRequestHeader changeRequest={changeRequest} />
|
||||||
@ -154,10 +157,14 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
/>
|
/>
|
||||||
<ChangeRequestReviewStatus
|
<ChangeRequestReviewStatus
|
||||||
state={changeRequest.state}
|
state={changeRequest.state}
|
||||||
|
environment={changeRequest.environment}
|
||||||
/>
|
/>
|
||||||
<StyledButtonBox>
|
<StyledButtonBox>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={changeRequest.state === 'In review'}
|
condition={
|
||||||
|
changeRequest.state === 'In review' &&
|
||||||
|
!hasApprovedAlready
|
||||||
|
}
|
||||||
show={<ReviewButton />}
|
show={<ReviewButton />}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Box, Theme, Typography, useTheme } from '@mui/material';
|
import { Box, Theme, Typography, useTheme } from '@mui/material';
|
||||||
import { ReactComponent as ChangesAppliedIcon } from 'assets/icons/merge.svg';
|
import { ReactComponent as ChangesAppliedIcon } from 'assets/icons/merge.svg';
|
||||||
import {
|
import {
|
||||||
@ -12,10 +12,35 @@ import {
|
|||||||
StyledDivider,
|
StyledDivider,
|
||||||
} from './ChangeRequestReviewStatus.styles';
|
} from './ChangeRequestReviewStatus.styles';
|
||||||
import { ChangeRequestState } from 'component/changeRequest/changeRequest.types';
|
import { ChangeRequestState } from 'component/changeRequest/changeRequest.types';
|
||||||
|
import { useRequiredPathParam } from '../../../../hooks/useRequiredPathParam';
|
||||||
|
import { useChangeRequestConfig } from '../../../../hooks/api/getters/useChangeRequestConfig/useChangeRequestConfig';
|
||||||
interface ISuggestChangeReviewsStatusProps {
|
interface ISuggestChangeReviewsStatusProps {
|
||||||
state: ChangeRequestState;
|
state: ChangeRequestState;
|
||||||
|
environment: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useChangeRequestRequiredApprovals = (projectId: string) => {
|
||||||
|
const { data } = useChangeRequestConfig(projectId);
|
||||||
|
|
||||||
|
const getChangeRequestRequiredApprovals = React.useCallback(
|
||||||
|
(environment: string): number => {
|
||||||
|
const config = data.find(draft => {
|
||||||
|
return (
|
||||||
|
draft.environment === environment &&
|
||||||
|
draft.changeRequestEnabled
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return config?.requiredApprovals || 1;
|
||||||
|
},
|
||||||
|
[data]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getChangeRequestRequiredApprovals,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const resolveBorder = (state: ChangeRequestState, theme: Theme) => {
|
const resolveBorder = (state: ChangeRequestState, theme: Theme) => {
|
||||||
if (state === 'Approved') {
|
if (state === 'Approved') {
|
||||||
return `2px solid ${theme.palette.success.main}`;
|
return `2px solid ${theme.palette.success.main}`;
|
||||||
@ -51,9 +76,8 @@ const resolveIconColors = (state: ChangeRequestState, theme: Theme) => {
|
|||||||
|
|
||||||
export const ChangeRequestReviewStatus: FC<
|
export const ChangeRequestReviewStatus: FC<
|
||||||
ISuggestChangeReviewsStatusProps
|
ISuggestChangeReviewsStatusProps
|
||||||
> = ({ state }) => {
|
> = ({ state, environment }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledOuterContainer>
|
<StyledOuterContainer>
|
||||||
<StyledButtonContainer {...resolveIconColors(state, theme)}>
|
<StyledButtonContainer {...resolveIconColors(state, theme)}>
|
||||||
@ -64,7 +88,7 @@ export const ChangeRequestReviewStatus: FC<
|
|||||||
/>
|
/>
|
||||||
</StyledButtonContainer>
|
</StyledButtonContainer>
|
||||||
<StyledReviewStatusContainer border={resolveBorder(state, theme)}>
|
<StyledReviewStatusContainer border={resolveBorder(state, theme)}>
|
||||||
<ResolveComponent state={state} />
|
<ResolveComponent state={state} environment={environment} />
|
||||||
</StyledReviewStatusContainer>
|
</StyledReviewStatusContainer>
|
||||||
</StyledOuterContainer>
|
</StyledOuterContainer>
|
||||||
);
|
);
|
||||||
@ -72,9 +96,10 @@ export const ChangeRequestReviewStatus: FC<
|
|||||||
|
|
||||||
interface IResolveComponentProps {
|
interface IResolveComponentProps {
|
||||||
state: ChangeRequestState;
|
state: ChangeRequestState;
|
||||||
|
environment: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ResolveComponent = ({ state }: IResolveComponentProps) => {
|
const ResolveComponent = ({ state, environment }: IResolveComponentProps) => {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -91,7 +116,7 @@ const ResolveComponent = ({ state }: IResolveComponentProps) => {
|
|||||||
return <Cancelled />;
|
return <Cancelled />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ReviewRequired />;
|
return <ReviewRequired environment={environment} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Approved = () => {
|
const Approved = () => {
|
||||||
@ -125,8 +150,16 @@ const Approved = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReviewRequired = () => {
|
interface IReviewRequiredProps {
|
||||||
|
environment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReviewRequired = ({ environment }: IReviewRequiredProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const { getChangeRequestRequiredApprovals } =
|
||||||
|
useChangeRequestRequiredApprovals(projectId);
|
||||||
|
const approvals = getChangeRequestRequiredApprovals(environment);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -137,7 +170,7 @@ const ReviewRequired = () => {
|
|||||||
Review required
|
Review required
|
||||||
</StyledReviewTitle>
|
</StyledReviewTitle>
|
||||||
<Typography>
|
<Typography>
|
||||||
At least 1 approving review must be submitted before
|
At least {approvals} approvals must be submitted before
|
||||||
changes can be applied
|
changes can be applied
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -18,6 +18,7 @@ export interface IChangeRequestEnvironmentConfig {
|
|||||||
environment: string;
|
environment: string;
|
||||||
type: string;
|
type: string;
|
||||||
changeRequestEnabled: boolean;
|
changeRequestEnabled: boolean;
|
||||||
|
requiredApprovals: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IChangeRequestFeature {
|
export interface IChangeRequestFeature {
|
||||||
|
@ -9,12 +9,15 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { SELECT_ITEM_ID } from 'utils/testIds';
|
import { SELECT_ITEM_ID } from 'utils/testIds';
|
||||||
import { KeyboardArrowDownOutlined } from '@mui/icons-material';
|
import { KeyboardArrowDownOutlined } from '@mui/icons-material';
|
||||||
|
import { SxProps } from '@mui/system';
|
||||||
|
import { Theme } from '@mui/material/styles';
|
||||||
|
|
||||||
export interface ISelectOption {
|
export interface ISelectOption {
|
||||||
key: string;
|
key: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGeneralSelectProps extends Omit<SelectProps, 'onChange'> {
|
export interface IGeneralSelectProps extends Omit<SelectProps, 'onChange'> {
|
||||||
@ -68,6 +71,7 @@ const GeneralSelect: React.FC<IGeneralSelectProps> = ({
|
|||||||
>
|
>
|
||||||
{options.map(option => (
|
{options.map(option => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
sx={option.sx}
|
||||||
key={option.key}
|
key={option.key}
|
||||||
value={option.key}
|
value={option.key}
|
||||||
title={option.title || ''}
|
title={option.title || ''}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState, VFC } from 'react';
|
import React, { useMemo, useState, VFC } from 'react';
|
||||||
import { HeaderGroup, useGlobalFilter, useTable } from 'react-table';
|
import { HeaderGroup, useGlobalFilter, useTable } from 'react-table';
|
||||||
import { Alert, Box, Typography } from '@mui/material';
|
import { Alert, Box, styled, Typography } from '@mui/material';
|
||||||
import {
|
import {
|
||||||
SortableTableHeader,
|
SortableTableHeader,
|
||||||
Table,
|
Table,
|
||||||
@ -17,57 +17,113 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { useChangeRequestConfig } from 'hooks/api/getters/useChangeRequestConfig/useChangeRequestConfig';
|
import { useChangeRequestConfig } from 'hooks/api/getters/useChangeRequestConfig/useChangeRequestConfig';
|
||||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
import {
|
||||||
|
IChangeRequestConfig,
|
||||||
|
useChangeRequestApi,
|
||||||
|
} from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||||
import { UPDATE_PROJECT } from '@server/types/permissions';
|
import { UPDATE_PROJECT } from '@server/types/permissions';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { ChangeRequestProcessHelp } from './ChangeRequestProcessHelp/ChangeRequestProcessHelp';
|
import { ChangeRequestProcessHelp } from './ChangeRequestProcessHelp/ChangeRequestProcessHelp';
|
||||||
|
import GeneralSelect from '../../../../common/GeneralSelect/GeneralSelect';
|
||||||
|
import { KeyboardArrowDownOutlined } from '@mui/icons-material';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
export const ChangeRequestConfiguration: VFC = () => {
|
export const ChangeRequestConfiguration: VFC = () => {
|
||||||
const [dialogState, setDialogState] = useState<{
|
const [dialogState, setDialogState] = useState<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
enableEnvironment?: string;
|
enableEnvironment: string;
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
|
requiredApprovals: number;
|
||||||
}>({
|
}>({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
enableEnvironment: '',
|
enableEnvironment: '',
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
|
requiredApprovals: 1,
|
||||||
});
|
});
|
||||||
|
const theme = useTheme();
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const { data, loading, refetchChangeRequestConfig } =
|
const { data, loading, refetchChangeRequestConfig } =
|
||||||
useChangeRequestConfig(projectId);
|
useChangeRequestConfig(projectId);
|
||||||
const { updateChangeRequestEnvironmentConfig } = useChangeRequestApi();
|
const { updateChangeRequestEnvironmentConfig } = useChangeRequestApi();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
|
||||||
const onClick = (enableEnvironment: string, isEnabled: boolean) => () => {
|
const onRowChange =
|
||||||
setDialogState({ isOpen: true, enableEnvironment, isEnabled });
|
(
|
||||||
|
enableEnvironment: string,
|
||||||
|
isEnabled: boolean,
|
||||||
|
requiredApprovals: number
|
||||||
|
) =>
|
||||||
|
() => {
|
||||||
|
setDialogState({
|
||||||
|
isOpen: true,
|
||||||
|
enableEnvironment,
|
||||||
|
isEnabled,
|
||||||
|
requiredApprovals,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onConfirm = async () => {
|
const onConfirm = async () => {
|
||||||
if (dialogState.enableEnvironment) {
|
if (dialogState.enableEnvironment) {
|
||||||
|
await updateConfiguration();
|
||||||
|
}
|
||||||
|
setDialogState({
|
||||||
|
isOpen: false,
|
||||||
|
enableEnvironment: '',
|
||||||
|
isEnabled: false,
|
||||||
|
requiredApprovals: 1,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
async function updateConfiguration(config?: IChangeRequestConfig) {
|
||||||
try {
|
try {
|
||||||
await updateChangeRequestEnvironmentConfig(
|
await updateChangeRequestEnvironmentConfig(
|
||||||
projectId,
|
config || {
|
||||||
dialogState.enableEnvironment,
|
project: projectId,
|
||||||
!dialogState.isEnabled
|
environment: dialogState.enableEnvironment,
|
||||||
|
enabled: !dialogState.isEnabled,
|
||||||
|
requiredApprovals: dialogState.requiredApprovals,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'Updated change request status',
|
title: 'Updated change request status',
|
||||||
text: 'Successfully updated change request status.',
|
text: 'Successfully updated change request status.',
|
||||||
});
|
});
|
||||||
refetchChangeRequestConfig();
|
await refetchChangeRequestConfig();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = formatUnknownError(error);
|
setToastApiError(formatUnknownError(error));
|
||||||
setToastApiError(message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setDialogState({
|
|
||||||
isOpen: false,
|
const approvalOptions = Array.from(Array(10).keys())
|
||||||
enableEnvironment: '',
|
.map(key => String(key + 1))
|
||||||
isEnabled: false,
|
.map(key => {
|
||||||
});
|
const labelText = key === '1' ? 'approval' : 'approvals';
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
label: `${key} ${labelText}`,
|
||||||
|
sx: { 'font-size': theme.fontSizes.smallBody },
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function onRequiredApprovalsChange(original: any, approvals: string) {
|
||||||
|
updateConfiguration({
|
||||||
|
project: projectId,
|
||||||
|
environment: original.environment,
|
||||||
|
enabled: original.changeRequestEnabled,
|
||||||
|
requiredApprovals: Number(approvals),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@ -82,6 +138,34 @@ export const ChangeRequestConfiguration: VFC = () => {
|
|||||||
disableGlobalFilter: true,
|
disableGlobalFilter: true,
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Header: 'Required approvals',
|
||||||
|
align: 'center',
|
||||||
|
Cell: ({ row: { original } }: any) => (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={original.changeRequestEnabled}
|
||||||
|
show={
|
||||||
|
<StyledBox data-loading>
|
||||||
|
<GeneralSelect
|
||||||
|
options={approvalOptions}
|
||||||
|
value={original.requiredApprovals || 1}
|
||||||
|
onChange={approvals => {
|
||||||
|
onRequiredApprovalsChange(
|
||||||
|
original,
|
||||||
|
approvals
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
IconComponent={KeyboardArrowDownOutlined}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</StyledBox>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
width: 100,
|
||||||
|
disableGlobalFilter: true,
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Header: 'Status',
|
Header: 'Status',
|
||||||
accessor: 'changeRequestEnabled',
|
accessor: 'changeRequestEnabled',
|
||||||
@ -89,22 +173,20 @@ export const ChangeRequestConfiguration: VFC = () => {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
|
|
||||||
Cell: ({ value, row: { original } }: any) => (
|
Cell: ({ value, row: { original } }: any) => (
|
||||||
<Box
|
<StyledBox data-loading>
|
||||||
sx={{ display: 'flex', justifyContent: 'center' }}
|
|
||||||
data-loading
|
|
||||||
>
|
|
||||||
<PermissionSwitch
|
<PermissionSwitch
|
||||||
checked={value}
|
checked={value}
|
||||||
environmentId={original.environment}
|
environmentId={original.environment}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
permission={UPDATE_PROJECT}
|
permission={UPDATE_PROJECT}
|
||||||
inputProps={{ 'aria-label': original.environment }}
|
inputProps={{ 'aria-label': original.environment }}
|
||||||
onClick={onClick(
|
onClick={onRowChange(
|
||||||
original.environment,
|
original.environment,
|
||||||
original.changeRequestEnabled
|
original.changeRequestEnabled,
|
||||||
|
original.requiredApprovals
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</StyledBox>
|
||||||
),
|
),
|
||||||
width: 100,
|
width: 100,
|
||||||
disableGlobalFilter: true,
|
disableGlobalFilter: true,
|
||||||
|
@ -10,6 +10,13 @@ export interface IChangeRequestsSchema {
|
|||||||
payload: string | boolean | object | number;
|
payload: string | boolean | object | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IChangeRequestConfig {
|
||||||
|
project: string;
|
||||||
|
environment: string;
|
||||||
|
enabled: boolean;
|
||||||
|
requiredApprovals: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const useChangeRequestApi = () => {
|
export const useChangeRequestApi = () => {
|
||||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||||
propagateErrors: true,
|
propagateErrors: true,
|
||||||
@ -67,15 +74,19 @@ export const useChangeRequestApi = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateChangeRequestEnvironmentConfig = async (
|
const updateChangeRequestEnvironmentConfig = async ({
|
||||||
project: string,
|
project,
|
||||||
environment: string,
|
enabled,
|
||||||
enabled: boolean
|
environment,
|
||||||
) => {
|
requiredApprovals,
|
||||||
|
}: IChangeRequestConfig) => {
|
||||||
const path = `api/admin/projects/${project}/environments/${environment}/change-requests/config`;
|
const path = `api/admin/projects/${project}/environments/${environment}/change-requests/config`;
|
||||||
const req = createRequest(path, {
|
const req = createRequest(path, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ changeRequestsEnabled: enabled }),
|
body: JSON.stringify({
|
||||||
|
changeRequestsEnabled: enabled,
|
||||||
|
requiredApprovals,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -16,8 +16,7 @@ import {
|
|||||||
ProjectGroupRemovedEvent,
|
ProjectGroupRemovedEvent,
|
||||||
ProjectGroupUpdateRoleEvent,
|
ProjectGroupUpdateRoleEvent,
|
||||||
} from '../types/events';
|
} from '../types/events';
|
||||||
import { IUnleashStores } from '../types';
|
import { IUnleashStores, IUnleashConfig } from '../types';
|
||||||
import { IUnleashConfig } from '../types/option';
|
|
||||||
import {
|
import {
|
||||||
FeatureToggle,
|
FeatureToggle,
|
||||||
IProject,
|
IProject,
|
||||||
@ -195,6 +194,13 @@ export default class ProjectService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addEnvironmentToProject(
|
||||||
|
project: string,
|
||||||
|
environment: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.store.addEnvironmentToProject(project, environment);
|
||||||
|
}
|
||||||
|
|
||||||
async changeProject(
|
async changeProject(
|
||||||
newProjectId: string,
|
newProjectId: string,
|
||||||
featureName: string,
|
featureName: string,
|
||||||
|
25
src/migrations/20221115072335-add-required-approvals.js
Normal file
25
src/migrations/20221115072335-add-required-approvals.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function (db, callback) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
ALTER TABLE change_request_settings ADD COLUMN required_approvals integer default 1;
|
||||||
|
|
||||||
|
ALTER TABLE change_request_settings
|
||||||
|
ADD CONSTRAINT change_request_settings_project_environment_key
|
||||||
|
UNIQUE (project, environment)
|
||||||
|
`,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (db, callback) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
ALTER TABLE change_request_settings DROP COLUMN IF EXISTS required_approvals;
|
||||||
|
ALTER TABLE change_request_settings
|
||||||
|
DROP CONSTRAINT IF EXISTS change_request_settings_project_environment_key;
|
||||||
|
`,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user