frontend fields refactoring
This commit is contained in:
parent
3e53958a35
commit
ae2cdb4f14
@ -1,20 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import Field from "@/components/Field.vue";
|
||||
import {type Field as FieldSpec} from "@/urlmaker/specs";
|
||||
import {validateOr, validatePreset, validateUrl} from "@/urlmaker/validators.ts";
|
||||
import TextField from "@/components/TextField.vue";
|
||||
import Btn from "@/components/Btn.vue";
|
||||
import {onMounted, onUnmounted, ref, watch} from "vue";
|
||||
import Modal from "@/components/Modal.vue";
|
||||
|
||||
const field: FieldSpec = {
|
||||
name: '',
|
||||
input_type: 'url',
|
||||
label: 'URL of feed or preset',
|
||||
default: '',
|
||||
required: true,
|
||||
validate: validateOr(validateUrl, validatePreset),
|
||||
}
|
||||
import {validatePreset, validateUrl} from "@/urlmaker/validators.ts";
|
||||
|
||||
const visible = defineModel('visible', {
|
||||
type: Boolean,
|
||||
@ -33,11 +23,10 @@ watch(visible, () => {
|
||||
|
||||
const valid = ref(false);
|
||||
watch(url, (value) => {
|
||||
valid.value = field.validate(value).ok;
|
||||
valid.value = validateUrl(value) || validatePreset(value);
|
||||
});
|
||||
|
||||
const accept = () => {
|
||||
valid.value = field.validate(url.value).ok;
|
||||
if (valid.value) {
|
||||
emit('update:modelValue', url.value);
|
||||
emit('update:visible', false);
|
||||
@ -59,7 +48,13 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<Modal v-model="visible">
|
||||
<Field :field="field" v-model="url" :focused="true"/>
|
||||
<TextField
|
||||
name="url"
|
||||
input_type="url"
|
||||
label="URL of feed or preset"
|
||||
v-model="url"
|
||||
:focused="true"
|
||||
/>
|
||||
<Btn :active="valid" @click="accept">Edit</Btn>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
46
frontend/wizard-vue/src/components/TextField.vue
Normal file
46
frontend/wizard-vue/src/components/TextField.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import {getCurrentInstance, onMounted, useTemplateRef} from "vue";
|
||||
|
||||
const {name, label, input_type, focused} = defineProps<{
|
||||
name: string
|
||||
label: string,
|
||||
input_type: 'text' | 'url',
|
||||
focused?: boolean,
|
||||
}>();
|
||||
const id = 'field' + getCurrentInstance()?.uid;
|
||||
const model = defineModel();
|
||||
|
||||
const inputRef = useTemplateRef('field');
|
||||
onMounted(() => {
|
||||
if(focused) inputRef.value?.focus();
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field">
|
||||
<div class="label"><label :for="id">{{ label }}</label></div>
|
||||
<div class="input">
|
||||
<input :type="input_type" :name="name" :id="id" v-model="model" ref="field"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
div.field {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
div.label {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
div.input {
|
||||
margin: 2px 0 0 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -19,8 +19,8 @@ watch(existingLink, async (value) => {
|
||||
if(!value) return;
|
||||
existingLink.value = "";
|
||||
try {
|
||||
if(validateUrl(value).ok) store.updateSpecs(await decodeUrl(value));
|
||||
else if (validatePreset(value).ok) store.updateSpecs(await decodePreset(value));
|
||||
if(validateUrl(value)) store.updateSpecs(await decodeUrl(value));
|
||||
else if (validatePreset(value)) store.updateSpecs(await decodePreset(value));
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
alert(`Decoding error: ${e}`);
|
||||
@ -60,7 +60,6 @@ function screenshot() {
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<SpecsForm class="specs-form"></SpecsForm>
|
||||
<!-- <Btn :active="store.formValid" @click="generateLink">Generate link</Btn>-->
|
||||
<Btn :active="store.formValid" @click="screenshot">Screenshot</Btn>
|
||||
<Btn @click="editModalVisible = true">Edit existing task / import preset</Btn>
|
||||
<Btn @click="store.reset">Reset Form</Btn>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {emptySpecs, type Field, type FieldNames, fields, type Specs} from "@/urlmaker/specs.ts";
|
||||
import {emptySpecs, type SpecField, fields, type Specs} from "@/urlmaker/specs.ts";
|
||||
import {computed, reactive} from "vue";
|
||||
import {debounce} from "es-toolkit";
|
||||
|
||||
@ -14,7 +14,7 @@ export const useWizardStore = defineStore('wizard', () => {
|
||||
|
||||
const formValid = computed(() => {
|
||||
return fields.every(field => (
|
||||
specs[field.name].length === 0 && !(field as Field).required || field.validate(specs[field.name]).ok
|
||||
!specs[field.name] && !(field as SpecField).required || field.validate(specs[field.name]!)
|
||||
));
|
||||
});
|
||||
|
||||
@ -22,7 +22,7 @@ export const useWizardStore = defineStore('wizard', () => {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(specs));
|
||||
}, 100);
|
||||
|
||||
function updateSpec(fieldName: FieldNames, newValue: string) {
|
||||
function updateSpec(fieldName: keyof Specs, newValue: string) {
|
||||
specs[fieldName] = newValue;
|
||||
updateLocalStorage();
|
||||
}
|
||||
|
||||
@ -4,20 +4,34 @@ import {
|
||||
validateUrl,
|
||||
type validator
|
||||
} from "@/urlmaker/validators.ts";
|
||||
import {rssalchemy} from "@/urlmaker/proto/specs.ts";
|
||||
|
||||
export interface Field {
|
||||
name: string
|
||||
input_type: string
|
||||
label: string
|
||||
default: string
|
||||
validate: validator
|
||||
required?: boolean
|
||||
export type SpecKey = ReturnType<rssalchemy.Specs['toObject']>;
|
||||
export type SpecValue = string | number;
|
||||
export type Specs = {[k in keyof SpecKey]: SpecValue};
|
||||
|
||||
export enum InputType {
|
||||
Url = 'url',
|
||||
Text = 'text',
|
||||
Radio = 'radio'
|
||||
}
|
||||
|
||||
export const fields = [
|
||||
export interface SpecField {
|
||||
name: keyof Specs
|
||||
input_type: InputType
|
||||
enum_values?: {[k: number]: string}
|
||||
label: string
|
||||
default: SpecValue
|
||||
validate: validator
|
||||
required?: boolean
|
||||
group?: string
|
||||
show_if?: (specs: Specs) => boolean
|
||||
}
|
||||
|
||||
export const fields: SpecField[] = [
|
||||
{
|
||||
name: 'url',
|
||||
input_type: 'url',
|
||||
input_type: InputType.Url,
|
||||
label: 'URL of page for converting',
|
||||
default: '',
|
||||
validate: validateUrl,
|
||||
@ -25,72 +39,71 @@ export const fields = [
|
||||
},
|
||||
{
|
||||
name: 'selector_post',
|
||||
input_type: 'text',
|
||||
input_type: InputType.Text,
|
||||
label: 'CSS Selector for post',
|
||||
default: '',
|
||||
validate: validateSelector,
|
||||
},
|
||||
{
|
||||
name: 'selector_title',
|
||||
input_type: 'text',
|
||||
input_type: InputType.Text,
|
||||
label: 'CSS Selector for title',
|
||||
default: '',
|
||||
validate: validateSelector,
|
||||
},
|
||||
{
|
||||
name: 'selector_link',
|
||||
input_type: 'text',
|
||||
input_type: InputType.Text,
|
||||
label: 'CSS Selector for link',
|
||||
default: '',
|
||||
validate: validateSelector,
|
||||
},
|
||||
{
|
||||
name: 'selector_description',
|
||||
input_type: 'text',
|
||||
input_type: InputType.Text,
|
||||
label: 'CSS Selector for description',
|
||||
default: '',
|
||||
validate: validateSelector,
|
||||
},
|
||||
{
|
||||
name: 'selector_author',
|
||||
input_type: 'text',
|
||||
input_type: InputType.Text,
|
||||
label: 'CSS Selector for author',
|
||||
default: '',
|
||||
validate: validateSelector,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'selector_created',
|
||||
input_type: 'text',
|
||||
input_type: InputType.Text,
|
||||
label: 'CSS Selector for created date',
|
||||
default: '',
|
||||
validate: validateSelector,
|
||||
group: 'created',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'selector_content',
|
||||
input_type: 'text',
|
||||
input_type: InputType.Text,
|
||||
label: 'CSS Selector for content',
|
||||
default: '',
|
||||
validate: validateSelector,
|
||||
},
|
||||
{
|
||||
name: 'selector_enclosure',
|
||||
input_type: 'text',
|
||||
input_type: InputType.Text,
|
||||
label: 'CSS Selector for enclosure (e.g. image url)',
|
||||
default: '',
|
||||
validate: validateSelector,
|
||||
},
|
||||
{
|
||||
name: 'cache_lifetime',
|
||||
input_type: 'text',
|
||||
input_type: InputType.Text,
|
||||
label: 'Cache lifetime (format examples: 10s, 1m, 2h)',
|
||||
default: '10m',
|
||||
validate: validateDuration,
|
||||
},
|
||||
] as const satisfies Field[];
|
||||
|
||||
export type FieldNames = (typeof fields)[number]['name'];
|
||||
|
||||
export type Specs = {[k in FieldNames]: string};
|
||||
];
|
||||
|
||||
export const emptySpecs = fields.reduce((o, f) => {
|
||||
o[f.name] = f.default;
|
||||
|
||||
@ -1,54 +1,31 @@
|
||||
import {presetPrefix} from "@/urlmaker/index.ts";
|
||||
import type {SpecValue} from "@/urlmaker/specs.ts";
|
||||
|
||||
type validResult = { ok: boolean, error?: string };
|
||||
export type validator = (v: string) => validResult
|
||||
export type validator = (v: SpecValue) => boolean;
|
||||
|
||||
export function validateUrl(s: string): validResult {
|
||||
export function validateUrl(s: SpecValue): boolean {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(s);
|
||||
return {
|
||||
ok: url.protocol === "http:" || url.protocol === "https:",
|
||||
error: 'Invalid URL protocol',
|
||||
};
|
||||
url = new URL(s as string);
|
||||
return url.protocol === "http:" || url.protocol === "https:"
|
||||
} catch {
|
||||
return {ok: false, error: 'Invalid URL'};
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function validatePreset(s: string): validResult {
|
||||
if(!s.startsWith(presetPrefix)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'Not a preset'
|
||||
}
|
||||
}
|
||||
return {ok: true}
|
||||
export function validatePreset(s: SpecValue): boolean {
|
||||
return (s as string).startsWith(presetPrefix);
|
||||
}
|
||||
|
||||
export function validateOr(...validators: validator[]): validator {
|
||||
return function(s: string): validResult {
|
||||
return validators.reduce<validResult>((res, v) => {
|
||||
let r = v(s);
|
||||
if(r.ok) res.ok = true;
|
||||
else res.error += r.error + '; ';
|
||||
return res;
|
||||
}, {ok: false, error: ''});
|
||||
}
|
||||
}
|
||||
|
||||
export function validateSelector(s: string): validResult {
|
||||
export function validateSelector(s: SpecValue): boolean {
|
||||
try {
|
||||
document.createDocumentFragment().querySelector(s);
|
||||
return {ok: true}
|
||||
document.createDocumentFragment().querySelector(s as string);
|
||||
return true;
|
||||
} catch {
|
||||
return {ok: false, error: 'Invalid selector'};
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function validateDuration(s: string): validResult {
|
||||
return {
|
||||
ok: /^\d+[smh]$/.test(s),
|
||||
error: 'Duration must be number and unit (s/m/h), example: 5s = 5 seconds'
|
||||
}
|
||||
export function validateDuration(s: SpecValue): boolean {
|
||||
return /^\d+[smh]$/.test(s as string);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user