You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							424 lines
						
					
					
						
							9.9 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							424 lines
						
					
					
						
							9.9 KiB
						
					
					
				| package components | |
| 
 | |
| import "fmt" | |
| 
 | |
| // FormFieldData represents common form field data | |
| type FormFieldData struct { | |
|     Name        string | |
|     Label       string | |
|     Description string | |
|     Required    bool | |
| } | |
| 
 | |
| // TextFieldData represents text input field data | |
| type TextFieldData struct { | |
|     FormFieldData | |
|     Value       string | |
|     Placeholder string | |
| } | |
| 
 | |
| // NumberFieldData represents number input field data | |
| type NumberFieldData struct { | |
|     FormFieldData | |
|     Value float64 | |
|     Step  string | |
|     Min   *float64 | |
|     Max   *float64 | |
| } | |
| 
 | |
| // CheckboxFieldData represents checkbox field data | |
| type CheckboxFieldData struct { | |
|     FormFieldData | |
|     Checked bool | |
| } | |
| 
 | |
| // SelectFieldData represents select field data | |
| type SelectFieldData struct { | |
|     FormFieldData | |
|     Value   string | |
|     Options []SelectOption | |
| } | |
| 
 | |
| type SelectOption struct { | |
|     Value string | |
|     Label string | |
| } | |
| 
 | |
| // DurationFieldData represents duration input field data | |
| type DurationFieldData struct { | |
|     FormFieldData | |
|     Value       string | |
|     Placeholder string | |
| } | |
| 
 | |
| // DurationInputFieldData represents duration input with number + unit dropdown | |
| type DurationInputFieldData struct { | |
|     FormFieldData | |
|     Seconds int // The duration value in seconds | |
| } | |
| 
 | |
| // TextField renders a Bootstrap text input field | |
| templ TextField(data TextFieldData) { | |
|     <div class="mb-3"> | |
|         <label for={ data.Name } class="form-label"> | |
|             { data.Label } | |
|             if data.Required { | |
|                 <span class="text-danger">*</span> | |
|             } | |
|         </label> | |
|         <input  | |
|             type="text"  | |
|             class="form-control"  | |
|             id={ data.Name }  | |
|             name={ data.Name }  | |
|             value={ data.Value } | |
|             if data.Placeholder != "" { | |
|                 placeholder={ data.Placeholder } | |
|             } | |
|             if data.Required { | |
|                 required | |
|             } | |
|         /> | |
|         if data.Description != "" { | |
|             <div class="form-text text-muted">{ data.Description }</div> | |
|         } | |
|     </div> | |
| } | |
| 
 | |
| // NumberField renders a Bootstrap number input field | |
| templ NumberField(data NumberFieldData) { | |
|     <div class="mb-3"> | |
|         <label for={ data.Name } class="form-label"> | |
|             { data.Label } | |
|             if data.Required { | |
|                 <span class="text-danger">*</span> | |
|             } | |
|         </label> | |
|         <input  | |
|             type="number"  | |
|             class="form-control"  | |
|             id={ data.Name }  | |
|             name={ data.Name }  | |
|             value={ fmt.Sprintf("%.6g", data.Value) } | |
|             if data.Step != "" { | |
|                 step={ data.Step } | |
|             } else { | |
|                 step="any" | |
|             } | |
|             if data.Min != nil { | |
|                 min={ fmt.Sprintf("%.6g", *data.Min) } | |
|             } | |
|             if data.Max != nil { | |
|                 max={ fmt.Sprintf("%.6g", *data.Max) } | |
|             } | |
|             if data.Required { | |
|                 required | |
|             } | |
|         /> | |
|         if data.Description != "" { | |
|             <div class="form-text text-muted">{ data.Description }</div> | |
|         } | |
|     </div> | |
| } | |
| 
 | |
| // CheckboxField renders a Bootstrap checkbox field | |
| templ CheckboxField(data CheckboxFieldData) { | |
|     <div class="mb-3"> | |
|         <div class="form-check"> | |
|             <input  | |
|                 type="checkbox"  | |
|                 class="form-check-input"  | |
|                 id={ data.Name }  | |
|                 name={ data.Name } | |
|                 if data.Checked { | |
|                     checked | |
|                 } | |
|             /> | |
|             <label class="form-check-label" for={ data.Name }> | |
|                 { data.Label } | |
|             </label> | |
|         </div> | |
|         if data.Description != "" { | |
|             <div class="form-text text-muted">{ data.Description }</div> | |
|         } | |
|     </div> | |
| } | |
| 
 | |
| // SelectField renders a Bootstrap select field | |
| templ SelectField(data SelectFieldData) { | |
|     <div class="mb-3"> | |
|         <label for={ data.Name } class="form-label"> | |
|             { data.Label } | |
|             if data.Required { | |
|                 <span class="text-danger">*</span> | |
|             } | |
|         </label> | |
|         <select  | |
|             class="form-select"  | |
|             id={ data.Name }  | |
|             name={ data.Name } | |
|             if data.Required { | |
|                 required | |
|             } | |
|         > | |
|             for _, option := range data.Options { | |
|                 <option  | |
|                     value={ option.Value } | |
|                     if option.Value == data.Value { | |
|                         selected | |
|                     } | |
|                 > | |
|                     { option.Label } | |
|                 </option> | |
|             } | |
|         </select> | |
|         if data.Description != "" { | |
|             <div class="form-text text-muted">{ data.Description }</div> | |
|         } | |
|     </div> | |
| } | |
| 
 | |
| // DurationField renders a Bootstrap duration input field | |
| templ DurationField(data DurationFieldData) { | |
|     <div class="mb-3"> | |
|         <label for={ data.Name } class="form-label"> | |
|             { data.Label } | |
|             if data.Required { | |
|                 <span class="text-danger">*</span> | |
|             } | |
|         </label> | |
|         <input  | |
|             type="text"  | |
|             class="form-control"  | |
|             id={ data.Name }  | |
|             name={ data.Name }  | |
|             value={ data.Value } | |
|             if data.Placeholder != "" { | |
|                 placeholder={ data.Placeholder } | |
|             } else { | |
|                 placeholder="e.g., 30m, 2h, 24h" | |
|             } | |
|             if data.Required { | |
|                 required | |
|             } | |
|         /> | |
|         if data.Description != "" { | |
|             <div class="form-text text-muted">{ data.Description }</div> | |
|         } | |
|     </div> | |
| } | |
| 
 | |
| // DurationInputField renders a Bootstrap duration input with number + unit dropdown | |
| templ DurationInputField(data DurationInputFieldData) { | |
| 	<div class="mb-3"> | |
| 		<label for={ data.Name } class="form-label"> | |
| 			{ data.Label } | |
| 			if data.Required { | |
| 				<span class="text-danger">*</span> | |
| 			} | |
| 		</label> | |
| 		<div class="input-group"> | |
| 			<input  | |
| 				type="number"  | |
| 				class="form-control"  | |
| 				id={ data.Name }  | |
| 				name={ data.Name }  | |
| 				value={ fmt.Sprintf("%.0f", convertSecondsToValue(data.Seconds, convertSecondsToUnit(data.Seconds))) } | |
| 				step="1" | |
| 				min="1" | |
| 				if data.Required { | |
| 					required | |
| 				} | |
| 			/> | |
| 			<select  | |
| 				class="form-select"  | |
| 				id={ data.Name + "_unit" }  | |
| 				name={ data.Name + "_unit" } | |
| 				style="max-width: 120px;" | |
| 			> | |
| 				<option  | |
| 					value="minutes" | |
| 					if convertSecondsToUnit(data.Seconds) == "minutes" { | |
| 						selected | |
| 					} | |
| 				> | |
| 					Minutes | |
| 				</option> | |
| 				<option  | |
| 					value="hours" | |
| 					if convertSecondsToUnit(data.Seconds) == "hours" { | |
| 						selected | |
| 					} | |
| 				> | |
| 					Hours | |
| 				</option> | |
| 				<option  | |
| 					value="days" | |
| 					if convertSecondsToUnit(data.Seconds) == "days" { | |
| 						selected | |
| 					} | |
| 				> | |
| 					Days | |
| 				</option> | |
| 			</select> | |
| 		</div> | |
| 		if data.Description != "" { | |
| 			<div class="form-text text-muted">{ data.Description }</div> | |
| 		} | |
| 	</div> | |
| } | |
| 
 | |
| // Helper functions for duration conversion (used by DurationInputField) | |
| 
 | |
| // Typed conversion functions for protobuf int32 (most common) - EXPORTED | |
| func ConvertInt32SecondsToDisplayValue(seconds int32) float64 { | |
| 	return convertIntSecondsToDisplayValue(int(seconds)) | |
| } | |
| 
 | |
| func GetInt32DisplayUnit(seconds int32) string { | |
| 	return getIntDisplayUnit(int(seconds)) | |
| } | |
| 
 | |
| // Typed conversion functions for regular int | |
| func convertIntSecondsToDisplayValue(seconds int) float64 { | |
| 	if seconds == 0 { | |
| 		return 0 | |
| 	} | |
| 	 | |
| 	// Check if it's evenly divisible by days | |
| 	if seconds%(24*3600) == 0 { | |
| 		return float64(seconds / (24 * 3600)) | |
| 	} | |
| 	 | |
| 	// Check if it's evenly divisible by hours | |
| 	if seconds%3600 == 0 { | |
| 		return float64(seconds / 3600) | |
| 	} | |
| 	 | |
| 	// Default to minutes | |
| 	return float64(seconds / 60) | |
| } | |
| 
 | |
| func getIntDisplayUnit(seconds int) string { | |
| 	if seconds == 0 { | |
| 		return "minutes" | |
| 	} | |
| 	 | |
| 	// Check if it's evenly divisible by days | |
| 	if seconds%(24*3600) == 0 { | |
| 		return "days" | |
| 	} | |
| 	 | |
| 	// Check if it's evenly divisible by hours | |
| 	if seconds%3600 == 0 { | |
| 		return "hours" | |
| 	} | |
| 	 | |
| 	// Default to minutes | |
| 	return "minutes" | |
| } | |
| 
 | |
| func convertSecondsToUnit(seconds int) string { | |
| 	if seconds == 0 { | |
| 		return "minutes" | |
| 	} | |
| 	 | |
| 	// Try days first | |
| 	if seconds%(24*3600) == 0 && seconds >= 24*3600 { | |
| 		return "days" | |
| 	} | |
| 	 | |
| 	// Try hours | |
| 	if seconds%3600 == 0 && seconds >= 3600 { | |
| 		return "hours" | |
| 	} | |
| 	 | |
| 	// Default to minutes | |
| 	return "minutes" | |
| } | |
| 
 | |
| func convertSecondsToValue(seconds int, unit string) float64 { | |
| 	if seconds == 0 { | |
| 		return 0 | |
| 	} | |
| 	 | |
| 	switch unit { | |
| 	case "days": | |
| 		return float64(seconds / (24 * 3600)) | |
| 	case "hours": | |
| 		return float64(seconds / 3600) | |
| 	case "minutes": | |
| 		return float64(seconds / 60) | |
| 	default: | |
| 		return float64(seconds / 60) // Default to minutes | |
| 	} | |
| } | |
| 
 | |
| // IntervalFieldData represents interval input field data with separate value and unit | |
| type IntervalFieldData struct { | |
|     FormFieldData | |
|     Seconds int // The interval value in seconds | |
| } | |
| 
 | |
| // IntervalField renders a Bootstrap interval input with number + unit dropdown (like task config) | |
| templ IntervalField(data IntervalFieldData) { | |
| 	<div class="mb-3"> | |
| 		<label for={ data.Name } class="form-label"> | |
| 			{ data.Label } | |
| 			if data.Required { | |
| 				<span class="text-danger">*</span> | |
| 			} | |
| 		</label> | |
| 		<div class="input-group"> | |
| 			<input  | |
| 				type="number"  | |
| 				class="form-control"  | |
| 				id={ data.Name + "_value" }  | |
| 				name={ data.Name + "_value" }  | |
| 				value={ fmt.Sprintf("%.0f", convertSecondsToValue(data.Seconds, convertSecondsToUnit(data.Seconds))) } | |
| 				step="1" | |
| 				min="1" | |
| 				if data.Required { | |
| 					required | |
| 				} | |
| 			/> | |
| 			<select  | |
| 				class="form-select"  | |
| 				id={ data.Name + "_unit" }  | |
| 				name={ data.Name + "_unit" } | |
| 				style="max-width: 120px;" | |
| 				if data.Required { | |
| 					required | |
| 				} | |
| 			> | |
| 				<option  | |
| 					value="minutes" | |
| 					if convertSecondsToUnit(data.Seconds) == "minutes" { | |
| 						selected | |
| 					} | |
| 				> | |
| 					Minutes | |
| 				</option> | |
| 				<option  | |
| 					value="hours" | |
| 					if convertSecondsToUnit(data.Seconds) == "hours" { | |
| 						selected | |
| 					} | |
| 				> | |
| 					Hours | |
| 				</option> | |
| 				<option  | |
| 					value="days" | |
| 					if convertSecondsToUnit(data.Seconds) == "days" { | |
| 						selected | |
| 					} | |
| 				> | |
| 					Days | |
| 				</option> | |
| 			</select> | |
| 		</div> | |
| 		if data.Description != "" { | |
| 			<div class="form-text text-muted">{ data.Description }</div> | |
| 		} | |
| 	</div> | |
| }  |