[ARVADOS-WORKBENCH2] updated: 1.4.1-113-g2f93708c

Git user git at public.curoverse.com
Thu Nov 14 18:47:02 UTC 2019


Summary of changes:
 src/common/config.ts                               |   3 -
 src/models/vocabulary.test.ts                      | 148 +++++++++++++++++++++
 src/models/vocabulary.ts                           |   8 +-
 .../details-panel/project-details.tsx              |  37 ++----
 .../project-properties-dialog.tsx                  |  35 ++---
 .../resource-properties-form/property-chip.tsx     |  52 ++++++++
 .../property-field-common.tsx                      |  33 ++++-
 .../property-key-field.tsx                         |  34 +----
 .../property-value-field.tsx                       |  34 +----
 src/views/collection-panel/collection-panel.tsx    |  34 ++---
 10 files changed, 273 insertions(+), 145 deletions(-)
 create mode 100644 src/models/vocabulary.test.ts
 create mode 100644 src/views-components/resource-properties-form/property-chip.tsx

       via  2f93708cdf1e4d750b6148077246f7c13c7ac402 (commit)
       via  a22226b9a6cc30b6d69d517d71ecb6b1b402419c (commit)
       via  6656a000d2992676dcfcbed51b34d327d304d05e (commit)
      from  63ffac0b419097265599243dd645c0447d1c24d6 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 2f93708cdf1e4d750b6148077246f7c13c7ac402
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Thu Nov 14 15:45:14 2019 -0300

    15067: Adds tests for vocabulary functions.
    
    Fixed a couple of bugs in the process.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima at veritasgenetics.com>

diff --git a/src/models/vocabulary.test.ts b/src/models/vocabulary.test.ts
new file mode 100644
index 00000000..87a8dfb2
--- /dev/null
+++ b/src/models/vocabulary.test.ts
@@ -0,0 +1,148 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as Vocabulary from './vocabulary';
+import { pipe } from 'lodash/fp';
+
+describe('Vocabulary', () => {
+    let vocabulary: Vocabulary.Vocabulary;
+
+    beforeEach(() => {
+        vocabulary = {
+            strict_tags: false,
+            tags: {
+                IDKEYCOMMENT: {
+                    labels: []
+                },
+                IDKEYANIMALS: {
+                    strict: false,
+                    labels: [
+                        {label: "Animal" },
+                        {label: "Creature"}
+                    ],
+                    values: {
+                        IDVALANIMALS1: {
+                            labels: [
+                                {label: "Human"},
+                                {label: "Homo sapiens"}
+                            ]
+                        },
+                        IDVALANIMALS2: {
+                            labels: [
+                                {label: "Dog"},
+                                {label: "Canis lupus familiaris"}
+                            ]
+                        },
+                    }
+                },
+                IDKEYSIZES: {
+                    labels: [{label: "Sizes"}],
+                    values: {
+                        IDVALSIZES1: {
+                            labels: [{label: "Small"}]
+                        },
+                        IDVALSIZES2: {
+                            labels: [{label: "Medium"}]
+                        },
+                        IDVALSIZES3: {
+                            labels: [{label: "Large"}]
+                        },
+                        IDVALSIZES4: {
+                            labels: []
+                        }
+                    }
+                }
+            }
+        }
+    });
+
+    it('returns the list of tag keys', () => {
+        const tagKeys = Vocabulary.getTags(vocabulary);
+        // Alphabetically ordered by label
+        expect(tagKeys).toEqual([
+            {id: "IDKEYANIMALS", label: "Animal"},
+            {id: "IDKEYANIMALS", label: "Creature"},
+            {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT"},
+            {id: "IDKEYSIZES", label: "Sizes"},
+        ]);
+    });
+
+    it('returns the tag values for a given key', () => {
+        const tagValues = Vocabulary.getTagValues('IDKEYSIZES', vocabulary);
+        // Alphabetically ordered by label
+        expect(tagValues).toEqual([
+            {id: "IDVALSIZES4", label: "IDVALSIZES4"},
+            {id: "IDVALSIZES3", label: "Large"},
+            {id: "IDVALSIZES2", label: "Medium"},
+            {id: "IDVALSIZES1", label: "Small"},
+        ])
+    });
+
+    it('returns an empty list of values for an non-existent key', () => {
+        const tagValues = Vocabulary.getTagValues('IDNONSENSE', vocabulary);
+        expect(tagValues).toEqual([]);
+    });
+
+    it('returns a key id for a given key label', () => {
+        const testCases = [
+            // Two labels belonging to the same ID
+            {keyLabel: 'Animal', expected: 'IDKEYANIMALS'},
+            {keyLabel: 'Creature', expected: 'IDKEYANIMALS'},
+            // Non-existent label returns empty string
+            {keyLabel: 'ThisKeyLabelDoesntExist', expected: ''},
+        ]
+        testCases.forEach(tc => {
+            const tagValueID = Vocabulary.getTagKeyID(tc.keyLabel, vocabulary);
+            expect(tagValueID).toEqual(tc.expected);
+        });
+    });
+
+    it('returns an key label for a given key id', () => {
+        const testCases = [
+            // ID with many labels return the first one
+            {keyID: 'IDKEYANIMALS', expected: 'Animal'},
+            // Key IDs without any labels or unknown keys should return the literal
+            // key from the API's response (that is, the key 'id')
+            {keyID: 'IDKEYCOMMENT', expected: 'IDKEYCOMMENT'},
+            {keyID: 'FOO', expected: 'FOO'},
+        ]
+        testCases.forEach(tc => {
+            const tagValueID = Vocabulary.getTagKeyLabel(tc.keyID, vocabulary);
+            expect(tagValueID).toEqual(tc.expected);
+        });
+    });
+
+    it('returns a value id for a given key id and value label', () => {
+        const testCases = [
+            // Key ID and value label known
+            {keyID: 'IDKEYANIMALS', valueLabel: 'Human', expected: 'IDVALANIMALS1'},
+            {keyID: 'IDKEYANIMALS', valueLabel: 'Homo sapiens', expected: 'IDVALANIMALS1'},
+            // Key ID known, value label unknown
+            {keyID: 'IDKEYANIMALS', valueLabel: 'Dinosaur', expected: ''},
+            // Key ID unknown
+            {keyID: 'IDNONSENSE', valueLabel: 'Does not matter', expected: ''},
+        ]
+        testCases.forEach(tc => {
+            const tagValueID = Vocabulary.getTagValueID(tc.keyID, tc.valueLabel, vocabulary);
+            expect(tagValueID).toEqual(tc.expected);
+        });
+    });
+
+    it('returns a value label for a given key & value id pair', () => {
+        const testCases = [
+            // Known key & value ids with multiple value labels: returns the first label
+            {keyId: 'IDKEYANIMALS', valueId: 'IDVALANIMALS1', expected: 'Human'},
+            // Values without label or unknown values should return the literal value from
+            // the API's response (that is, the value 'id')
+            {keyId: 'IDKEYSIZES', valueId: 'IDVALSIZES4', expected: 'IDVALSIZES4'},
+            {keyId: 'IDKEYCOMMENT', valueId: 'FOO', expected: 'FOO'},
+            {keyId: 'IDKEYANIMALS', valueId: 'BAR', expected: 'BAR'},
+            {keyId: 'IDKEYNONSENSE', valueId: 'FOOBAR', expected: 'FOOBAR'},
+        ]
+        testCases.forEach(tc => {
+            const tagValueLabel = Vocabulary.getTagValueLabel(tc.keyId, tc.valueId, vocabulary);
+            expect(tagValueLabel).toEqual(tc.expected);
+        });
+    });
+});
diff --git a/src/models/vocabulary.ts b/src/models/vocabulary.ts
index ebef70f0..03f28c07 100644
--- a/src/models/vocabulary.ts
+++ b/src/models/vocabulary.ts
@@ -25,8 +25,8 @@ export interface Tag {
 }
 
 export interface PropFieldSuggestion {
-    "id": string;
-    "label": string;
+    id: string;
+    label: string;
 }
 
 const VOCABULARY_VALIDATORS = [
@@ -68,7 +68,7 @@ export const getTagValues = (tagKeyID: string, vocabulary: Vocabulary) => {
     const tag = vocabulary.tags[tagKeyID];
     const ret = tag && tag.values
         ? Object.keys(tag.values).map(
-            tagValueID => tag.values![tagValueID].labels
+            tagValueID => tag.values![tagValueID].labels && tag.values![tagValueID].labels.length > 0
                 ? tag.values![tagValueID].labels.map(
                     lbl => Object.assign({}, {"id": tagValueID, "label": lbl.label}))
                 : [{"id": tagValueID, "label": tagValueID}])
@@ -81,7 +81,7 @@ export const getTagValues = (tagKeyID: string, vocabulary: Vocabulary) => {
 export const getTags = ({ tags }: Vocabulary) => {
     const ret = tags && Object.keys(tags)
         ? Object.keys(tags).map(
-            tagID => tags[tagID].labels
+            tagID => tags[tagID].labels && tags[tagID].labels.length > 0
                 ? tags[tagID].labels.map(
                     lbl => Object.assign({}, {"id": tagID, "label": lbl.label}))
                 : [{"id": tagID, "label": tagID}])

commit a22226b9a6cc30b6d69d517d71ecb6b1b402419c
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Thu Nov 14 11:28:16 2019 -0300

    15067: Generalizes handleSelect & handleBlur on property form fields.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima at veritasgenetics.com>

diff --git a/src/common/config.ts b/src/common/config.ts
index cbac7b19..7d974342 100644
--- a/src/common/config.ts
+++ b/src/common/config.ts
@@ -125,9 +125,6 @@ remove the entire ${varName} entry from ${WORKBENCH_CONFIG_URL}`);
                 else {
                     vocabularyUrl = clusterConfigJSON.Workbench.VocabularyURL || "/vocabulary-example.json";
                 }
-                // FIXME: The following line is for dev testing purposes
-                vocabularyUrl = "/vocabulary-example.json";
-
                 config.vocabularyUrl = vocabularyUrl;
 
                 return { config, apiHost: workbenchConfig.API_HOST };
diff --git a/src/views-components/resource-properties-form/property-field-common.tsx b/src/views-components/resource-properties-form/property-field-common.tsx
index a90ce923..65d0c7c8 100644
--- a/src/views-components/resource-properties-form/property-field-common.tsx
+++ b/src/views-components/resource-properties-form/property-field-common.tsx
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { connect } from 'react-redux';
-import { WrappedFieldMetaProps, WrappedFieldInputProps, WrappedFieldProps } from 'redux-form';
+import { change, WrappedFieldMetaProps, WrappedFieldInputProps, WrappedFieldProps } from 'redux-form';
 import { Vocabulary, PropFieldSuggestion } from '~/models/vocabulary';
 import { RootState } from '~/store/store';
 import { getVocabulary } from '~/store/vocabulary/vocabulary-selectors';
@@ -28,18 +28,39 @@ export const getErrorMsg = (meta: WrappedFieldMetaProps) =>
         ? meta.error
         : '';
 
-export const handleBlur = ({ onBlur, value }: WrappedFieldInputProps) =>
-    () =>
-        onBlur(value);
-
 export const buildProps = ({ input, meta }: WrappedFieldProps) => {
     return {
         value: input.value,
         onChange: input.onChange,
-        onBlur: handleBlur(input),
         items: ITEMS_PLACEHOLDER,
         renderSuggestion: (item:PropFieldSuggestion) => item.label,
         error: hasError(meta),
         helperText: getErrorMsg(meta),
     };
 };
+
+// Attempts to match a manually typed value label with a value ID, when the user
+// doesn't select the value from the suggestions list.
+export const handleBlur = (
+    fieldName: string,
+    formName: string,
+    { dispatch }: WrappedFieldMetaProps,
+    { onBlur, value }: WrappedFieldInputProps,
+    fieldValue: string) =>
+        () => {
+            dispatch(change(formName, fieldName, fieldValue));
+            onBlur(value);
+        };
+
+// When selecting a property value, save its ID for later usage.
+export const handleSelect = (
+    fieldName: string,
+    formName: string,
+    { onChange }: WrappedFieldInputProps,
+    { dispatch }: WrappedFieldMetaProps ) =>
+        (item:PropFieldSuggestion) => {
+            if (item) {
+                onChange(item.label);
+                dispatch(change(formName, fieldName, item.id));
+            }
+        };
diff --git a/src/views-components/resource-properties-form/property-key-field.tsx b/src/views-components/resource-properties-form/property-key-field.tsx
index ef51d250..89a03946 100644
--- a/src/views-components/resource-properties-form/property-key-field.tsx
+++ b/src/views-components/resource-properties-form/property-key-field.tsx
@@ -3,12 +3,11 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { change, WrappedFieldProps, WrappedFieldMetaProps, WrappedFieldInputProps,
-    Field, FormName } from 'redux-form';
+import { WrappedFieldProps, Field, FormName } from 'redux-form';
 import { memoize } from 'lodash';
 import { Autocomplete } from '~/components/autocomplete/autocomplete';
-import { Vocabulary, getTags, getTagKeyID, PropFieldSuggestion } from '~/models/vocabulary';
-import { connectVocabulary, VocabularyProp, buildProps } from '~/views-components/resource-properties-form/property-field-common';
+import { Vocabulary, getTags, getTagKeyID } from '~/models/vocabulary';
+import { handleSelect, handleBlur, connectVocabulary, VocabularyProp, buildProps } from '~/views-components/resource-properties-form/property-field-common';
 import { TAG_KEY_VALIDATION } from '~/validators/validators';
 import { escapeRegExp } from '~/common/regexp.ts';
 
@@ -29,9 +28,9 @@ export const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & V
         <Autocomplete
             label='Key'
             suggestions={getSuggestions(props.input.value, vocabulary)}
-            onSelect={handleSelect(data.form, props.input, props.meta)}
+            onSelect={handleSelect(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)}
+            onBlur={handleBlur(PROPERTY_KEY_FIELD_ID, data.form, props.meta, props.input, getTagKeyID(props.input.value, vocabulary))}
             {...buildProps(props)}
-            onBlur={handleBlur(data.form, props.meta, props.input, vocabulary)}
         />
     )}/>;
 
@@ -51,26 +50,3 @@ const getSuggestions = (value: string, vocabulary: Vocabulary) => {
     const re = new RegExp(escapeRegExp(value), "i");
     return getTags(vocabulary).filter(tag => re.test(tag.label) && tag.label !== value);
 };
-
-// Attempts to match a manually typed key label with a key ID, when the user
-// doesn't select the key from the suggestions list.
-const handleBlur = (
-    formName: string,
-    { dispatch }: WrappedFieldMetaProps,
-    { onBlur, value }: WrappedFieldInputProps,
-    vocabulary: Vocabulary) =>
-    () => {
-        dispatch(change(formName, PROPERTY_KEY_FIELD_ID, getTagKeyID(value, vocabulary)));
-        onBlur(value);
-    };
-
-// When selecting a property key, save its ID for later usage.
-const handleSelect = (
-    formName: string,
-    { onChange }: WrappedFieldInputProps,
-    { dispatch }: WrappedFieldMetaProps) => {
-        return (item:PropFieldSuggestion) => {
-            onChange(item.label);
-            dispatch(change(formName, PROPERTY_KEY_FIELD_ID, item.id));
-    };
-};
diff --git a/src/views-components/resource-properties-form/property-value-field.tsx b/src/views-components/resource-properties-form/property-value-field.tsx
index e34170bc..4df44619 100644
--- a/src/views-components/resource-properties-form/property-value-field.tsx
+++ b/src/views-components/resource-properties-form/property-value-field.tsx
@@ -3,13 +3,12 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { change, WrappedFieldProps, WrappedFieldMetaProps, WrappedFieldInputProps,
-    Field, formValues, FormName } from 'redux-form';
+import { WrappedFieldProps, Field, formValues, FormName } from 'redux-form';
 import { compose } from 'redux';
 import { Autocomplete } from '~/components/autocomplete/autocomplete';
-import { Vocabulary, getTagValueID, isStrictTag, getTagValues, PropFieldSuggestion } from '~/models/vocabulary';
+import { Vocabulary, isStrictTag, getTagValues, getTagValueID } from '~/models/vocabulary';
 import { PROPERTY_KEY_FIELD_ID } from '~/views-components/resource-properties-form/property-key-field';
-import { VocabularyProp, connectVocabulary, buildProps } from '~/views-components/resource-properties-form/property-field-common';
+import { handleSelect, handleBlur, VocabularyProp, connectVocabulary, buildProps } from '~/views-components/resource-properties-form/property-field-common';
 import { TAG_VALUE_VALIDATION } from '~/validators/validators';
 import { escapeRegExp } from '~/common/regexp.ts';
 
@@ -39,9 +38,9 @@ export const PropertyValueInput = ({ vocabulary, propertyKey, ...props }: Wrappe
         <Autocomplete
             label='Value'
             suggestions={getSuggestions(props.input.value, propertyKey, vocabulary)}
-            onSelect={handleSelect(data.form, props.input, props.meta)}
+            onSelect={handleSelect(PROPERTY_VALUE_FIELD_ID, data.form, props.input, props.meta)}
+            onBlur={handleBlur(PROPERTY_VALUE_FIELD_ID, data.form, props.meta, props.input, getTagValueID(propertyKey, props.input.value, vocabulary))}
             {...buildProps(props)}
-            onBlur={handleBlur(data.form, props.meta, props.input, vocabulary, propertyKey)}
         />
     )}/>;
 
@@ -61,26 +60,3 @@ const getSuggestions = (value: string, tagName: string, vocabulary: Vocabulary)
     return getTagValues(tagName, vocabulary).filter(v => re.test(v.label) && v.label !== value);
 };
 
-// Attempts to match a manually typed value label with a value ID, when the user
-// doesn't select the value from the suggestions list.
-const handleBlur = (
-    formName: string,
-    { dispatch }: WrappedFieldMetaProps,
-    { onBlur, value }: WrappedFieldInputProps,
-    vocabulary: Vocabulary,
-    tagKeyID: string) =>
-        () => {
-            dispatch(change(formName, PROPERTY_VALUE_FIELD_ID, getTagValueID(tagKeyID, value, vocabulary)));
-            onBlur(value);
-        };
-
-// When selecting a property value, save its ID for later usage.
-const handleSelect = (
-    formName: string,
-    { onChange }: WrappedFieldInputProps,
-    { dispatch }: WrappedFieldMetaProps) => {
-        return (item:PropFieldSuggestion) => {
-            onChange(item.label);
-            dispatch(change(formName, PROPERTY_VALUE_FIELD_ID, item.id));
-    };
-};

commit 6656a000d2992676dcfcbed51b34d327d304d05e
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Thu Nov 14 10:06:55 2019 -0300

    15067: Creates PropertyChipComponent to be used where tags need listing.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima at veritasgenetics.com>

diff --git a/src/views-components/details-panel/project-details.tsx b/src/views-components/details-panel/project-details.tsx
index 7db4df7b..59035da1 100644
--- a/src/views-components/details-panel/project-details.tsx
+++ b/src/views-components/details-panel/project-details.tsx
@@ -4,7 +4,6 @@
 
 import * as React from 'react';
 import { connect } from 'react-redux';
-import { RootState } from '~/store/store';
 import { openProjectPropertiesDialog } from '~/store/details-panel/details-panel-action';
 import { ProjectIcon, RenameIcon } from '~/components/icon/icon';
 import { ProjectResource } from '~/models/project';
@@ -14,13 +13,10 @@ import { resourceLabel } from '~/common/labels';
 import { DetailsData } from "./details-data";
 import { DetailsAttribute } from "~/components/details-attribute/details-attribute";
 import { RichTextEditorLink } from '~/components/rich-text-editor-link/rich-text-editor-link';
-import { withStyles, StyleRulesCallback, Chip, WithStyles } from '@material-ui/core';
+import { withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
-import * as CopyToClipboard from 'react-copy-to-clipboard';
-import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
-import { getTagValueLabel, getTagKeyLabel, Vocabulary } from '~/models/vocabulary';
-import { getVocabulary } from "~/store/vocabulary/vocabulary-selectors";
 import { Dispatch } from 'redux';
+import { PropertyChipComponent } from '../resource-properties-form/property-chip';
 
 export class ProjectDetails extends DetailsData<ProjectResource> {
     getIcon(className?: string) {
@@ -45,35 +41,23 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     }
 });
 
-
 interface ProjectDetailsComponentDataProps {
     project: ProjectResource;
-    vocabulary: Vocabulary;
 }
 
 interface ProjectDetailsComponentActionProps {
     onClick: () => void;
-    onCopy: (message: string) => void;
 }
 
-const mapStateToProps = ({ properties }: RootState) => ({
-    vocabulary: getVocabulary(properties),
-});
-
 const mapDispatchToProps = (dispatch: Dispatch) => ({
     onClick: () => dispatch<any>(openProjectPropertiesDialog()),
-    onCopy: (message: string) => dispatch(snackbarActions.OPEN_SNACKBAR({
-        message,
-        hideDuration: 2000,
-        kind: SnackbarKind.SUCCESS
-    }))
 });
 
 type ProjectDetailsComponentProps = ProjectDetailsComponentDataProps & ProjectDetailsComponentActionProps & WithStyles<CssRules>;
 
-const ProjectDetailsComponent = connect(mapStateToProps, mapDispatchToProps)(
+const ProjectDetailsComponent = connect(null, mapDispatchToProps)(
     withStyles(styles)(
-        ({ classes, project, onClick, vocabulary, onCopy }: ProjectDetailsComponentProps) => <div>
+        ({ classes, project, onClick }: ProjectDetailsComponentProps) => <div>
             <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROJECT)} />
             {/* Missing attr */}
             <DetailsAttribute label='Size' value='---' />
@@ -98,14 +82,11 @@ const ProjectDetailsComponent = connect(mapStateToProps, mapDispatchToProps)(
                 </div>
             </DetailsAttribute>
             {
-                Object.keys(project.properties).map(k => {
-                    const label = `${getTagKeyLabel(k, vocabulary)}: ${getTagValueLabel(k, project.properties[k], vocabulary)}`;
-                    return (
-                        <CopyToClipboard key={k} text={label} onCopy={() => onCopy("Copied")}>
-                            <Chip key={k} className={classes.tag} label={label} />
-                        </CopyToClipboard>
-                    );
-                })
+                Object.keys(project.properties).map(k =>
+                    <PropertyChipComponent key={k}
+                        propKey={k} propValue={project.properties[k]}
+                        className={classes.tag} />
+                )
             }
         </div>
     ));
diff --git a/src/views-components/project-properties-dialog/project-properties-dialog.tsx b/src/views-components/project-properties-dialog/project-properties-dialog.tsx
index a071a985..7a4cfba6 100644
--- a/src/views-components/project-properties-dialog/project-properties-dialog.tsx
+++ b/src/views-components/project-properties-dialog/project-properties-dialog.tsx
@@ -9,14 +9,11 @@ import { RootState } from '~/store/store';
 import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
 import { ProjectResource } from '~/models/project';
 import { PROJECT_PROPERTIES_DIALOG_NAME, deleteProjectProperty } from '~/store/details-panel/details-panel-action';
-import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Chip, withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core';
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { ProjectPropertiesForm } from '~/views-components/project-properties-dialog/project-properties-form';
 import { getResource } from '~/store/resources/resources';
-import * as CopyToClipboard from 'react-copy-to-clipboard';
-import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
-import { getTagValueLabel, getTagKeyLabel, Vocabulary } from '~/models/vocabulary';
-import { getVocabulary } from "~/store/vocabulary/vocabulary-selectors";
+import { PropertyChipComponent } from "../resource-properties-form/property-chip";
 
 type CssRules = 'tag';
 
@@ -29,26 +26,18 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 
 interface ProjectPropertiesDialogDataProps {
     project: ProjectResource;
-    vocabulary: Vocabulary;
 }
 
 interface ProjectPropertiesDialogActionProps {
     handleDelete: (key: string) => void;
-    onCopy: (message: string) => void;
 }
 
 const mapStateToProps = ({ detailsPanel, resources, properties }: RootState): ProjectPropertiesDialogDataProps => ({
     project: getResource(detailsPanel.resourceUuid)(resources) as ProjectResource,
-    vocabulary: getVocabulary(properties),
 });
 
 const mapDispatchToProps = (dispatch: Dispatch): ProjectPropertiesDialogActionProps => ({
     handleDelete: (key: string) => dispatch<any>(deleteProjectProperty(key)),
-    onCopy: (message: string) => dispatch(snackbarActions.OPEN_SNACKBAR({
-                message,
-                hideDuration: 2000,
-                kind: SnackbarKind.SUCCESS
-            }))
 });
 
 type ProjectPropertiesDialogProps =  ProjectPropertiesDialogDataProps & ProjectPropertiesDialogActionProps & WithDialogProps<{}> & WithStyles<CssRules>;
@@ -56,7 +45,7 @@ type ProjectPropertiesDialogProps =  ProjectPropertiesDialogDataProps & ProjectP
 export const ProjectPropertiesDialog = connect(mapStateToProps, mapDispatchToProps)(
     withStyles(styles)(
     withDialog(PROJECT_PROPERTIES_DIALOG_NAME)(
-        ({ classes, open, closeDialog, handleDelete, onCopy, project, vocabulary }: ProjectPropertiesDialogProps) =>
+        ({ classes, open, closeDialog, handleDelete, project }: ProjectPropertiesDialogProps) =>
             <Dialog open={open}
                 onClose={closeDialog}
                 fullWidth
@@ -65,16 +54,11 @@ export const ProjectPropertiesDialog = connect(mapStateToProps, mapDispatchToPro
                 <DialogContent>
                     <ProjectPropertiesForm />
                     {project && project.properties &&
-                        Object.keys(project.properties).map(k => {
-                            const label = `${getTagKeyLabel(k, vocabulary)}: ${getTagValueLabel(k, project.properties[k], vocabulary)}`;
-                            return (
-                                <CopyToClipboard key={k} text={label} onCopy={() => onCopy("Copied")}>
-                                    <Chip key={k} className={classes.tag}
-                                        onDelete={() => handleDelete(k)}
-                                        label={label} />
-                                </CopyToClipboard>
-                            );
-                        })
+                        Object.keys(project.properties).map(k =>
+                            <PropertyChipComponent
+                                onDelete={() => handleDelete(k)}
+                                key={k} className={classes.tag}
+                                propKey={k} propValue={project.properties[k]} />)
                     }
                 </DialogContent>
                 <DialogActions>
@@ -86,4 +70,5 @@ export const ProjectPropertiesDialog = connect(mapStateToProps, mapDispatchToPro
                     </Button>
                 </DialogActions>
             </Dialog>
-)));
\ No newline at end of file
+    )
+));
\ No newline at end of file
diff --git a/src/views-components/resource-properties-form/property-chip.tsx b/src/views-components/resource-properties-form/property-chip.tsx
new file mode 100644
index 00000000..c51a8d8e
--- /dev/null
+++ b/src/views-components/resource-properties-form/property-chip.tsx
@@ -0,0 +1,52 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Chip } from '@material-ui/core';
+import { connect } from 'react-redux';
+import { RootState } from '~/store/store';
+import * as CopyToClipboard from 'react-copy-to-clipboard';
+import { getVocabulary } from '~/store/vocabulary/vocabulary-selectors';
+import { Dispatch } from 'redux';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { getTagValueLabel, getTagKeyLabel, Vocabulary } from '~/models/vocabulary';
+
+interface PropertyChipComponentDataProps {
+    propKey: string;
+    propValue: string;
+    className: string;
+    vocabulary: Vocabulary;
+}
+
+interface PropertyChipComponentActionProps {
+    onDelete?: () => void;
+    onCopy: (message: string) => void;
+}
+
+type PropertyChipComponentProps = PropertyChipComponentActionProps & PropertyChipComponentDataProps;
+
+const mapStateToProps = ({ properties }: RootState) => ({
+    vocabulary: getVocabulary(properties),
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+    onCopy: (message: string) => dispatch(snackbarActions.OPEN_SNACKBAR({
+        message,
+        hideDuration: 2000,
+        kind: SnackbarKind.SUCCESS
+    }))
+});
+
+// Renders a Chip with copyable-on-click tag:value data based on the vocabulary
+export const PropertyChipComponent = connect(mapStateToProps, mapDispatchToProps)(
+    ({ propKey, propValue, vocabulary, className, onCopy, onDelete }: PropertyChipComponentProps) => {
+        const label = `${getTagKeyLabel(propKey, vocabulary)}: ${getTagValueLabel(propKey, propValue, vocabulary)}`;
+        return (
+            <CopyToClipboard key={propKey} text={label} onCopy={() => onCopy("Copied to clipboard")}>
+                <Chip onDelete={onDelete} key={propKey}
+                    className={className} label={label} />
+            </CopyToClipboard>
+        );
+    }
+);
diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx
index 87768e64..28ae2f05 100644
--- a/src/views/collection-panel/collection-panel.tsx
+++ b/src/views/collection-panel/collection-panel.tsx
@@ -5,9 +5,8 @@
 import * as React from 'react';
 import {
     StyleRulesCallback, WithStyles, withStyles, Card,
-    CardHeader, IconButton, CardContent, Grid, Chip, Tooltip
+    CardHeader, IconButton, CardContent, Grid, Tooltip
 } from '@material-ui/core';
-import { compose } from "redux";
 import { connect, DispatchProp } from "react-redux";
 import { RouteComponentProps } from 'react-router';
 import { ArvadosTheme } from '~/common/custom-theme';
@@ -25,10 +24,8 @@ import { formatFileSize } from "~/common/formatters";
 import { getResourceData } from "~/store/resources-data/resources-data";
 import { ResourceData } from "~/store/resources-data/resources-data-reducer";
 import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
-import * as CopyToClipboard from 'react-copy-to-clipboard';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
-import { connectVocabulary, VocabularyProp } from '~/views-components/resource-properties-form/property-field-common';
-import { getTagValueLabel, getTagKeyLabel } from '~/models/vocabulary';
+import { PropertyChipComponent } from '~/views-components/resource-properties-form/property-chip';
 
 type CssRules = 'card' | 'iconHeader' | 'tag' | 'label' | 'value' | 'link';
 
@@ -68,19 +65,16 @@ interface CollectionPanelDataProps {
 type CollectionPanelProps = CollectionPanelDataProps & DispatchProp
     & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
 
-export const CollectionPanel = compose(
-    connectVocabulary,
-    withStyles(styles))(
-        connect((state: RootState, props: RouteComponentProps<{ id: string }> & VocabularyProp) => {
+export const CollectionPanel = withStyles(styles)(
+        connect((state: RootState, props: RouteComponentProps<{ id: string }>) => {
             const item = getResource(props.match.params.id)(state.resources);
             const data = getResourceData(props.match.params.id)(state.resourcesData);
-            const vocabulary = props.vocabulary;
-            return { item, data, vocabulary };
+            return { item, data };
         })(
-        class extends React.Component<CollectionPanelProps & VocabularyProp> {
+        class extends React.Component<CollectionPanelProps> {
 
             render() {
-                const { classes, item, data, dispatch, vocabulary } = this.props;
+                const { classes, item, data, dispatch } = this.props;
                 return item
                     ? <>
                         <Card className={classes.card}>
@@ -136,14 +130,12 @@ export const CollectionPanel = compose(
                                         <CollectionTagForm />
                                     </Grid>
                                     <Grid item xs={12}>
-                                        {Object.keys(item.properties).map(k => {
-                                            const label = `${getTagKeyLabel(k, vocabulary)}: ${getTagValueLabel(k, item.properties[k], vocabulary)}`;
-                                            return <CopyToClipboard key={k} text={label} onCopy={() => this.onCopy("Copied")}>
-                                                <Chip className={classes.tag}
-                                                    onDelete={this.handleDelete(k)}
-                                                    label={label} />
-                                            </CopyToClipboard>;
-                                        })}
+                                        {Object.keys(item.properties).map(k =>
+                                            <PropertyChipComponent
+                                                key={k} className={classes.tag}
+                                                onDelete={this.handleDelete(k)}
+                                                propKey={k} propValue={item.properties[k]} />
+                                        )}
                                     </Grid>
                                 </Grid>
                             </CardContent>

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list