feat: Add support for List and Checkbox in Custom Attributes (#3439)

This commit is contained in:
Sivin Varghese 2021-11-24 09:00:55 +05:30 committed by GitHub
parent 0530e9491c
commit da8f9d0337
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 388 additions and 94 deletions

View file

@ -2,77 +2,113 @@
<div class="custom-attribute">
<div class="title-wrap">
<h4 class="text-block-title title error">
<span class="attribute-name" :class="{ error: $v.editedValue.$error }">
{{ label }}
</span>
<div v-if="isAttributeTypeCheckbox" class="checkbox-wrap">
<input
v-model="editedValue"
class="checkbox"
type="checkbox"
@change="onUpdate"
/>
</div>
<div class="name-button__wrap">
<span
class="attribute-name"
:class="{ error: $v.editedValue.$error }"
>
{{ label }}
</span>
<woot-button
v-if="showActions"
v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
variant="link"
size="medium"
color-scheme="secondary"
icon="ion-trash-a"
class-names="delete-button"
@click="onDelete"
/>
</div>
</h4>
</div>
<div v-show="isEditing">
<div class="input-group small">
<input
ref="inputfield"
v-model="editedValue"
:type="inputType"
class="input-group-field"
autofocus="true"
:class="{ error: $v.editedValue.$error }"
@blur="$v.editedValue.$touch"
@keyup.enter="onUpdate"
/>
<div class="input-group-button">
<woot-button size="small" icon="ion-checkmark" @click="onUpdate" />
<div v-if="notAttributeTypeCheckboxAndList">
<div v-show="isEditing">
<div class="input-group small">
<input
ref="inputfield"
v-model="editedValue"
:type="inputType"
class="input-group-field"
autofocus="true"
:class="{ error: $v.editedValue.$error }"
@blur="$v.editedValue.$touch"
@keyup.enter="onUpdate"
/>
<div class="input-group-button">
<woot-button size="small" icon="ion-checkmark" @click="onUpdate" />
</div>
</div>
<span v-if="shouldShowErrorMessage" class="error-message">
{{ errorMessage }}
</span>
</div>
<div
v-show="!isEditing"
class="value--view"
:class="{ 'is-editable': showActions }"
>
<a
v-if="isAttributeTypeLink"
:href="value"
target="_blank"
rel="noopener noreferrer"
class="value"
>
{{ value || '---' }}
</a>
<p v-else class="value">
{{ formattedValue || '---' }}
</p>
<div class="action-buttons__wrap">
<woot-button
v-if="showActions"
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
variant="link"
size="small"
color-scheme="secondary"
icon="ion-clipboard"
class-names="edit-button"
@click="onCopy"
/>
<woot-button
v-if="showActions"
v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
variant="link"
size="small"
color-scheme="secondary"
icon="ion-compose"
class-names="edit-button"
@click="onEdit"
/>
</div>
</div>
<span v-if="shouldShowErrorMessage" class="error-message">
{{ errorMessage }}
</span>
</div>
<div
v-show="!isEditing"
class="value--view"
:class="{ 'is-editable': showActions }"
>
<a
v-if="isAttributeTypeLink"
:href="value"
target="_blank"
rel="noopener noreferrer"
class="value"
>
{{ value || '---' }}
</a>
<p v-else class="value">
{{ formattedValue || '---' }}
</p>
<woot-button
v-if="showActions"
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
variant="link"
size="small"
color-scheme="secondary"
icon="ion-clipboard"
class-names="edit-button"
@click="onCopy"
/>
<woot-button
v-if="showActions"
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
variant="link"
size="small"
color-scheme="secondary"
icon="ion-compose"
class-names="edit-button"
@click="onEdit"
/>
<woot-button
v-if="showActions"
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
variant="link"
size="small"
color-scheme="secondary"
icon="ion-trash-a"
class-names="edit-button"
@click="onDelete"
<div v-if="isAttributeTypeList">
<multiselect-dropdown
:options="listOptions"
:selected-item="selectedItem"
:has-thumbnail="false"
:multiselector-placeholder="
$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.PLACEHOLDER')
"
:no-search-result="
$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.NO_RESULT')
"
:input-placeholder="
$t(
'CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.SEARCH_INPUT_PLACEHOLDER'
)
"
@click="onUpdateListValue"
/>
</div>
</div>
@ -82,13 +118,18 @@
import format from 'date-fns/format';
import { required, url } from 'vuelidate/lib/validators';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
const DATE_FORMAT = 'yyyy-MM-dd';
export default {
components: {
MultiselectDropdown,
},
props: {
label: { type: String, required: true },
value: { type: [String, Number], default: '' },
values: { type: Array, default: () => [] },
value: { type: [String, Number, Boolean], default: '' },
showActions: { type: Boolean, default: false },
attributeType: { type: String, default: 'text' },
attributeKey: { type: String, required: true },
@ -115,9 +156,28 @@ export default {
},
computed: {
listOptions() {
return this.values.map((value, index) => ({
id: index + 1,
name: value,
}));
},
selectedItem() {
const id = this.values.indexOf(this.editedValue) + 1;
return { id, name: this.editedValue };
},
isAttributeTypeCheckbox() {
return this.attributeType === 'checkbox';
},
isAttributeTypeList() {
return this.attributeType === 'list';
},
isAttributeTypeLink() {
return this.attributeType === 'link';
},
notAttributeTypeCheckboxAndList() {
return !this.isAttributeTypeCheckbox && !this.isAttributeTypeList;
},
inputType() {
return this.isAttributeTypeLink ? 'url' : this.attributeType;
},
@ -156,6 +216,12 @@ export default {
this.focusInput();
});
},
onUpdateListValue(value) {
if (value) {
this.editedValue = value.name;
this.onUpdate();
}
},
onUpdate() {
const updatedValue =
this.attributeType === 'date'
@ -194,8 +260,23 @@ export default {
display: flex;
align-items: center;
margin: 0;
width: 100%;
}
.checkbox-wrap {
display: flex;
align-items: center;
}
.checkbox {
margin: 0 var(--space-small) 0 0;
}
.name-button__wrap {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.attribute-name {
width: 100%;
&.error {
color: var(--r-400);
}
@ -206,22 +287,34 @@ export default {
.edit-button {
display: none;
}
.delete-button {
display: flex;
justify-content: flex-end;
width: var(--space-normal);
}
.value--view {
display: flex;
&.is-editable:hover {
.value {
background: var(--color-background);
margin-bottom: 0;
}
.edit-button {
display: block;
}
}
.action-buttons__wrap {
display: flex;
max-width: var(--space-larger);
}
}
.value {
display: inline-block;
min-width: var(--space-mega);
border-radius: var(--border-radius-small);
margin-bottom: 0;
word-break: break-all;
padding: var(--space-micro) var(--space-smaller);
}
@ -235,4 +328,17 @@ export default {
margin-top: -1.6rem;
width: 100%;
}
::v-deep {
.selector-wrap {
margin: 0;
top: var(--space-smaller);
.selector-name {
margin-left: 0;
}
}
.name {
margin-left: 0;
}
}
</style>

View file

@ -27,7 +27,12 @@
"TYPE": {
"LABEL": "Type",
"PLACEHOLDER": "Please select a type",
"ERROR": "Type is required"
"ERROR": "Type is required",
"LIST": {
"LABEL": "List Values",
"PLACEHOLDER": "Please enter value and press enter key",
"ERROR": "Must have at least one value"
}
},
"KEY": {
"LABEL": "Key",
@ -58,6 +63,12 @@
"EDIT": {
"TITLE": "Edit Custom Attribute",
"UPDATE_BUTTON_TEXT": "Update",
"TYPE": {
"LIST": {
"LABEL": "List Values",
"PLACEHOLDER": "Please enter values and press enter key"
}
},
"API": {
"SUCCESS_MESSAGE": "Custom Attribute updated successfully",
"ERROR_MESSAGE": "There was an error updating custom attribute, please try again"

View file

@ -279,6 +279,13 @@
"TITLE": "Add attributes",
"PLACEHOLDER": "Search attributes",
"NO_RESULT": "No attributes found"
},
"ATTRIBUTE_TYPE": {
"LIST": {
"PLACEHOLDER": "Select value",
"SEARCH_INPUT_PLACEHOLDER": "Search value",
"NO_RESULT": "No result found"
}
}
},
"VALIDATIONS": {

View file

@ -5,6 +5,7 @@
:key="attribute.id"
:attribute-key="attribute.attribute_key"
:attribute-type="attribute.attribute_display_type"
:values="attribute.attribute_values"
:label="attribute.attribute_display_name"
:icon="attribute.icon"
emoji=""

View file

@ -30,6 +30,15 @@
@input="onDisplayNameChange"
@blur="$v.displayName.$touch"
/>
<woot-input
v-model="attributeKey"
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.LABEL')"
type="text"
:class="{ error: $v.attributeKey.$error }"
:error="$v.attributeKey.$error ? keyErrorMessage : ''"
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.PLACEHOLDER')"
@blur="$v.attributeKey.$touch"
/>
<label :class="{ error: $v.description.$error }">
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.LABEL') }}
<textarea
@ -54,15 +63,29 @@
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
</span>
</label>
<woot-input
v-model="attributeKey"
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.LABEL')"
type="text"
:class="{ error: $v.attributeKey.$error }"
:error="$v.attributeKey.$error ? keyErrorMessage : ''"
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.PLACEHOLDER')"
@blur="$v.attributeKey.$touch"
/>
<div v-if="isAttributeTypeList" class="multiselect--wrap">
<label>
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.LABEL') }}
</label>
<multiselect
ref="tagInput"
v-model="values"
:placeholder="
$t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.PLACEHOLDER')
"
label="name"
track-by="name"
:class="{ invalid: isMultiselectInvalid }"
:options="options"
:multiple="true"
:taggable="true"
@close="onTouch"
@tag="addTagValue"
/>
<label v-show="isMultiselectInvalid" class="error-message">
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }}
</label>
</div>
<div class="modal-footer">
<woot-submit-button
:disabled="isButtonDisabled"
@ -103,7 +126,10 @@ export default {
attributeKey: '',
models: ATTRIBUTE_MODELS,
types: ATTRIBUTE_TYPES,
values: [],
options: [],
show: true,
isTouched: false,
};
},
@ -111,11 +137,21 @@ export default {
...mapGetters({
uiFlags: 'getUIFlags',
}),
isMultiselectInvalid() {
return this.isTouched && this.values.length === 0;
},
isTagInputInvalid() {
return this.isAttributeTypeList && this.values.length === 0;
},
attributeListValues() {
return this.values.map(item => item.name);
},
isButtonDisabled() {
return (
this.$v.displayName.$invalid ||
this.$v.description.$invalid ||
this.uiFlags.isCreating
this.uiFlags.isCreating ||
this.isTagInputInvalid
);
},
keyErrorMessage() {
@ -124,6 +160,9 @@ export default {
}
return this.$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.ERROR');
},
isAttributeTypeList() {
return this.attributeType === 6;
},
},
validations: {
@ -149,6 +188,16 @@ export default {
},
methods: {
addTagValue(tagValue) {
const tag = {
name: tagValue,
};
this.values.push(tag);
this.$refs.tagInput.$el.focus();
},
onTouch() {
this.isTouched = true;
},
onDisplayNameChange() {
this.attributeKey = convertToSlug(this.displayName);
},
@ -164,6 +213,7 @@ export default {
attribute_model: this.attributeModel,
attribute_display_type: this.attributeType,
attribute_key: this.attributeKey,
attribute_values: this.attributeListValues,
});
this.alertMessage = this.$t('ATTRIBUTES_MGMT.ADD.API.SUCCESS_MESSAGE');
this.onClose();
@ -183,4 +233,30 @@ export default {
padding: 0 var(--space-small) var(--space-small) 0;
font-family: monospace;
}
.multiselect--wrap {
margin-bottom: var(--space-normal);
.error-message {
color: var(--r-400);
font-size: var(--font-size-small);
font-weight: var(--font-weight-normal);
}
.invalid {
::v-deep {
.multiselect__tags {
border: 1px solid var(--r-400);
}
}
}
}
::v-deep {
.multiselect {
margin-bottom: 0;
}
.multiselect__content-wrapper {
display: none;
}
.multiselect--active .multiselect__tags {
border-radius: var(--border-radius-normal);
}
}
</style>

View file

@ -16,6 +16,16 @@
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.NAME.PLACEHOLDER')"
@blur="$v.displayName.$touch"
/>
<woot-input
v-model.trim="attributeKey"
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.LABEL')"
type="text"
:class="{ error: $v.attributeKey.$error }"
:error="$v.attributeKey.$error ? keyErrorMessage : ''"
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.PLACEHOLDER')"
readonly
@blur="$v.attributeKey.$touch"
/>
<label :class="{ error: $v.description.$error }">
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.LABEL') }}
<textarea
@ -40,22 +50,29 @@
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
</span>
</label>
<woot-input
v-model.trim="attributeKey"
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.LABEL')"
type="text"
:class="{ error: $v.attributeKey.$error }"
:error="$v.attributeKey.$error ? keyErrorMessage : ''"
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.PLACEHOLDER')"
readonly
@blur="$v.attributeKey.$touch"
/>
<div v-if="isAttributeTypeList" class="multiselect--wrap">
<label>
{{ $t('ATTRIBUTES_MGMT.EDIT.TYPE.LIST.LABEL') }}
</label>
<multiselect
ref="tagInput"
v-model="values"
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.PLACEHOLDER')"
label="name"
track-by="name"
:class="{ invalid: isMultiselectInvalid }"
:options="options"
:multiple="true"
:taggable="true"
@tag="addTagValue"
/>
<label v-show="isMultiselectInvalid" class="error-message">
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LIST.ERROR') }}
</label>
</div>
</div>
<div class="modal-footer">
<woot-button
:is-loading="isUpdating"
:disabled="$v.description.$invalid"
>
<woot-button :is-loading="isUpdating" :disabled="isButtonDisabled">
{{ $t('ATTRIBUTES_MGMT.EDIT.UPDATE_BUTTON_TEXT') }}
</woot-button>
<woot-button variant="clear" @click.prevent="onClose">
@ -92,6 +109,9 @@ export default {
types: ATTRIBUTE_TYPES,
show: true,
attributeKey: '',
values: [],
options: [],
isTouched: true,
};
},
validations: {
@ -116,6 +136,22 @@ export default {
...mapGetters({
uiFlags: 'attributes/getUIFlags',
}),
setAttributeListValue() {
return this.selectedAttribute.attribute_values.map(values => ({
name: values,
}));
},
updatedAttributeListValues() {
return this.values.map(item => item.name);
},
isButtonDisabled() {
return this.$v.description.$invalid || this.isMultiselectInvalid;
},
isMultiselectInvalid() {
return (
this.isAttributeTypeList && this.isTouched && this.values.length === 0
);
},
pageTitle() {
return `${this.$t('ATTRIBUTES_MGMT.EDIT.TITLE')} - ${
this.selectedAttribute.attribute_display_name
@ -134,6 +170,9 @@ export default {
}
return this.$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.ERROR');
},
isAttributeTypeList() {
return this.attributeType === 6;
},
},
mounted() {
this.setFormValues();
@ -142,11 +181,19 @@ export default {
onClose() {
this.$emit('on-close');
},
addTagValue(tagValue) {
const tag = {
name: tagValue,
};
this.values.push(tag);
this.$refs.tagInput.$el.focus();
},
setFormValues() {
this.displayName = this.selectedAttribute.attribute_display_name;
this.description = this.selectedAttribute.attribute_description;
this.attributeType = this.selectedAttributeType;
this.attributeKey = this.selectedAttribute.attribute_key;
this.values = this.setAttributeListValue;
},
async editAttributes() {
this.$v.$touch();
@ -158,6 +205,7 @@ export default {
id: this.selectedAttribute.id,
attribute_description: this.description,
attribute_display_name: this.displayName,
attribute_values: this.updatedAttributeListValues,
});
this.alertMessage = this.$t('ATTRIBUTES_MGMT.EDIT.API.SUCCESS_MESSAGE');
@ -178,4 +226,30 @@ export default {
padding: 0 var(--space-small) var(--space-small) 0;
font-family: monospace;
}
.multiselect--wrap {
margin-bottom: var(--space-normal);
.error-message {
color: var(--r-400);
font-size: var(--font-size-small);
font-weight: var(--font-weight-normal);
}
.invalid {
::v-deep {
.multiselect__tags {
border: 1px solid var(--r-400);
}
}
}
}
::v-deep {
.multiselect {
margin-bottom: 0;
}
.multiselect__content-wrapper {
display: none;
}
.multiselect--active .multiselect__tags {
border-radius: var(--border-radius-normal);
}
}
</style>

View file

@ -26,4 +26,12 @@ export const ATTRIBUTE_TYPES = [
id: 5,
option: 'Date',
},
{
id: 6,
option: 'List',
},
{
id: 7,
option: 'Checkbox',
},
];

View file

@ -12,7 +12,7 @@
>
<div class="selector-user-wrap">
<Thumbnail
v-if="hasValue"
v-if="hasValue && hasThumbnail"
:src="selectedItem.thumbnail"
size="24px"
:status="selectedItem.availability_status"
@ -48,6 +48,7 @@
v-if="showSearchDropdown"
:options="options"
:selected-item="selectedItem"
:has-thumbnail="hasThumbnail"
:input-placeholder="inputPlaceholder"
:no-search-result="noSearchResult"
@click="onClickSelectItem"
@ -75,9 +76,13 @@ export default {
type: Object,
default: () => ({}),
},
hasThumbnail: {
type: Boolean,
default: true,
},
multiselectorTitle: {
type: String,
default: 'Select',
default: '',
},
multiselectorPlaceholder: {
type: String,
@ -116,6 +121,7 @@ export default {
onClickSelectItem(value) {
this.$emit('click', value);
this.onCloseDropdown();
},
},
};

View file

@ -27,6 +27,7 @@
>
<div class="user-wrap">
<Thumbnail
v-if="hasThumbnail"
:src="option.thumbnail"
size="24px"
:username="option.name"
@ -76,6 +77,10 @@ export default {
type: Object,
default: () => ({}),
},
hasThumbnail: {
type: Boolean,
default: true,
},
inputPlaceholder: {
type: String,
default: 'Search',