fix: create community and channel name validation
Fixes #2050. This PR contains changes to fix the name validation for new communities and new channels in communities. In the process of updating this, better validation was also added to both popups (create community and create channel), including the prevention of the "Create" button from being enabled until all form fields were valid. During this process, it was noticed that the community image cropper was not actually cropping the image *in the preview*. Once the community was created, status-go was successfully cropping the image as the user intended. However, the preview thumbnail prior to creation was not accurately showing the cropped image preview and showing the entire image centred instead. *This is still yet to be fixed.* One solution is to upgrade Qt to `5.15` to take advantage of Image QML's `sourceClipRect`.
This commit is contained in:
parent
7eda54a141
commit
9e6bd7a2da
|
@ -10,6 +10,9 @@ ModalPopup {
|
|||
property string communityId
|
||||
readonly property int maxDescChars: 140
|
||||
property string nameValidationError: ""
|
||||
property bool isValid:
|
||||
nameInput.isValid &&
|
||||
descriptionTextArea.isValid
|
||||
|
||||
id: popup
|
||||
height: 600
|
||||
|
@ -18,22 +21,12 @@ ModalPopup {
|
|||
nameInput.text = "";
|
||||
nameInput.forceActiveFocus(Qt.MouseFocusReason)
|
||||
}
|
||||
onClosed: destroy()
|
||||
|
||||
function validate() {
|
||||
nameValidationError = ""
|
||||
|
||||
if (nameInput.text === "") {
|
||||
//% "You need to enter a name"
|
||||
nameValidationError = qsTrId("you-need-to-enter-a-name")
|
||||
} else if (!(/^[a-z0-9\-\ ]+$/i.test(nameInput.text))) {
|
||||
//% "Please restrict your name to letters, numbers, dashes and spaces"
|
||||
nameValidationError = qsTrId("please-restrict-your-name-to-letters--numbers--dashes-and-spaces")
|
||||
} else if (nameInput.text.length > 100) {
|
||||
//% "Your name needs to be 100 characters or shorter"
|
||||
nameValidationError = qsTrId("your-name-needs-to-be-100-characters-or-shorter")
|
||||
}
|
||||
|
||||
return !nameValidationError && !descriptionTextArea.validationError
|
||||
nameInput.validate()
|
||||
descriptionTextArea.validate()
|
||||
return isValid
|
||||
}
|
||||
|
||||
//% "New channel"
|
||||
|
@ -66,6 +59,30 @@ ModalPopup {
|
|||
//% "A cool name"
|
||||
placeholderText: qsTrId("a-cool-name")
|
||||
validationError: popup.nameValidationError
|
||||
|
||||
property bool isValid: false
|
||||
|
||||
onTextEdited: {
|
||||
if (text.includes(" ")) {
|
||||
text = text.replace(" ", "-")
|
||||
}
|
||||
validate()
|
||||
}
|
||||
|
||||
function validate() {
|
||||
validationError = ""
|
||||
if (nameInput.text === "") {
|
||||
//% "You need to enter a name"
|
||||
validationError = qsTrId("you-need-to-enter-a-name")
|
||||
} else if (!(/^[a-z0-9\-]+$/.test(nameInput.text))) {
|
||||
validationError = qsTr("Use only lowercase letters (a to z), numbers & dashes (-). Do not use chat keys.")
|
||||
} else if (nameInput.text.length > 100) {
|
||||
//% "Your name needs to be 100 characters or shorter"
|
||||
validationError = qsTrId("your-name-needs-to-be-100-characters-or-shorter")
|
||||
}
|
||||
isValid = validationError === ""
|
||||
return validationError
|
||||
}
|
||||
}
|
||||
|
||||
StyledTextArea {
|
||||
|
@ -75,10 +92,30 @@ ModalPopup {
|
|||
//% "What your channel is about"
|
||||
placeholderText: qsTrId("what-your-channel-is-about")
|
||||
//% "The description cannot exceed %1 characters"
|
||||
validationError: descriptionTextArea.text.length > maxDescChars ? qsTrId("the-description-cannot-exceed--1-characters").arg(maxDescChars) : ""
|
||||
validationError: descriptionTextArea.text.length > popup.maxDescChars ? qsTrId("the-description-cannot-exceed-140-characters") :
|
||||
popup.descriptionValidationError || ""
|
||||
anchors.top: nameInput.bottom
|
||||
anchors.topMargin: Style.current.bigPadding
|
||||
customHeight: 88
|
||||
|
||||
property bool isValid: false
|
||||
onTextChanged: validate()
|
||||
|
||||
function resetValidation() {
|
||||
isValid = false
|
||||
validationError = ""
|
||||
}
|
||||
|
||||
function validate() {
|
||||
validationError = ""
|
||||
if (text.length > popup.maxDescChars) {
|
||||
validationError = qsTrId("the-description-cannot-exceed-140-characters")
|
||||
}
|
||||
if (text === "") {
|
||||
validationError = qsTr("You need to enter a description")
|
||||
}
|
||||
isValid = validationError === ""
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
|
@ -129,6 +166,7 @@ ModalPopup {
|
|||
}
|
||||
|
||||
footer: StatusButton {
|
||||
enabled: popup.isValid
|
||||
//% "Create"
|
||||
text: qsTrId("create")
|
||||
anchors.right: parent.right
|
||||
|
|
|
@ -8,67 +8,34 @@ import "../../../../shared/status"
|
|||
|
||||
ModalPopup {
|
||||
readonly property int maxDescChars: 140
|
||||
property string nameValidationError: ""
|
||||
property string descriptionValidationError: ""
|
||||
property string colorValidationError: ""
|
||||
property string selectedImageValidationError: ""
|
||||
property string selectedImage: ""
|
||||
property var imageDimensions: ({
|
||||
aX: 0,
|
||||
aY: 0,
|
||||
bY: 1,
|
||||
bY: 1
|
||||
})
|
||||
|
||||
property QtObject community: chatsModel.communities.activeCommunity
|
||||
|
||||
property bool isEdit: false
|
||||
property bool isValid:
|
||||
nameInput.isValid &&
|
||||
addImageButton.isValid &&
|
||||
descriptionTextArea.isValid &&
|
||||
colorPicker.isValid
|
||||
|
||||
id: popup
|
||||
height: 600
|
||||
|
||||
onOpened: {
|
||||
nameInput.text = isEdit ? community.name : "";
|
||||
descriptionTextArea.text = isEdit ? community.description : "";
|
||||
nameValidationError = "";
|
||||
colorValidationError = "";
|
||||
selectedImageValidationError = "";
|
||||
if (isEdit) {
|
||||
nameInput.text = community.name;
|
||||
descriptionTextArea.text = community.description;
|
||||
}
|
||||
nameInput.forceActiveFocus(Qt.MouseFocusReason)
|
||||
}
|
||||
onClosed: destroy()
|
||||
|
||||
function validate() {
|
||||
nameValidationError = ""
|
||||
colorValidationError = ""
|
||||
selectedImageValidationError = ""
|
||||
descriptionValidationError = ""
|
||||
|
||||
if (nameInput.text === "") {
|
||||
//% "You need to enter a name"
|
||||
nameValidationError = qsTrId("you-need-to-enter-a-name")
|
||||
} else if (!(/^[a-z0-9\-\ ]+$/i.test(nameInput.text))) {
|
||||
//% "Please restrict your name to letters, numbers, dashes and spaces"
|
||||
nameValidationError = qsTrId("please-restrict-your-name-to-letters--numbers--dashes-and-spaces")
|
||||
} else if (nameInput.text.length > 100) {
|
||||
//% "Your name needs to be 100 characters or shorter"
|
||||
nameValidationError = qsTrId("your-name-needs-to-be-100-characters-or-shorter")
|
||||
}
|
||||
|
||||
if (descriptionTextArea.text === "") {
|
||||
descriptionValidationError = qsTr("You need to enter a description")
|
||||
}
|
||||
|
||||
if (selectedImage === "") {
|
||||
//% "You need to select an image"
|
||||
selectedImageValidationError = qsTrId("you-need-to-select-an-image")
|
||||
}
|
||||
|
||||
if (colorPicker.text === "") {
|
||||
colorValidationError = qsTr("You need to enter a color")
|
||||
} else if (!Utils.isHexColor(colorPicker.text)) {
|
||||
colorValidationError = qsTr("This field needs to be an hexadecimal color (eg: #4360DF)")
|
||||
}
|
||||
|
||||
return !nameValidationError && !descriptionTextArea.validationError && !colorValidationError && !descriptionValidationError
|
||||
nameInput.validate()
|
||||
addImageButton.validate()
|
||||
descriptionTextArea.validate()
|
||||
colorPicker.validate()
|
||||
return isValid
|
||||
}
|
||||
|
||||
title: isEdit ?
|
||||
|
@ -106,7 +73,29 @@ ModalPopup {
|
|||
label: qsTrId("name-your-community")
|
||||
//% "A catchy name"
|
||||
placeholderText: qsTrId("name-your-community-placeholder")
|
||||
validationError: popup.nameValidationError
|
||||
property bool isValid: false
|
||||
|
||||
onTextEdited: {
|
||||
if (text.includes(" ")) {
|
||||
text = text.replace(" ", "-")
|
||||
}
|
||||
validate()
|
||||
}
|
||||
|
||||
function validate() {
|
||||
validationError = ""
|
||||
if (nameInput.text === "") {
|
||||
//% "You need to enter a name"
|
||||
validationError = qsTrId("you-need-to-enter-a-name")
|
||||
} else if (!(/^[a-z0-9\-]+$/.test(nameInput.text))) {
|
||||
validationError = qsTr("Use only lowercase letters (a to z), numbers & dashes (-). Do not use chat keys.")
|
||||
} else if (nameInput.text.length > 100) {
|
||||
//% "Your name needs to be 100 characters or shorter"
|
||||
validationError = qsTrId("your-name-needs-to-be-100-characters-or-shorter")
|
||||
}
|
||||
isValid = validationError === ""
|
||||
return validationError
|
||||
}
|
||||
}
|
||||
|
||||
StyledTextArea {
|
||||
|
@ -116,18 +105,37 @@ ModalPopup {
|
|||
//% "What your community is about"
|
||||
placeholderText: qsTrId("what-your-community-is-about")
|
||||
//% "The description cannot exceed 140 characters"
|
||||
validationError: descriptionTextArea.text.length > maxDescChars ? qsTrId("the-description-cannot-exceed-140-characters") :
|
||||
validationError: descriptionTextArea.text.length > popup.maxDescChars ? qsTrId("the-description-cannot-exceed-140-characters") :
|
||||
popup.descriptionValidationError || ""
|
||||
anchors.top: nameInput.bottom
|
||||
anchors.topMargin: Style.current.bigPadding
|
||||
customHeight: 88
|
||||
textField.selectByMouse: true
|
||||
textField.wrapMode: TextEdit.Wrap
|
||||
|
||||
property bool isValid: false
|
||||
onTextChanged: validate()
|
||||
|
||||
function resetValidation() {
|
||||
isValid = false
|
||||
validationError = ""
|
||||
}
|
||||
|
||||
function validate() {
|
||||
validationError = ""
|
||||
if (text.length > popup.maxDescChars) {
|
||||
validationError = qsTrId("the-description-cannot-exceed-140-characters")
|
||||
}
|
||||
if (text === "") {
|
||||
validationError = qsTr("You need to enter a description")
|
||||
}
|
||||
isValid = validationError === ""
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: charLimit
|
||||
text: `${descriptionTextArea.text.length}/${maxDescChars}`
|
||||
text: `${descriptionTextArea.text.length}/${popup.maxDescChars}`
|
||||
anchors.top: descriptionTextArea.bottom
|
||||
anchors.topMargin: !descriptionTextArea.validationError ? 5 : - Style.current.smallPadding
|
||||
anchors.right: descriptionTextArea.right
|
||||
|
@ -139,9 +147,11 @@ ModalPopup {
|
|||
id: thumbnailText
|
||||
//% "Thumbnail image"
|
||||
text: qsTrId("thumbnail-image")
|
||||
anchors.top: descriptionTextArea.bottom
|
||||
anchors.top: charLimit.bottom
|
||||
anchors.topMargin: Style.current.smallPadding
|
||||
font.pixelSize: 15
|
||||
font.pixelSize: 13
|
||||
color: Style.current.textColor
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
|
||||
|
@ -154,6 +164,23 @@ ModalPopup {
|
|||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: thumbnailText.bottom
|
||||
anchors.topMargin: Style.current.padding
|
||||
property bool isValid: false
|
||||
property string selectedImage: ""
|
||||
|
||||
onSelectedImageChanged: validate()
|
||||
|
||||
function resetValidation() {
|
||||
isValid = false
|
||||
imageValidation.text = ""
|
||||
}
|
||||
|
||||
function validate() {
|
||||
imageValidation.text = ""
|
||||
if (selectedImage === "") {
|
||||
imageValidation.text = qsTr("Please select an image")
|
||||
}
|
||||
isValid = imageValidation.text === ""
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: imageDialog
|
||||
|
@ -164,27 +191,38 @@ ModalPopup {
|
|||
//% "Image files (*.jpg *.jpeg *.png)"
|
||||
qsTrId("image-files----jpg---jpeg---png-")
|
||||
]
|
||||
onRejected: {
|
||||
addImageButton.validate()
|
||||
}
|
||||
onAccepted: {
|
||||
popup.selectedImage = imageDialog.fileUrls[0]
|
||||
addImageButton.selectedImage = imageDialog.fileUrls[0]
|
||||
imageCropperModal.open()
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: imagePreview
|
||||
visible: !!popup.selectedImage
|
||||
source: popup.selectedImage
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
Rectangle {
|
||||
id: imagePreviewCropper
|
||||
clip: true
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
radius: parent.width / 2
|
||||
visible: !!addImageButton.selectedImage
|
||||
|
||||
Image {
|
||||
id: imagePreview
|
||||
visible: !!addImageButton.selectedImage
|
||||
source: addImageButton.selectedImage
|
||||
fillMode: Image.PreserveAspectFit
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
}
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: imagePreview.width
|
||||
height: imagePreview.height
|
||||
radius: imagePreview.width / 2
|
||||
width: imageCropperModal.width
|
||||
height: imageCropperModal.height
|
||||
radius: width / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -242,29 +280,52 @@ ModalPopup {
|
|||
|
||||
ImageCropperModal {
|
||||
id: imageCropperModal
|
||||
selectedImage: popup.selectedImage
|
||||
selectedImage: addImageButton.selectedImage
|
||||
ratio: "1:1"
|
||||
onCropFinished: {
|
||||
imageDimensions.aX = aX
|
||||
imageDimensions.aY = aY
|
||||
imageDimensions.bX = bX
|
||||
imageDimensions.bY = bY
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
StyledText {
|
||||
id: imageValidation
|
||||
visible: text && text !== ""
|
||||
anchors.top: addImageButton.bottom
|
||||
anchors.topMargin: Style.current.smallPadding
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
color: Style.current.danger
|
||||
}
|
||||
|
||||
Input {
|
||||
property string defaultColor: "#4360DF"
|
||||
property bool isValid: true
|
||||
|
||||
id: colorPicker
|
||||
label: qsTr("Community color")
|
||||
placeholderText: qsTr("Pick a color")
|
||||
anchors.top: addImageButton.bottom
|
||||
anchors.top: imageValidation.bottom
|
||||
anchors.topMargin: Style.current.smallPadding
|
||||
validationError: popup.colorValidationError
|
||||
textField.text: defaultColor
|
||||
textField.onReleased: colorDialog.open()
|
||||
|
||||
onTextChanged: validate()
|
||||
|
||||
function resetValidation() {
|
||||
isValid = true
|
||||
validationError = ""
|
||||
}
|
||||
|
||||
function validate() {
|
||||
validationError = ""
|
||||
if (text === "") {
|
||||
validationError = qsTr("Please enter a color")
|
||||
} else if (!Utils.isHexColor(colorPicker.text)) {
|
||||
validationError = qsTr("Must be an hexadecimal color (eg: #4360DF)")
|
||||
}
|
||||
isValid = validationError === ""
|
||||
}
|
||||
|
||||
StatusIconButton {
|
||||
icon.name: "caret"
|
||||
iconRotation: -90
|
||||
|
@ -368,6 +429,7 @@ ModalPopup {
|
|||
}
|
||||
|
||||
footer: StatusButton {
|
||||
enabled: popup.isValid
|
||||
text: isEdit ?
|
||||
//% "Edit"
|
||||
qsTrId("edit") :
|
||||
|
@ -389,11 +451,11 @@ ModalPopup {
|
|||
membershipRequirementSettingPopup.checkedMembership,
|
||||
ensOnlySwitch.switchChecked,
|
||||
colorPicker.text,
|
||||
popup.selectedImage,
|
||||
imageDimensions.aX,
|
||||
imageDimensions.aY,
|
||||
imageDimensions.bX,
|
||||
imageDimensions.bY)
|
||||
addImageButton.selectedImage,
|
||||
imageCropperModal.aX,
|
||||
imageCropperModal.aY,
|
||||
imageCropperModal.bX,
|
||||
imageCropperModal.bY)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
|
|
|
@ -3,7 +3,8 @@ import "."
|
|||
import "../imports"
|
||||
|
||||
Item {
|
||||
property var image
|
||||
id: root
|
||||
property Image image
|
||||
property alias selectorRectangle: selectorRectangle
|
||||
property string ratio: ""
|
||||
property var splitRatio: !!ratio ? ratio.split(":") : null
|
||||
|
@ -11,6 +12,7 @@ Item {
|
|||
property int heightRatio: !!ratio ? parseInt(splitRatio[1]) : -1
|
||||
property bool settingCorners: false
|
||||
property int draggedCorner: 0
|
||||
property bool ready: false
|
||||
|
||||
readonly property int topLeft: 0
|
||||
readonly property int topRight: 1
|
||||
|
@ -96,6 +98,7 @@ Item {
|
|||
if (image.status === Image.Ready) {
|
||||
selectorRectangle.initialSetup()
|
||||
selectorRectangle.visible = true
|
||||
root.ready = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,11 @@ import "./status"
|
|||
|
||||
ModalPopup {
|
||||
property string selectedImage
|
||||
property string ratio
|
||||
property string ratio: "1:1"
|
||||
property int aX: 0
|
||||
property int aY: 0
|
||||
property int bX: 0
|
||||
property int bY: 0
|
||||
signal cropFinished(aX: int, aY: int, bX: int, bY: int)
|
||||
|
||||
id: cropImageModal
|
||||
|
@ -29,6 +33,28 @@ ModalPopup {
|
|||
y: image.y
|
||||
image: image
|
||||
ratio: cropImageModal.ratio
|
||||
onReadyChanged: {
|
||||
if (ready) {
|
||||
// cropImageModal.calculateCrop()
|
||||
cropImageModal.aX = Qt.binding(function() {
|
||||
const aXPercent = imageCropper.selectorRectangle.x / image.width
|
||||
return Math.round(aXPercent * image.sourceSize.width)
|
||||
})
|
||||
cropImageModal.aY = Qt.binding(function() {
|
||||
const aYPercent = imageCropper.selectorRectangle.y / image.height
|
||||
return Math.round(aYPercent * image.sourceSize.height)
|
||||
})
|
||||
cropImageModal.bX = Qt.binding(function() {
|
||||
const bXPercent = (imageCropper.selectorRectangle.x + imageCropper.selectorRectangle.width) / image.width
|
||||
return Math.round(bXPercent * image.sourceSize.width)
|
||||
})
|
||||
cropImageModal.bY = Qt.binding(function() {
|
||||
const bYPercent = (imageCropper.selectorRectangle.y + imageCropper.selectorRectangle.height) / image.height
|
||||
return Math.round(bYPercent * image.sourceSize.height)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
footer: StatusButton {
|
||||
|
|
Loading…
Reference in New Issue