Edit number/text column inside item card [SCI-9600][SCI-9696] (#6546)

* Refactor Repository Text and Number value component [SCI-9600]

- Refactor text and number components.
- Add number validation.

* Create a Textarea standalone component [SCI-9600][SCI-9618]
This commit is contained in:
Soufiane 2023-11-07 12:30:49 +01:00 committed by GitHub
parent a0da544e27
commit dbff182314
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 209 additions and 124 deletions

View file

@ -0,0 +1,135 @@
<template>
<textarea v-if="editing"
ref="textareaRef"
class="leading-5 inline-block outline-none border-solid font-normal border-[1px] box-content
overflow-x-hidden overflow-y-auto resize-none rounded px-4 py-2 w-[calc(100%-2rem)]
border-sn-science-blue"
:class="{
'max-h-[4rem]': collapsed,
'max-h-[40rem]': !collapsed
}"
:placeholder="i18n.t('repositories.item_card.repository_number_value.placeholder')"
v-model="value"
@keydown="handleKeydown"
@blur="handleBlur" />
<div v-else
:ref="unEditableRef"
class="grid box-content sci-cursor-edit font-normal border-solid px-4 py-2 border-sn-light-grey rounded
leading-5 border outline-none hover:border-sn-sleepy-grey overflow-y-auto whitespace-pre-line
w-[calc(100%-2rem)]"
:class="{ 'max-h-[4rem]': collapsed,
'max-h-[40rem]': !collapsed, }"
@click="enableEdit">
<span v-html="value || noContentPlaceholder" ></span>
</div>
</template>
<script>
export default {
name: "Textarea",
data() {
return {
value: '',
editing: false,
};
},
props: {
expandable: { type: Boolean, required: true },
collapsed: { type: Boolean, required: true },
initialValue: String,
noContentPlaceholder: String,
decimals: { type: Number, default: null },
unEditableRef: { type: String, required: true },
},
mounted() {
this.value = this.initialValue;
this.$nextTick(() => {
this.toggleExpandableState();
});
},
beforeUpdate() {
if (!this.$refs.textareaRef) return;
if (this.decimals !== null) this.validateNumberInput();
},
watch: {
initialValue: {
handler() {
this.value = this.initialValue;
this.toggleExpandableState();
},
deep: true,
},
editing() {
this.$nextTick(() => {
if (this.editing) {
this.setCaretAtEnd();
this.refreshTextareaHeight();
return;
}
this.toggleExpandableState();
})
},
},
computed: {
canEdit() {
return this.permissions?.can_manage && !this.inArchivedRepositoryRow;
}
},
methods: {
handleKeydown(event) {
if (event.key === 'Enter') {
event.preventDefault();
this.$refs.textareaRef.blur();
}
},
handleBlur() {
this.editing = false;
this.toggleExpandableState();
this.$emit('update', this.value);
},
toggleExpandableState() {
this.$nextTick(() => {
if (!this.$refs[this.unEditableRef]) return;
const maxCollapsedHeight = '80';
const scrollHeight = this.$refs[this.unEditableRef].scrollHeight;
const expandable = scrollHeight > maxCollapsedHeight;
this.$emit('toggleExpandableState', expandable);
});
},
enableEdit(e) {
if (e && $(e.target).hasClass('atwho-user-popover')) return;
if (e && $(e.target).hasClass('sa-link')) return;
if (e && $(e.target).parent().hasClass('atwho-inserted')) return;
this.editing = true;
},
refreshTextareaHeight() {
this.$nextTick(() => {
if (!this.editing) return;
const textarea = this.$refs.textareaRef;
textarea.style.height = '0px';
// 16px is the height of the textarea's line
textarea.style.height = textarea.scrollHeight - 16 + 'px';
});
},
setCaretAtEnd() {
this.$nextTick(() => {
if (!this.editing) return;
this.$refs.textareaRef.focus();
});
},
validateNumberInput() {
const regexp = this.decimals === 0 ? /[^0-9]/g : /[^0-9.]/g;
const decimalsRegex = new RegExp(`^\\d*(\\.\\d{0,${this.decimals}})?`);
let value = this.value;
value = value.replace(regexp, '');
value = value.match(decimalsRegex)[0];
this.value = value;
}
},
};
</script>

View file

@ -1,7 +1,6 @@
<template> <template>
<div id="repository-number-value-wrapper" <div id="repository-number-value-wrapper"
class="flex flex-col min-h-[46px] h-auto gap-[6px]" class="flex flex-col min-h-[46px] h-auto gap-[6px]">
>
<div class="font-inter text-sm font-semibold leading-5 flex justify-between"> <div class="font-inter text-sm font-semibold leading-5 flex justify-between">
<div class="truncate" :class="{ 'w-4/5': expandable }" :title="colName">{{ colName }}</div> <div class="truncate" :class="{ 'w-4/5': expandable }" :title="colName">{{ colName }}</div>
<div @click="toggleCollapse" <div @click="toggleCollapse"
@ -14,90 +13,76 @@
}} }}
</div> </div>
</div> </div>
<div> <div v-if="canEdit">
<inline-edit <text-area :initialValue="colVal.toString()"
v-if="permissions?.can_manage && !inArchivedRepositoryRow" :noContentPlaceholder="noContentPlaceholder"
ref="numberRef" :decimals="decimals"
:value="colVal" :unEditableRef="`numberRef`"
:placeholder="i18n.t('repositories.item_card.repository_number_value.placeholder')" :expandable="expandable"
:noContentPlaceholder="i18n.t('repositories.item_card.repository_number_value.no_number')" :collapsed="collapsed"
:allowBlank="true" @toggleExpandableState="toggleExpandableState"
:smartAnnotation="false" @update="update" />
:attributeName="`${colName} `" </div>
:allowNewLine="false"
:singleLine="false"
:expandable="true"
:collapsed="collapsed"
@editingEnabled="editing = true"
@editingDisabled="editing = false"
@update="update"
></inline-edit>
<div v-else-if="colVal" <div v-else-if="colVal"
ref="numberRef" ref="numberRef"
class="text-sn-dark-grey box-content font-inter text-sm font-normal leading-5 min-h-[20px] overflow-y-auto" class="text-sn-dark-grey box-content font-inter text-sm font-normal leading-5 min-h-[20px] overflow-y-auto"
:class="{ :class="{
'max-h-[4rem]': collapsed, 'max-h-[4rem]': collapsed,
'max-h-[40rem]': !collapsed 'max-h-[40rem]': !collapsed
}" }">
>
{{ colVal }} {{ colVal }}
</div> </div>
<div v-else <div v-else
class="text-sn-dark-grey font-inter text-sm font-normal leading-5"> class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t("repositories.item_card.repository_number_value.no_number") }} {{ i18n.t("repositories.item_card.repository_number_value.no_number") }}
</div> </div>
</div>
</div> </div>
</template> </template>
<script> <script>
import InlineEdit from "../../shared/inline_edit.vue";
import repositoryValueMixin from "./mixins/repository_value.js"; import repositoryValueMixin from "./mixins/repository_value.js";
import Textarea from "../Textarea.vue";
export default { export default {
name: "RepositoryNumberValue", name: "RepositoryNumberValue",
mixins: [repositoryValueMixin], mixins: [repositoryValueMixin],
components: { components: {
"inline-edit": InlineEdit 'text-area': Textarea,
},
data() {
return {
expandable: false,
collapsed: true,
numberValue: '',
};
}, },
props: { props: {
data_type: String, data_type: String,
inArchivedRepository: Boolean,
colId: Number, colId: Number,
colName: String, colName: String,
colVal: String, colVal: String,
permissions: null, permissions: null,
inArchivedRepositoryRow: Boolean, inArchivedRepositoryRow: Boolean,
}, },
data() { created() {
return { // constants
expandable: false, this.noContentPlaceholder = this.i18n.t("repositories.item_card.repository_number_value.no_number");
collapsed: true this.decimals = Number(document.getElementById(`${this.colId}`).dataset['metadataDecimals']) || 0;
};
}, },
mounted() { computed: {
this.$nextTick(() => { canEdit() {
this.toggleExpandableState() return this.permissions?.can_manage && !this.inArchivedRepositoryRow;
}); }
},
updated() {
this.$nextTick(() => {
this.toggleExpandableState();
});
}, },
methods: { methods: {
toggleCollapse() { toggleCollapse() {
if (this.expandable) { if (!this.expandable) return;
this.collapsed = !this.collapsed;
}
},
toggleExpandableState() {
if (!this.$refs.numberRef) return;
const offsetHeight = this.$refs.numberRef.offsetHeight; this.collapsed = !this.collapsed;
const scrollHeight = this.$refs.numberRef.scrollHeight; },
this.expandable = scrollHeight > offsetHeight; toggleExpandableState(expandable) {
} this.expandable = expandable;
},
} }
}; };
</script> </script>

View file

@ -13,59 +13,49 @@
}} }}
</div> </div>
</div> </div>
<div>
<inline-edit <div v-if="canEdit">
v-if="permissions?.can_manage && !inArchivedRepositoryRow" <text-area :initialValue="colVal?.edit"
ref="textRef" :noContentPlaceholder="noContentPlaceholder"
:value="colVal?.edit" :unEditableRef="`textRef`"
:placeholder="i18n.t('repositories.item_card.repository_text_value.placeholder')" :expandable="expandable"
:noContentPlaceholder="i18n.t('repositories.item_card.repository_text_value.no_text')" :collapsed="collapsed"
:allowBlank="true" @toggleExpandableState="toggleExpandableState"
:smartAnnotation="false" @update="update" />
:attributeName="`${colName} `" </div>
:allowNewLine="false" <div v-else-if="colVal?.edit"
:singleLine="false" ref="textRef"
:expandable="true" class="text-sn-dark-grey box-content text-sm font-normal leading-5 overflow-y-auto px-4 py-2
:collapsed="collapsed" border-sn-light-grey border border-solid rounded w-[calc(100%-2rem)]]"
@editingEnabled="editing = true" :class="{
@editingDisabled="editing = false" 'max-h-[4rem]': collapsed,
@update="update" 'max-h-[40rem]': !collapsed
></inline-edit> }"
<div v-else-if="colVal?.edit" >
ref="textRef" {{ colVal?.edit }}
class="text-sn-dark-grey box-content font-inter text-sm font-normal leading-5 overflow-y-auto px-4 py-2 border-sn-light-grey border border-solid rounded" </div>
:class="{ <div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5 px-4 py-2 w-[calc(100%-2rem)]]">
'max-h-[4rem]': collapsed, {{ i18n.t("repositories.item_card.repository_text_value.no_text") }}
'max-h-[40rem]': !collapsed
}"
>
{{ colVal?.edit }}
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t("repositories.item_card.repository_text_value.no_text") }}
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import InlineEdit from "../../shared/inline_edit.vue";
import repositoryValueMixin from "./mixins/repository_value.js"; import repositoryValueMixin from "./mixins/repository_value.js";
import Textarea from "../Textarea.vue";
export default { export default {
name: "RepositoryTextValue", name: "RepositoryTextValue",
mixins: [repositoryValueMixin], mixins: [repositoryValueMixin],
components: { components: {
"inline-edit": InlineEdit 'text-area': Textarea,
}, },
data() { data() {
return { return {
edit: null,
view: null,
contentExpanded: false,
expandable: false, expandable: false,
collapsed: true collapsed: true,
} textValue: '',
};
}, },
props: { props: {
data_type: String, data_type: String,
@ -75,48 +65,23 @@ export default {
permissions: null, permissions: null,
inArchivedRepositoryRow: Boolean, inArchivedRepositoryRow: Boolean,
}, },
mounted() { created() {
this.$nextTick(() => { // constants
if (this.$refs.textRef) { this.noContentPlaceholder = this.i18n.t("repositories.item_card.repository_text_value.no_text");
const textHeight = this.$refs.textRef.scrollHeight
this.expandable = textHeight > 60 // 60px
}
this.toggleExpandableState();
});
}, },
watch: { computed: {
editing() { canEdit() {
if (this.editing) return; return this.permissions?.can_manage && !this.inArchivedRepositoryRow;
this.toggleExpandableState();
} }
}, },
methods: { methods: {
toggleCollapse() { toggleCollapse() {
if (this.expandable) { if (!this.expandable) return;
this.collapsed = !this.collapsed;
} this.collapsed = !this.collapsed;
}, },
toggleExpandableState() { toggleExpandableState(expandable) {
if (!this.$refs.textRef) return; this.expandable = expandable;
let offsetHeight;
let scrollHeight;
if (Object.keys(this.$refs.textRef.$refs).length > 0) {
const keys = Object.keys(this.$refs.textRef.$refs)
keys.forEach((ref) => {
if (this.$refs.textRef.$refs[ref] !== undefined) {
offsetHeight = this.$refs.textRef.$refs[ref].offsetHeight;
scrollHeight = this.$refs.textRef.$refs[ref].scrollHeight;
this.expandable = scrollHeight > offsetHeight;
}
});
return;
}
offsetHeight = this.$refs.textRef.offsetHeight;
scrollHeight = this.$refs.textRef.scrollHeight;
this.expandable = scrollHeight > offsetHeight;
}, },
}, },
}; };