add almost all validations from backend config

This commit is contained in:
Jake Potrebic 2021-03-19 17:00:38 -07:00
parent f3802c7793
commit 7d9e0c128a
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
11 changed files with 132 additions and 38 deletions

View File

@ -1,6 +1,7 @@
<template>
<div class="markdown-editor">
<div v-show="isEditing && !preview" class="ml-4">
<!-- TODO validations for pageContent min/max length from validations state -->
<v-textarea v-model="rawEdited" outlined :rows="rawEdited.split(/\r\n|\r|\n/g).length + 3" :rules="rules" />
</div>
<Markdown v-show="!isEditing" :raw="raw" class="ml-4" />

View File

@ -67,8 +67,9 @@
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator';
import { Component, State, Vue } from 'nuxt-property-decorator';
import Dropdown, { Control } from '~/components/layouts/Dropdown.vue';
import { RootState } from '~/store';
@Component({
components: {
@ -128,11 +129,14 @@ export default class Header extends Vue {
icon: 'mdi-book',
title: this.$t('nav.new.project'),
});
controls.push({
link: '/organizations/new',
icon: 'mdi-account-group',
title: this.$t('nav.new.organization'),
});
// TODO get user orgs count
if (0 - 1 < this.validations.maxOrgCount) {
controls.push({
link: '/organizations/new',
icon: 'mdi-account-group',
title: this.$t('nav.new.organization'),
});
}
return controls;
}
@ -210,6 +214,9 @@ export default class Header extends Vue {
});
return controls;
}
@State((state: RootState) => state.validations)
validations!: RootState['validations'];
}
</script>

View File

@ -7,7 +7,15 @@
<v-card-title>{{ $t('channel.new.title') }}</v-card-title>
<v-card-text>
<v-form v-model="validForm">
<v-text-field v-model.trim="form.name" :label="$t('channel.new.name')" :rules="[$util.$vc.require($t('channel.new.name'))]" />
<v-text-field
v-model.trim="form.name"
:label="$t('channel.new.name')"
:rules="[
$util.$vc.require($t('channel.new.name')),
$util.$vc.regex($t('channel.new.name'), validations.project.channels.regex),
$util.$vc.maxLength(validations.project.channels.max),
]"
/>
<v-card-subtitle class="pa-0 text-center">{{ $t('channel.new.color') }}</v-card-subtitle>
<v-item-group v-model="form.color" mandatory>
<v-container>
@ -44,9 +52,10 @@
</template>
<script lang="ts">
import { Component } from 'nuxt-property-decorator';
import { Component, State } from 'nuxt-property-decorator';
import { Color, ProjectChannel } from 'hangar-internal';
import { HangarFormModal } from '../mixins';
import { RootState } from '~/store';
@Component
export default class NewChannelModal extends HangarFormModal {
@ -82,9 +91,13 @@ export default class NewChannelModal extends HangarFormModal {
}
createChannel() {
// TODO check channel name against existing channels
this.$emit('create', this.form);
this.dialog = false;
}
@State((state: RootState) => state.validations)
validations!: RootState['validations'];
}
</script>

View File

@ -1,14 +1,35 @@
<template>
<v-dialog v-model="dialog" max-width="500" persistent>
<template #activator="{ on }">
<v-btn icon class="primary" :class="activatorClass" v-on="on"><v-icon>mdi-plus</v-icon></v-btn>
<v-btn v-if="pageRoots.length < validations.project.maxPageCount" icon class="primary" :class="activatorClass" v-on="on">
<v-icon>mdi-plus</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title>{{ $t('page.new.title') }}</v-card-title>
<v-card-text>
<v-form ref="modalForm" v-model="validForm">
<v-text-field v-model.trim="form.name" filled :rules="[$util.$vc.require('Page name')]" :label="$t('page.new.name')" />
<v-select v-model="form.parent" :items="pageRoots" filled clearable :label="$t('page.new.parent')" item-text="name" item-value="id" />
<v-text-field
v-model.trim="form.name"
filled
:rules="[
$util.$vc.require($t('page.new.name')),
$util.$vc.regex($t('page.new.name'), validations.project.pageName.regex),
$util.$vc.maxLength(validations.project.pageName.max),
$util.$vc.minLength(validations.project.pageName.min),
]"
:label="$t('page.new.name')"
/>
<v-select
v-model="form.parent"
:items="pageRoots"
filled
clearable
hide-details
:label="$t('page.new.parent')"
item-text="name"
item-value="id"
/>
</v-form>
</v-card-text>
<v-card-actions class="justify-end">
@ -20,10 +41,11 @@
</template>
<script lang="ts">
import { Component, Prop } from 'nuxt-property-decorator';
import { Component, Prop, State } from 'nuxt-property-decorator';
import { PropType } from 'vue';
import { HangarProjectPage } from 'hangar-internal';
import { HangarFormModal } from '~/components/mixins';
import { RootState } from '~/store';
@Component
export default class NewPageModal extends HangarFormModal {
@ -42,6 +64,7 @@ export default class NewPageModal extends HangarFormModal {
return this.flatDeep(this.pages);
}
// TODO these should be sorted into somewhat of an order
flatDeep(pages: HangarProjectPage[]): HangarProjectPage[] {
let ps: HangarProjectPage[] = [];
for (const page of pages) {
@ -70,5 +93,8 @@ export default class NewPageModal extends HangarFormModal {
this.loading = false;
});
}
@State((state: RootState) => state.validations)
validations!: RootState['validations'];
}
</script>

View File

@ -527,7 +527,9 @@ const msgs: LocaleMessageObject = {
},
validation: {
required: '{0} is required',
maxLength: 'Max length is {0}',
maxLength: 'Maximum length is {0}',
minLength: 'Minimum length is {0}',
invalidFormat: '{0} is invalid',
},
};

View File

@ -115,12 +115,11 @@ import Tag from '~/components/Tag.vue';
import DonationModal from '~/components/donation/DonationModal.vue';
import MemberList from '~/components/MemberList.vue';
import Markdown from '~/components/Markdown.vue';
import NewPageModal from '~/components/modals/pages/NewPageModal.vue';
import { DocPageMixin } from '~/components/mixins';
import ProjectPageList from '~/components/projects/ProjectPageList.vue';
@Component({
components: { ProjectPageList, NewPageModal, Markdown, MemberList, DonationModal, MarkdownEditor, Tag },
components: { ProjectPageList, Markdown, MemberList, DonationModal, MarkdownEditor, Tag },
})
export default class DocsPage extends DocPageMixin {
async asyncData({ $api, params, $util }: Context) {

View File

@ -40,6 +40,9 @@
dense
hide-details
filled
clearable
:counter="validations.project.keywords.max"
:rules="[$util.$vc.maxLength(validations.project.keywords.max)]"
:delimiters="[' ', ',', '.']"
prepend-inner-icon="mdi-file-word-box"
/>
@ -56,7 +59,7 @@
filled
prepend-inner-icon="mdi-home-search"
:label="$t('project.new.step3.homepage')"
:rules="[$util.$vc.url(false)]"
:rules="[$util.$vc.url]"
/>
</div>
<v-divider />
@ -71,7 +74,7 @@
filled
prepend-inner-icon="mdi-bug"
:label="$t('project.new.step3.issues')"
:rules="[$util.$vc.url(false)]"
:rules="[$util.$vc.url]"
/>
</div>
<v-divider />
@ -86,7 +89,7 @@
filled
prepend-inner-icon="mdi-source-branch"
:label="$t('project.new.step3.source')"
:rules="[$util.$vc.url(false)]"
:rules="[$util.$vc.url]"
/>
</div>
<v-divider />
@ -101,7 +104,7 @@
filled
prepend-inner-icon="mdi-face-agent"
:label="$t('project.new.step3.support')"
:rules="[$util.$vc.url(false)]"
:rules="[$util.$vc.url]"
/>
</div>
<v-divider />
@ -137,7 +140,7 @@
dense
clearable
filled
:rules="[$util.$vc.url(false)]"
:rules="[$util.$vc.url]"
:label="$t('project.settings.licenceUrl')"
/>
</v-col>

View File

@ -21,6 +21,7 @@
<v-btn color="warning" block @click="reset">{{ $t('general.reset') }}</v-btn>
</v-col>
<v-col :md="isFile ? 4 : 6" sm="6" cols="12">
<!-- TODO validate version string against existing versions. complex because they only have to be unique per-platform -->
<v-text-field
v-model="pendingVersion.versionString"
:hide-details="isFile"
@ -28,7 +29,10 @@
:disabled="isFile"
:autofocus="!isFile"
filled
:rules="[$util.$vc.require('Version string')]"
:rules="[
$util.$vc.require($t('version.new.form.versionString')),
$util.$vc.regex($t('version.new.form.versionString'), validations.version.regex),
]"
/>
</v-col>
<v-col v-if="isFile" md="4" sm="6" cols="12">
@ -48,7 +52,7 @@
v-model="pendingVersion.externalUrl"
:label="$t('version.new.form.externalUrl')"
filled
:rules="[$util.$vc.require('External URL')]"
:rules="[$util.$vc.require($t('version.new.form.externalUrl'))]"
/>
</v-col>
</v-row>
@ -72,7 +76,7 @@
<v-col class="flex-grow-0 pl-0 pr-4" align-self="center">
<NewChannelModal @create="addChannel">
<template #activator="{ on, attrs }">
<v-btn color="info" v-bind="attrs" v-on="on">
<v-btn v-if="channels.length < validations.project.maxChannelCount" color="info" v-bind="attrs" v-on="on">
{{ $t('version.new.form.addChannel') }}
<v-icon right>mdi-plus</v-icon>
</v-btn>
@ -167,7 +171,7 @@
</template>
<script lang="ts">
import { Component } from 'nuxt-property-decorator';
import { Component, State } from 'nuxt-property-decorator';
import remove from 'lodash-es/remove';
import { IPlatform, PendingVersion, ProjectChannel } from 'hangar-internal';
import { ProjectNamespace } from 'hangar-api';
@ -337,6 +341,9 @@ export default class ProjectVersionsNewPage extends HangarProjectMixin {
this.file = null;
this.pendingVersion = null;
}
@State((state: RootState) => state.validations)
validations!: RootState['validations'];
}
</script>
<style lang="scss" scoped>

View File

@ -48,7 +48,7 @@
v-model.trim="taglineForm"
counter="100"
:label="$t('author.taglineLabel')"
:rules="[$util.$vc.require($t('author.taglineLabel')), $util.$vc.maxLength(100)]"
:rules="[$util.$vc.require($t('author.taglineLabel')), $util.$vc.maxLength(validations.userTagline.max)]"
/>
<template #other-btns>
<v-btn color="info" text :loading="loading.resetTagline" :disabled="!user.tagline" @click.stop="resetTagline">{{
@ -72,11 +72,12 @@
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator';
import { Component, State, Vue } from 'nuxt-property-decorator';
import { HangarUser } from 'hangar-internal';
import { Context } from '@nuxt/types';
import UserAvatar from '../components/UserAvatar.vue';
import HangarModal from '~/components/modals/HangarModal.vue';
import { RootState } from '~/store';
interface Button {
icon: string;
@ -150,6 +151,9 @@ export default class UserParentPage extends Vue {
if (typeof user === 'undefined') return;
return { user };
}
@State((state: RootState) => state.validations)
validations!: RootState['validations'];
}
</script>

View File

@ -44,7 +44,7 @@
item-text="name"
item-value="userId"
:label="$t('project.new.step2.userselect')"
:rules="[$util.$vc.require('Project owner')]"
:rules="[$util.$vc.require($t('project.new.step2.userselect'))]"
:append-icon="createAsIcon"
/>
</v-col>
@ -57,7 +57,11 @@
filled
:error-messages="nameErrors"
:label="$t('project.new.step2.projectname')"
:rules="[$util.$vc.require('Name')]"
:rules="[
$util.$vc.require($t('project.new.step2.projectname')),
$util.$vc.regex($t('project.new.step2.projectname'), validations.project.name.regex),
$util.$vc.maxLength(validations.project.name.max),
]"
append-icon="mdi-form-textbox"
/>
</v-col>
@ -68,7 +72,7 @@
filled
clearable
:label="$t('project.new.step2.projectsummary')"
:rules="[$util.$vc.require('Description')]"
:rules="[$util.$vc.require($t('project.new.step2.projectsummary')), $util.$vc.maxLength(validations.project.desc.max)]"
append-icon="mdi-card-text"
/>
</v-col>
@ -82,7 +86,7 @@
:label="$t('project.new.step2.projectcategory')"
item-text="title"
item-value="apiName"
:rules="[$util.$vc.require('Category')]"
:rules="[$util.$vc.require($t('project.new.step2.projectcategory'))]"
/>
</v-col>
</v-row>
@ -109,6 +113,7 @@
hide-details
filled
:label="$t('project.new.step3.homepage')"
:rules="[$util.$vc.url]"
append-icon="mdi-home-search"
/>
</v-col>
@ -119,6 +124,7 @@
hide-details
filled
:label="$t('project.new.step3.issues')"
:rules="[$util.$vc.url]"
append-icon="mdi-bug"
/>
</v-col>
@ -129,6 +135,7 @@
hide-details
filled
:label="$t('project.new.step3.source')"
:rules="[$util.$vc.url]"
append-icon="mdi-source-branch"
/>
</v-col>
@ -139,6 +146,7 @@
hide-details
filled
:label="$t('project.new.step3.support')"
:rules="[$util.$vc.url]"
append-icon="mdi-face-agent"
/>
</v-col>
@ -164,7 +172,14 @@
<v-text-field v-model.trim="form.settings.license.name" dense hide-details filled :label="$t('project.new.step3.customName')" />
</v-col>
<v-col cols="12" :md="isCustomLicense ? 12 : 6">
<v-text-field v-model.trim="form.settings.license.url" dense hide-details filled :label="$t('project.new.step3.url')" />
<v-text-field
v-model.trim="form.settings.license.url"
dense
hide-details
filled
:label="$t('project.new.step3.url')"
:rules="[$util.$vc.url]"
/>
</v-col>
</v-row>
<div class="text-h6 pt-5">
@ -182,6 +197,7 @@
dense
hide-details
filled
clearable
:delimiters="[' ', ',', '.']"
:label="$t('project.new.step3.keywords')"
append-icon="mdi-file-word-box"
@ -228,7 +244,7 @@
</template>
<script lang="ts">
import { Component, Vue, Watch } from 'nuxt-property-decorator';
import { Component, State, Vue, Watch } from 'nuxt-property-decorator';
import { Context } from '@nuxt/types';
import { ProjectOwner, ProjectSettingsForm } from 'hangar-internal';
import { AxiosError } from 'axios';
@ -293,6 +309,9 @@ export default class NewPage extends Vue {
return ['MIT', 'Apache 2.0', 'GPL', 'LGPL', '(custom)'];
}
@State((state: RootState) => state.validations)
validations!: RootState['validations'];
async asyncData({ $api }: Context) {
return {
projectOwners: await $api.requestInternal<ProjectOwner[]>('projects/possibleOwners'),

View File

@ -219,13 +219,26 @@ const createUtil = ({ store, error, app: { i18n } }: Context) => {
$vc = {
require: (name: TranslateResult = 'Field') => (v: string) => !!v || i18n.t('validation.required', [name]),
maxLength: (maxLength: number) => (v: string) => (!!v && v.length <= maxLength) || i18n.t('validation.maxLength', [maxLength]),
maxLength: (maxLength: number) => (v: string | any[]) => {
return (
((v === null || typeof v === 'string') && !v) ||
(Array.isArray(v) && v.length === 0) ||
v.length <= maxLength ||
i18n.t('validation.maxLength', [maxLength])
);
},
minLength: (minLength: number) => (v: string | any[]) => {
return (
((v === null || typeof v === 'string') && !v) ||
(Array.isArray(v) && v.length === 0) ||
v.length >= minLength ||
i18n.t('validation.minLength', [minLength])
);
},
requireNonEmptyArray: (name: TranslateResult = 'Field') => (v: any[]) => v.length > 0 || i18n.t('validation.required', [name]),
url: (require: boolean) => (v: string) => {
if (!require && !v) {
return true;
}
return new RegExp((store.state as RootState).validations.urlRegex).test(v) || i18n.t('general.error.invalidUrl');
url: (v: string) => !v || new RegExp((store.state as RootState).validations.urlRegex).test(v) || i18n.t('general.error.invalidUrl'),
regex: (name: TranslateResult = 'Field', regexp: string) => (v: string) => {
return !v || new RegExp(regexp).test(v) || i18n.t('validation.invalidFormat', [name]);
},
};