diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 016c88685..d44fcf5c6 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -202,7 +202,8 @@ "noAccess": "Vous n'avez pas les droits pour accéder à cette ressource.", "geometry": "Géométrie", "true": "Oui", - "false": "Non" + "false": "Non", + "other": "Autre" } }, "help": { diff --git a/src/modules/CRUD/components/DataTable/DataTable.js b/src/modules/CRUD/components/DataTable/DataTable.js index b691e12d9..d4bb0222b 100644 --- a/src/modules/CRUD/components/DataTable/DataTable.js +++ b/src/modules/CRUD/components/DataTable/DataTable.js @@ -31,7 +31,7 @@ class DataTable extends React.Component { loadData = () => { const { getFeaturesList } = this.props; - const { layer: { id } } = this.getView(); + const { layer: { id } = {} } = this.getView(); if (!id) return; getFeaturesList(id); this.setState({ @@ -87,7 +87,7 @@ class DataTable extends React.Component { onHoverCell = () => null, } = this.props; - const { layer: { name }, name: displayName = name } = this.getView(); + const { layer: { name } = {}, name: displayName = name } = this.getView(); const { data, columns, loading } = this.state; diff --git a/src/modules/CRUD/components/Details/Details.js b/src/modules/CRUD/components/Details/Details.js index 89222d9c2..b10b06b07 100644 --- a/src/modules/CRUD/components/Details/Details.js +++ b/src/modules/CRUD/components/Details/Details.js @@ -14,8 +14,12 @@ import './styles.scss'; class Details extends React.Component { static propTypes = { - view: PropTypes.shape({}).isRequired, - feature: PropTypes.shape({}), + view: PropTypes.shape({ + formSchema: PropTypes.shape({}), + }), + feature: PropTypes.shape({ + properties: PropTypes.shape({}), + }), fetchFeature: PropTypes.func.isRequired, match: PropTypes.shape({ params: PropTypes.shape({ @@ -32,7 +36,12 @@ class Details extends React.Component { }; static defaultProps = { - feature: {}, + view: { + formSchema: {}, + }, + feature: { + properties: {}, + }, match: { params: { id: undefined, @@ -85,7 +94,7 @@ class Details extends React.Component { }, }, feature, - feature: { properties } = {}, + feature: { properties }, detailsHasLoaded, } = this.props; @@ -98,7 +107,7 @@ class Details extends React.Component { } if ( - properties !== prevProperties + (properties !== prevProperties) || prevParamId !== paramId || prevParamAction !== paramAction ) { @@ -123,29 +132,39 @@ class Details extends React.Component { } } - setSchema = () => { + buildSchema = (schemaProperties, properties = {}) => { const { match: { params: { id: paramId } }, - feature: { properties }, + } = this.props; + return Object.keys(schemaProperties).reduce((list, prop) => ({ + ...list, + [prop]: { + ...schemaProperties[prop], + ...(schemaProperties[prop].type === 'object') + ? { properties: this.buildSchema(schemaProperties[prop].properties, properties[prop]) } + : {}, + ...(paramId !== ACTION_CREATE && properties[prop] && schemaProperties[prop].type !== 'object') + ? { default: properties[prop] } + : {}, + }, + }), {}); + } + + setSchema = () => { + const { + feature: { properties = {} }, view: { formSchema: schema = {} }, } = this.props; - if (Object.keys(schema).length) { - this.setState({ - schema: { - type: 'object', - ...schema, - properties: Object.keys(schema.properties).reduce((list, prop) => ({ - ...list, - [prop]: { - ...schema.properties[prop], - ...(properties && paramId !== ACTION_CREATE) - ? { default: properties[prop] } - : {}, - }, - }), {}), - }, - }); + if (!Object.keys(properties).length && !Object.keys(schema).length) { + return; } + this.setState({ + schema: { + type: 'object', + ...schema, + properties: this.buildSchema(schema.properties, properties), + }, + }); } renderContent = () => { @@ -164,10 +183,11 @@ class Details extends React.Component { updateControls={updateControls} action={paramAction || paramId} view={view} + feature={feature} /> ); } - return ; + return ; } onSizeChange = () => { @@ -189,7 +209,6 @@ class Details extends React.Component { toast.displayError(t('CRUD.details.errorNoFeature')); return ; } - const isLoading = !Object.keys(feature).length && paramId !== ACTION_CREATE; return ( diff --git a/src/modules/CRUD/components/Details/DownloadButtons/DownloadButtons.js b/src/modules/CRUD/components/Details/DownloadButtons/DownloadButtons.js index ce6b6e4ed..81b45c6f1 100644 --- a/src/modules/CRUD/components/Details/DownloadButtons/DownloadButtons.js +++ b/src/modules/CRUD/components/Details/DownloadButtons/DownloadButtons.js @@ -2,27 +2,26 @@ import React from 'react'; import PropTypes from 'prop-types'; import { AnchorButton, ButtonGroup } from '@blueprintjs/core'; -const DownloadButtons = ({ files, id, ...rest }) => id && files.length > 0 && ( +const DownloadButtons = ({ documents, ...rest }) => documents.length > 0 && ( - {files.map(({ name, url }) => ( - {name} + {documents.map(({ download_url: url, template_file: file, template_name: name }) => ( + {name} ))} ); DownloadButtons.propTypes = { - files: PropTypes.arrayOf( + documents: PropTypes.arrayOf( PropTypes.shape({ - name: PropTypes.string, - url: PropTypes.string, + template_name: PropTypes.string, + download_url: PropTypes.string, + template_file: PropTypes.string, }), ), - id: PropTypes.number, }; DownloadButtons.defaultProps = { - files: [], - id: '', + documents: [], }; export default DownloadButtons; diff --git a/src/modules/CRUD/components/Details/DownloadButtons/DownloadButtons.test.js b/src/modules/CRUD/components/Details/DownloadButtons/DownloadButtons.test.js index cfe85196e..4537795aa 100644 --- a/src/modules/CRUD/components/Details/DownloadButtons/DownloadButtons.test.js +++ b/src/modules/CRUD/components/Details/DownloadButtons/DownloadButtons.test.js @@ -10,14 +10,15 @@ jest.mock('@blueprintjs/core', () => ({ const props = { - files: [{ - name: 'FileName', - url: 'path/to/file/{id}', + documents: [{ + template_name: 'FileName', + download_url: 'path/to/file/', + template_file: 'file1.pdf', }, { - name: 'FileName2', - url: 'path/to/file2/{id}', + template_name: 'FileName2', + download_url: 'path/to/file2/', + template_file: 'file2.pdf', }], - id: 3, }; diff --git a/src/modules/CRUD/components/Details/Edit/Edit.js b/src/modules/CRUD/components/Details/Edit/Edit.js index 4f3d4ee03..54b0616d4 100644 --- a/src/modules/CRUD/components/Details/Edit/Edit.js +++ b/src/modules/CRUD/components/Details/Edit/Edit.js @@ -34,7 +34,9 @@ function updateSchemaPropertiesValues (properties, formData) { class Edit extends React.Component { static propTypes = { map: PropTypes.shape({}), - feature: PropTypes.shape({}), + feature: PropTypes.shape({ + title: PropTypes.string, + }), saveFeature: PropTypes.func.isRequired, view: PropTypes.shape({}).isRequired, layerPaint: PropTypes.shape({}).isRequired, @@ -54,7 +56,9 @@ class Edit extends React.Component { static defaultProps = { map: {}, - feature: {}, + feature: { + title: undefined, + }, displayAddFeature: true, displayChangeFeature: true, updateControls () {}, @@ -284,6 +288,7 @@ class Edit extends React.Component { paramId, displayAddFeature, displayChangeFeature, + feature: { title }, } = this.props; if ( @@ -294,7 +299,6 @@ class Edit extends React.Component { return (); } - const { name: { default: title } = {} } = properties || {}; const mainTitle = action === ACTION_CREATE ? t('CRUD.details.create', { layer: displayName }) : (title || t('CRUD.details.noFeature')); diff --git a/src/modules/CRUD/components/Details/Edit/Edit.test.js b/src/modules/CRUD/components/Details/Edit/Edit.test.js index 080c4de64..07b94c68b 100644 --- a/src/modules/CRUD/components/Details/Edit/Edit.test.js +++ b/src/modules/CRUD/components/Details/Edit/Edit.test.js @@ -55,7 +55,9 @@ beforeEach(() => { props = { settings: {}, map: {}, - feature: {}, + feature: { + title: 'Title of the feature', + }, saveFeature: jest.fn(), updateControls: jest.fn(), view: { @@ -76,6 +78,12 @@ beforeEach(() => { properties: { city: { type: 'boolean', title: 'City' }, name: { type: 'text', default: 'Title' }, + group: { + type: 'object', + properties: { + name: { type: 'string', title: 'Group name' }, + }, + }, }, }, history: { @@ -294,6 +302,12 @@ it('should update schema', () => { city: { title: 'City', type: 'boolean' }, geometryFromMap: { default: false, title: 'CRUD.details.geometry', type: 'boolean' }, name: { default: 'Title', type: 'text' }, + group: { + type: 'object', + properties: { + name: { type: 'string', title: 'Group name' }, + }, + }, }, }, }); diff --git a/src/modules/CRUD/components/Details/Edit/__snapshots__/Edit.test.js.snap b/src/modules/CRUD/components/Details/Edit/__snapshots__/Edit.test.js.snap index 634b17f77..ede18be83 100644 --- a/src/modules/CRUD/components/Details/Edit/__snapshots__/Edit.test.js.snap +++ b/src/modules/CRUD/components/Details/Edit/__snapshots__/Edit.test.js.snap @@ -16,7 +16,7 @@ exports[`Snapshots should render correctly 1`] = `

- Title + Title of the feature

- CRUD.details.noFeature + Title of the feature
['', undefined].includes(value); +const emptyStringNullOrUndef = value => ['', null, undefined].includes(value); const isHTML = value => { const div = document.createElement('div'); @@ -30,7 +30,7 @@ const formattedProp = ({ value, t }) => { : t('CRUD.details.false'); } - if (emptyStringOrUndef(value)) { + if (emptyStringNullOrUndef(value)) { return t(NO_FEATURE); } @@ -53,59 +53,107 @@ const formattedProp = ({ value, t }) => { return value; }; -const Read = ({ - t, - match: { params: { layer: paramLayer, id: paramId } }, - schema: { title: schemaTitle, properties = {} }, - displayViewFeature, - view: { templates, uiSchema: { 'ui:order': order } = {} }, - feature: { id }, -}) => { - if (!displayViewFeature) { - toast.displayError(t('CRUD.details.noAccess')); - return (); + +class Read extends React.Component { + state = { + tabs: [], + } + + componentDidMount () { + this.generatesTabs(); } - const { name: { default: title } = {} } = properties; - const hasProperties = !!Object.keys(properties).length; + componentDidUpdate ({ + feature: { display_properties: prevDisplayProperties }, + }) { + const { + feature: { display_properties: displayProperties }, + } = this.props; + if (prevDisplayProperties !== displayProperties) { + this.generatesTabs(); + } + } - const orderedProperties = orderProperties(Object.keys(properties), order); + generatesTabs = () => { + const { + feature: { display_properties: displayProperties }, + } = this.props; + this.setState({ + tabs: Object.keys(displayProperties) + .map(tabs => ({ ...displayProperties[tabs] })) + .sort((a, b) => a.order - b.order), + }); + } - return ( -
-
-

{title || t(NO_FEATURE)}

- -
- {hasProperties && ( -
- {schemaTitle && ( -

{schemaTitle}

- )} -
    - {orderedProperties.map(prop => ( -
  • - {properties[prop].title || prop} - - {formattedProp({ value: properties[prop].default, t })} - -
  • - ))} -
+ renderPanel = properties => { + const { t } = this.props; + return ( +
    + {Object.keys(properties).map(name => ( +
  • + {name} + + {formattedProp({ value: properties[name], t })} + +
  • + ))} +
+ ); + } + + render () { + const { + t, + match: { params: { layer: paramLayer, id: paramId } }, + location: { hash }, + displayViewFeature, + feature: { title: featureTitle, documents }, + } = this.props; + + if (!displayViewFeature) { + toast.displayError(t('CRUD.details.noAccess')); + return (); + } + + const { tabs } = this.state; + const hasProperties = !!tabs.length; + + return ( +
+
+

{featureTitle || t(NO_FEATURE)}

+
- )} - -
- ); -}; + {hasProperties && ( +
+ + {tabs.map(({ title, slug = 'other', properties }) => ( + {title || t('CRUD.details.other')}} + panel={this.renderPanel(properties)} + /> + ))} + + +
+ )} + +
+ ); + } +} Read.propTypes = { match: PropTypes.shape({ @@ -114,15 +162,14 @@ Read.propTypes = { id: PropTypes.string, }), }), - schema: PropTypes.shape({ - properties: PropTypes.shape({}), + location: PropTypes.shape({ + hash: PropTypes.string, }), displayViewFeature: PropTypes.bool, - view: PropTypes.shape({ - templates: PropTypes.array, - }), feature: PropTypes.shape({ - id: PropTypes.number, + title: PropTypes.string, + documents: PropTypes.array, + display_properties: PropTypes.shape({}), }), t: PropTypes.func, }; @@ -134,15 +181,14 @@ Read.defaultProps = { id: undefined, }, }, - schema: { - properties: {}, - }, + location: PropTypes.shape({ + hash: undefined, + }), displayViewFeature: true, - view: { - templates: [], - }, feature: { - id: undefined, + title: '', + documents: [], + display_properties: {}, }, t: text => text, }; diff --git a/src/modules/CRUD/components/Details/Read/Read.test.js b/src/modules/CRUD/components/Details/Read/Read.test.js index d6e7f9a1b..160c2457b 100644 --- a/src/modules/CRUD/components/Details/Read/Read.test.js +++ b/src/modules/CRUD/components/Details/Read/Read.test.js @@ -4,8 +4,20 @@ import renderer from 'react-test-renderer'; import Read from './Read'; +jest.mock('@blueprintjs/core', () => { + const Tabs = ({ children }) =>
    {children}
; + const Tab = ({ title, panel }) =>
  • {title}{panel}
  • ; + Tabs.Expander = () => null; + + return { + Tabs, + Tab, + }; +}); + jest.mock('react-router-dom', () => ({ Redirect: () =>
    Error because Redirect
    , + Link: ({ children }) => {children}, })); jest.mock('../../../../../utils/toast', () => ({ @@ -25,62 +37,38 @@ jest.mock('../Actions', () => () => (
    Actions
    )); const props = { t: text => text, match: { params: { layer: 'layerFoo', id: 'layerId' } }, - schema: { - title: 'Foo Title', - properties: { - city: { - type: 'string', - title: 'Ville', - default: 'Agen, Lot-et-Garonne, Nouvelle-Aquitaine', - }, - name: { - type: 'string', - title: 'Nom', - default: 'Cathedrale Saint-Caprais', - }, - description: { - type: 'string', - title: 'Description', - default: '', - }, - numero: { - type: 'integer', - title: 'Numéro', - default: 2, - }, - validation: { - type: 'boolean', - title: 'Validation', - default: true, - }, - available: { - type: 'boolean', - title: 'Validation', - default: false, + displayViewFeature: true, + location: { + hash: '', + }, + feature: { + title: 'Title of the feature', + display_properties: { + 'Group 1': { + title: 'Name of the group', + slug: 'slug-group', + order: 1, + pictogram: null, + properties: { + Numéro: 2, + Validation: true, + Available: false, + Array: [1, 2, 3], + EmptyValue: ' ', + }, }, - labels: { - type: 'array', - items: { - enum: [ - 'VPAH', - 'PM 1979', - 'PM 1981', - 'PM 1991', - 'PM 1992', - ], - type: 'string', + __default__: { + title: '', + pictogram: null, + order: 9999, + properties: { + 'key without value': null, + 'Hello foo bar': 'Value hello foo bar', + Contact: 'Foo contact', }, - title: 'Labels', - uniqueItems: true, - default: ['PM 1979', 'PM 1981'], }, }, }, - displayViewFeature: true, - layer: {}, - feature: { - id: undefined, - }, }; diff --git a/src/modules/CRUD/components/Details/Read/__snapshots__/Read.test.js.snap b/src/modules/CRUD/components/Details/Read/__snapshots__/Read.test.js.snap index 1a4162b05..88a4a0089 100644 --- a/src/modules/CRUD/components/Details/Read/__snapshots__/Read.test.js.snap +++ b/src/modules/CRUD/components/Details/Read/__snapshots__/Read.test.js.snap @@ -16,7 +16,7 @@ exports[`should render correctly 1`] = `

    - Cathedrale Saint-Caprais + Title of the feature

    DownloadButtons @@ -25,125 +25,157 @@ exports[`should render correctly 1`] = `
    -

    - Foo Title -

    -
      -
    • - - Ville - - -
      - -
    • -
    • - - Nom - - -
      - -
    • -
    • - - Description - - - CRUD.details.noFeature +
        +
      • + + Name of the group -
      • -
      • - - Numéro - - - 2 - -
      • -
      • - - Validation - - - CRUD.details.true - +
      • + + Numéro + + + 2 + +
      • +
      • + + Validation + + + CRUD.details.true + +
      • +
      • + + Available + + + CRUD.details.false + +
      • +
      • + + Array + + + 1, 2, 3 + +
      • +
      • + + EmptyValue + + +
        + +
      • +
    • -
    • - - Validation - - - CRUD.details.false +
    • + + CRUD.details.other -
    • -
    • - - Labels - - - PM 1979, PM 1981 - +
    • + + key without value + + + CRUD.details.noFeature + +
    • +
    • + + Hello foo bar + + +
      + +
    • +
    • + + Contact + + +
      + +
    • +
    diff --git a/src/modules/CRUD/services/features.js b/src/modules/CRUD/services/features.js index 723eaa5b6..005c18ee6 100644 --- a/src/modules/CRUD/services/features.js +++ b/src/modules/CRUD/services/features.js @@ -1,19 +1,19 @@ import Api from '@terralego/core/modules/Api'; export const fetchFeaturesList = layerId => - Api.request(`layer/${layerId}/feature/`); + Api.request(`crud/layer/${layerId}/features/`); export const fetchFeature = (layerId, featureId) => - Api.request(`layer/${layerId}/feature/${featureId}/`); + Api.request(`crud/layer/${layerId}/features/${featureId}/`); const createFeature = (layerId, body) => - Api.request(`layer/${layerId}/feature/`, { method: 'POST', body }); + Api.request(`crud/layer/${layerId}/features/`, { method: 'POST', body }); const updateFeature = (layerId, featureId, body) => - Api.request(`layer/${layerId}/feature/${featureId}/`, { method: 'PUT', body }); + Api.request(`crud/layer/${layerId}/features/${featureId}/`, { method: 'PUT', body }); export const deleteFeature = (layerId, featureId) => - Api.request(`layer/${layerId}/feature/${featureId}/`, { method: 'DELETE' }); + Api.request(`crud/layer/${layerId}/features/${featureId}/`, { method: 'DELETE' }); export const saveFeature = (layerId, featureId, body) => ( featureId diff --git a/src/modules/CRUD/services/features.test.js b/src/modules/CRUD/services/features.test.js index d1ff8c2fa..5df08c336 100644 --- a/src/modules/CRUD/services/features.test.js +++ b/src/modules/CRUD/services/features.test.js @@ -8,27 +8,27 @@ jest.mock('@terralego/core/modules/Api', () => ({ it('should fetch list of feature', () => { fetchFeaturesList('foo'); - expect(Api.request).toHaveBeenCalledWith('layer/foo/feature/'); + expect(Api.request).toHaveBeenCalledWith('crud/layer/foo/features/'); }); it('should fetch a feature', () => { fetchFeature('foo', '1337'); - expect(Api.request).toHaveBeenCalledWith('layer/foo/feature/1337/'); + expect(Api.request).toHaveBeenCalledWith('crud/layer/foo/features/1337/'); }); it('should delete a feature', () => { deleteFeature('foo', '1337'); - expect(Api.request).toHaveBeenCalledWith('layer/foo/feature/1337/', { method: 'DELETE' }); + expect(Api.request).toHaveBeenCalledWith('crud/layer/foo/features/1337/', { method: 'DELETE' }); }); it('should create a feature', () => { saveFeature('foo', false, { bar: 'bar' }); - expect(Api.request).toHaveBeenCalledWith('layer/foo/feature/', { method: 'POST', body: { bar: 'bar' } }); + expect(Api.request).toHaveBeenCalledWith('crud/layer/foo/features/', { method: 'POST', body: { bar: 'bar' } }); }); it('should update a feature', () => { saveFeature('foo', '1337', { bar: 'bar' }); - expect(Api.request).toHaveBeenCalledWith('layer/foo/feature/1337/', { method: 'PUT', body: { bar: 'bar' } }); + expect(Api.request).toHaveBeenCalledWith('crud/layer/foo/features/1337/', { method: 'PUT', body: { bar: 'bar' } }); }); it('should get bounds', () => {