feat: css template add/edit modal (#11296)
This commit is contained in:
parent
9f8d0e7a06
commit
6f3d4c131f
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 React from 'react';
|
||||
import thunk from 'redux-thunk';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import CssTemplateModal from 'src/views/CRUD/csstemplates/CssTemplateModal';
|
||||
import Modal from 'src/common/components/Modal';
|
||||
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||
import { CssEditor } from 'src/components/AsyncAceEditor';
|
||||
import { styledMount as mount } from 'spec/helpers/theming';
|
||||
|
||||
const mockData = { id: 1, template_name: 'test' };
|
||||
const FETCH_CSS_TEMPLATE_ENDPOINT = 'glob:*/api/v1/css_template/*';
|
||||
const CSS_TEMPLATE_PAYLOAD = { result: mockData };
|
||||
|
||||
fetchMock.get(FETCH_CSS_TEMPLATE_ENDPOINT, CSS_TEMPLATE_PAYLOAD);
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
const store = mockStore({});
|
||||
|
||||
const mockedProps = {
|
||||
addDangerToast: () => {},
|
||||
onCssTemplateAdd: jest.fn(() => []),
|
||||
onHide: () => {},
|
||||
show: true,
|
||||
cssTemplate: mockData,
|
||||
};
|
||||
|
||||
async function mountAndWait(props = mockedProps) {
|
||||
const mounted = mount(<CssTemplateModal show {...props} />, {
|
||||
context: { store },
|
||||
});
|
||||
await waitForComponentToPaint(mounted);
|
||||
|
||||
return mounted;
|
||||
}
|
||||
|
||||
describe('CssTemplateModal', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeAll(async () => {
|
||||
wrapper = await mountAndWait();
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.find(CssTemplateModal)).toExist();
|
||||
});
|
||||
|
||||
it('renders a Modal', () => {
|
||||
expect(wrapper.find(Modal)).toExist();
|
||||
});
|
||||
|
||||
it('renders add header when no css template is included', async () => {
|
||||
const addWrapper = await mountAndWait({});
|
||||
expect(
|
||||
addWrapper.find('[data-test="css-template-modal-title"]').text(),
|
||||
).toEqual('Add CSS Template');
|
||||
});
|
||||
|
||||
it('renders edit header when css template prop is included', () => {
|
||||
expect(
|
||||
wrapper.find('[data-test="css-template-modal-title"]').text(),
|
||||
).toEqual('Edit CSS Template Properties');
|
||||
});
|
||||
|
||||
it('renders input elements for template name', () => {
|
||||
expect(wrapper.find('input[name="template_name"]')).toExist();
|
||||
});
|
||||
|
||||
it('renders css editor for css', () => {
|
||||
expect(wrapper.find(CssEditor)).toExist();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 React, { FunctionComponent, useState, useEffect } from 'react';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { useSingleViewResource } from 'src/views/CRUD/hooks';
|
||||
|
||||
import Icon from 'src/components/Icon';
|
||||
import Modal from 'src/common/components/Modal';
|
||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||
import { CssEditor } from 'src/components/AsyncAceEditor';
|
||||
|
||||
import { TemplateObject } from './types';
|
||||
|
||||
interface CssTemplateModalProps {
|
||||
addDangerToast: (msg: string) => void;
|
||||
cssTemplate?: TemplateObject | null;
|
||||
onCssTemplateAdd?: (cssTemplate?: TemplateObject) => void;
|
||||
onHide: () => void;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
const StyledCssTemplateTitle = styled.div`
|
||||
margin: ${({ theme }) => theme.gridUnit * 2}px auto
|
||||
${({ theme }) => theme.gridUnit * 4}px auto;
|
||||
`;
|
||||
|
||||
const StyledCssEditor = styled(CssEditor)`
|
||||
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||
border: 1px solid ${({ theme }) => theme.colors.secondary.light2};
|
||||
`;
|
||||
|
||||
const StyledIcon = styled(Icon)`
|
||||
margin: auto ${({ theme }) => theme.gridUnit * 2}px auto 0;
|
||||
`;
|
||||
|
||||
const TemplateContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.gridUnit * 10}px;
|
||||
|
||||
.control-label {
|
||||
margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
}
|
||||
|
||||
.required {
|
||||
margin-left: ${({ theme }) => theme.gridUnit / 2}px;
|
||||
color: ${({ theme }) => theme.colors.error.base};
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
padding: ${({ theme }) => theme.gridUnit * 1.5}px
|
||||
${({ theme }) => theme.gridUnit * 2}px;
|
||||
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||
border-radius: ${({ theme }) => theme.gridUnit}px;
|
||||
width: 50%;
|
||||
}
|
||||
`;
|
||||
|
||||
const CssTemplateModal: FunctionComponent<CssTemplateModalProps> = ({
|
||||
addDangerToast,
|
||||
onCssTemplateAdd,
|
||||
onHide,
|
||||
show,
|
||||
cssTemplate = null,
|
||||
}) => {
|
||||
const [disableSave, setDisableSave] = useState<boolean>(true);
|
||||
const [
|
||||
currentCssTemplate,
|
||||
setCurrentCssTemplate,
|
||||
] = useState<TemplateObject | null>(null);
|
||||
const [isHidden, setIsHidden] = useState<boolean>(true);
|
||||
const isEditMode = cssTemplate !== null;
|
||||
|
||||
// cssTemplate fetch logic
|
||||
const {
|
||||
state: { loading, resource },
|
||||
fetchResource,
|
||||
createResource,
|
||||
updateResource,
|
||||
} = useSingleViewResource<TemplateObject>(
|
||||
'css_template',
|
||||
t('css_template'),
|
||||
addDangerToast,
|
||||
);
|
||||
|
||||
// Functions
|
||||
const hide = () => {
|
||||
setIsHidden(true);
|
||||
onHide();
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
if (isEditMode) {
|
||||
// Edit
|
||||
if (currentCssTemplate && currentCssTemplate.id) {
|
||||
const update_id = currentCssTemplate.id;
|
||||
delete currentCssTemplate.id;
|
||||
delete currentCssTemplate.created_by;
|
||||
updateResource(update_id, currentCssTemplate).then(() => {
|
||||
if (onCssTemplateAdd) {
|
||||
onCssTemplateAdd();
|
||||
}
|
||||
|
||||
hide();
|
||||
});
|
||||
}
|
||||
} else if (currentCssTemplate) {
|
||||
// Create
|
||||
createResource(currentCssTemplate).then(() => {
|
||||
if (onCssTemplateAdd) {
|
||||
onCssTemplateAdd();
|
||||
}
|
||||
|
||||
hide();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onTemplateNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { target } = event;
|
||||
|
||||
const data = {
|
||||
...currentCssTemplate,
|
||||
template_name: currentCssTemplate ? currentCssTemplate.template_name : '',
|
||||
css: currentCssTemplate ? currentCssTemplate.css : '',
|
||||
};
|
||||
|
||||
data[target.name] = target.value;
|
||||
setCurrentCssTemplate(data);
|
||||
};
|
||||
|
||||
const onCssChange = (css: string) => {
|
||||
const data = {
|
||||
...currentCssTemplate,
|
||||
template_name: currentCssTemplate ? currentCssTemplate.template_name : '',
|
||||
css,
|
||||
};
|
||||
setCurrentCssTemplate(data);
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
if (
|
||||
currentCssTemplate &&
|
||||
currentCssTemplate.template_name.length &&
|
||||
currentCssTemplate.css &&
|
||||
currentCssTemplate.css.length
|
||||
) {
|
||||
setDisableSave(false);
|
||||
} else {
|
||||
setDisableSave(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize
|
||||
if (
|
||||
isEditMode &&
|
||||
(!currentCssTemplate ||
|
||||
!currentCssTemplate.id ||
|
||||
(cssTemplate && cssTemplate.id !== currentCssTemplate.id) ||
|
||||
(isHidden && show))
|
||||
) {
|
||||
if (cssTemplate && cssTemplate.id !== null && !loading) {
|
||||
const id = cssTemplate.id || 0;
|
||||
|
||||
fetchResource(id).then(() => {
|
||||
setCurrentCssTemplate(resource);
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
!isEditMode &&
|
||||
(!currentCssTemplate || currentCssTemplate.id || (isHidden && show))
|
||||
) {
|
||||
setCurrentCssTemplate({
|
||||
template_name: '',
|
||||
css: '',
|
||||
});
|
||||
}
|
||||
|
||||
// Validation
|
||||
useEffect(() => {
|
||||
validate();
|
||||
}, [
|
||||
currentCssTemplate ? currentCssTemplate.template_name : '',
|
||||
currentCssTemplate ? currentCssTemplate.css : '',
|
||||
]);
|
||||
|
||||
// Show/hide
|
||||
if (isHidden && show) {
|
||||
setIsHidden(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
disablePrimaryButton={disableSave}
|
||||
onHandledPrimaryAction={onSave}
|
||||
onHide={hide}
|
||||
primaryButtonName={isEditMode ? t('Save') : t('Add')}
|
||||
show={show}
|
||||
width="55%"
|
||||
title={
|
||||
<h4 data-test="css-template-modal-title">
|
||||
{isEditMode ? (
|
||||
<StyledIcon name="edit-alt" />
|
||||
) : (
|
||||
<StyledIcon name="plus-large" />
|
||||
)}
|
||||
{isEditMode
|
||||
? t('Edit CSS Template Properties')
|
||||
: t('Add CSS Template')}
|
||||
</h4>
|
||||
}
|
||||
>
|
||||
<StyledCssTemplateTitle>
|
||||
<h4>{t('Basic Information')}</h4>
|
||||
</StyledCssTemplateTitle>
|
||||
<TemplateContainer>
|
||||
<div className="control-label">
|
||||
{t('css template name')}
|
||||
<span className="required">*</span>
|
||||
</div>
|
||||
<input
|
||||
name="template_name"
|
||||
onChange={onTemplateNameChange}
|
||||
type="text"
|
||||
value={currentCssTemplate?.template_name}
|
||||
/>
|
||||
</TemplateContainer>
|
||||
<TemplateContainer>
|
||||
<div className="control-label">
|
||||
{t('css')}
|
||||
<span className="required">*</span>
|
||||
</div>
|
||||
<StyledCssEditor
|
||||
onChange={onCssChange}
|
||||
value={currentCssTemplate?.css}
|
||||
width="100%"
|
||||
/>
|
||||
</TemplateContainer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default withToasts(CssTemplateModal);
|
||||
|
|
@ -17,16 +17,18 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import moment from 'moment';
|
||||
import { useListViewResource } from 'src/views/CRUD/hooks';
|
||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||
import SubMenu from 'src/components/Menu/SubMenu';
|
||||
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
||||
import { IconName } from 'src/components/Icon';
|
||||
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
|
||||
// import ListView, { Filters } from 'src/components/ListView';
|
||||
import ListView from 'src/components/ListView';
|
||||
import CssTemplateModal from './CssTemplateModal';
|
||||
import { TemplateObject } from './types';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
|
||||
|
|
@ -35,19 +37,6 @@ interface CssTemplatesListProps {
|
|||
addSuccessToast: (msg: string) => void;
|
||||
}
|
||||
|
||||
type TemplateObject = {
|
||||
id?: number;
|
||||
changed_on_delta_humanized: string;
|
||||
created_on: string;
|
||||
created_by: {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
};
|
||||
css: string;
|
||||
template_name: string;
|
||||
};
|
||||
|
||||
function CssTemplatesList({
|
||||
addDangerToast,
|
||||
addSuccessToast,
|
||||
|
|
@ -60,17 +49,29 @@ function CssTemplatesList({
|
|||
},
|
||||
hasPerm,
|
||||
fetchData,
|
||||
// refreshData,
|
||||
refreshData,
|
||||
} = useListViewResource<TemplateObject>(
|
||||
'css_template',
|
||||
t('css templates'),
|
||||
addDangerToast,
|
||||
);
|
||||
const [cssTemplateModalOpen, setCssTemplateModalOpen] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const [
|
||||
currentCssTemplate,
|
||||
setCurrentCssTemplate,
|
||||
] = useState<TemplateObject | null>(null);
|
||||
|
||||
const canCreate = hasPerm('can_add');
|
||||
const canEdit = hasPerm('can_edit');
|
||||
const canDelete = hasPerm('can_delete');
|
||||
|
||||
function handleCssTemplateEdit(cssTemplate: TemplateObject) {
|
||||
setCurrentCssTemplate(cssTemplate);
|
||||
setCssTemplateModalOpen(true);
|
||||
}
|
||||
|
||||
const initialSort = [{ id: 'template_name', desc: true }];
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
|
|
@ -127,7 +128,7 @@ function CssTemplatesList({
|
|||
},
|
||||
{
|
||||
Cell: ({ row: { original } }: any) => {
|
||||
const handleEdit = () => {}; // handleDatabaseEdit(original);
|
||||
const handleEdit = () => handleCssTemplateEdit(original);
|
||||
const handleDelete = () => {}; // openDatabaseDeleteModal(original);
|
||||
|
||||
const actions = [
|
||||
|
|
@ -166,9 +167,34 @@ function CssTemplatesList({
|
|||
[canDelete, canCreate],
|
||||
);
|
||||
|
||||
const subMenuButtons: SubMenuProps['buttons'] = [];
|
||||
|
||||
if (canCreate) {
|
||||
subMenuButtons.push({
|
||||
name: (
|
||||
<>
|
||||
{' '}
|
||||
<i className="fa fa-plus" /> {t('Css Template')}
|
||||
</>
|
||||
),
|
||||
buttonStyle: 'primary',
|
||||
onClick: () => {
|
||||
setCurrentCssTemplate(null);
|
||||
setCssTemplateModalOpen(true);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubMenu name={t('CSS Templates')} />
|
||||
<SubMenu name={t('CSS Templates')} buttons={subMenuButtons} />
|
||||
<CssTemplateModal
|
||||
addDangerToast={addDangerToast}
|
||||
cssTemplate={currentCssTemplate}
|
||||
onCssTemplateAdd={() => refreshData()}
|
||||
onHide={() => setCssTemplateModalOpen(false)}
|
||||
show={cssTemplateModalOpen}
|
||||
/>
|
||||
<ListView<TemplateObject>
|
||||
className="css-templates-list-view"
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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.
|
||||
*/
|
||||
type CreatedByUser = {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
};
|
||||
|
||||
export type TemplateObject = {
|
||||
id?: number;
|
||||
changed_on_delta_humanized?: string;
|
||||
created_on?: string;
|
||||
created_by?: CreatedByUser;
|
||||
css?: string;
|
||||
template_name: string;
|
||||
};
|
||||
Loading…
Reference in New Issue