Skip to content
Commits on Source (19)
......@@ -17,7 +17,7 @@
Procédure d'installation :
```
git clone https://gitlab.mim-libre.fr/alphabet/laboite
git clone https://gitlab.mim-libre.fr/alphabet/laboite.git
cd laboite
cp config/settings.development.json.sample config/settings.development.json
cd app
......@@ -72,7 +72,7 @@ http://localhost:3010
#### Via l'interface utilisateur **localhost:3000**
À partir de l'appliation `LaBoite` que vous accédez à partir du navigateur
À partir de l'application `LaBoite` que vous accédez à partir du navigateur
```
http://localhost:3000
......
......@@ -17,7 +17,7 @@
Install process :
```
git clone https://gitlab.mim-libre.fr/alphabet/laboite
git clone https://gitlab.mim-libre.fr/alphabet/laboite.git
cd laboite
cp config/settings.development.json.sample config/settings.development.json
cd app
......
......@@ -4,40 +4,16 @@ Copier `settings-development.json.sample` dans `settings-development.json` et me
## public:
| Key | Type | Default value | Description |
| :--------------------------------------- | -------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| appName | string | "Sondage" | Nom de l'application |
| appDescription | string | "" | Description de l'application qui sera affiché sous le titre |
| enableKeycloak | boolean | false | Si true, keycloak est activé |
| keycloakUrl | string | "" | Keycloak URL |
| keycloakRealm | string | "" | Keycloak Realm |
| laboiteBlogURL | string | "" | Laboite Blog URL |
| enableBBB | boolean | true | Si true, Big Blue Button est activé |
| BBBUrl | string | "" | Big Blue Button URL |
| minioSSL | boolean | false | Si true, minio est en SSL |
| minioPort | number | null | Minio port |
| minioEndPoint | string | "" | Minio End Point |
| minioBucket | string | "" | Minio Bucket |
| imageFilesTypes | [string] | ["svg", "png", "jpg", "gif", "jpeg"] | Extensions de fichiers autorisées pour les images |
| audioFilesTypes | [string] | ["wav", "mp3", "ogg"] | Extensions de fichiers autorisées pour les sons |
| videoFilesTypes | [string] | ["mp4", "webm", "avi", "wmv"] | Extensions de fichiers autorisées pour les videos |
| textFilesTypes | [string] | ["pdf", "odt", "txt", "docx"] | Extensions de fichiers autorisées pour les documents |
| otherFilesTypes | [string] | ["csv"] | Extensions de fichiers autorisées pour les autres fichiers |
| minioFileSize | number | 500000 | Taille de fichier maximale lors du téléchargement d’images de services dans l’espace d’administration |
| minioStorageFilesSize | number | 3000000 | Taille de fichier maximale lors du téléchargement d’images de services dans l’espace utilisateur |
| maxMinioDiskPerUser | number | 1000000 | Capacité maximale du disque par utilisateur |
| NotificationsExpireDays | object | {} | Nombre de jours pour conserver les notications par type (null ou 0 pour infini) |
| NotificationsExpireDays.setRole | number | null | Nombre de jours pour conserver les notications setRole (null ou 0 pour infini) |
| NotificationsExpireDays.unsetRole | number | null | Nombre de jours pour conserver les notications unsetRole (null ou 0 pour infini) |
| NotificationsExpireDays.request | number | null | Nombre de jours pour conserver les notications de requête (null ou 0 pour infini) |
| NotificationsExpireDays.group | number | null | Nombre de jours pour conserver les notications de groupe (null ou 0 pour infini) |
| NotificationsExpireDays.default | number | null | Nombre de jours pour ne pas conserver de notications de type (null ou 0 pour infini) |
| groupPlugins | object | {} | Plugins externes pour les groupes |
| PLUGINNAME | object | {} | Paramètres généraux du plugin de groupe, voir ci-dessous "nextcloud" et "rocketChat" pour des paramètres spécifiques |
| groupPlugins.PLUGINNAME.enable | boolean | false | Si true, le plugin de groupe est activé |
| groupPlugins.PLUGINNAME.URL | string | "" | Plugin groupe URL |
| groupPlugins.PLUGINNAME.groupURL | string | "" | [URL]/group/[GROUPSLUG]" "[URL]/apps/files/?dir=/[GROUPNAME] |
| groupPlugins.PLUGINNAME.enableChangeName | boolean | true | Si true, changer le nom du groupe pour ce plugin de groupe est possible |
| Key | Type | Default value | Description |
| :------------------ | ------- | ----------------------- | ------------------------------------------------------------------ |
| appName | string | "Sondage" | Nom de l'application |
| appDescription | string | "" | Description de l'application qui sera affiché sous le titre |
| enableKeycloak | boolean | false | Si true, keycloak est activé |
| keycloakUrl | string | "" | Keycloak URL |
| keycloakRealm | string | "" | Keycloak Realm |
| services | object | {} | Contient les url des services |
| services.sondageUrl | string | "http://localhost:3010" | L'url est nécessaire pour le remplissage des mails de confirmation |
| laboiteHost | string | "http://localhost:3000" | Url de l'application la Boite |
## keycloak:
......@@ -74,12 +50,6 @@ Copier `settings-development.json.sample` dans `settings-development.json` et me
## private:
| Key | Type | Default value | Description |
| --------------------- | -------- | ------------------------------------------ | ------------------------------------------------------------------- |
| loginExpirationInDays | number | 90 | Nombre de jours d’expiration du jeton de la session |
| fillWithFakeData | boolean | false | Si true, les fausses données sont générées au début |
| minioAccess | string | "" | Minio user |
| minioSecret | string | "" | Minio password |
| apiKeys | [string] | [""] | Clés d’accès API pour les services externes |
| BBBSecret | string | "" | Big Blue Button Secret |
| whiteDomains | [string] | ["^ac-[a-z-]_\\.fr", "^[a-z-]_\\.gouv.fr"] | Emails dans le white domain pour l’activation de compte utilisateur |
| Key | Type | Default value | Description |
| ------- | -------- | ------------- | ------------------------------------------ |
| apiKeys | [string] | [""] | API access keys pour les services externes |
......@@ -4,40 +4,16 @@ Copy `settings-development.json.sample` to `settings-development.json` and updat
## public:
| Key | Type | Default value | Description |
| :--------------------------------------- | -------- | ------------------------------------ | ------------------------------------------------------------------------------------------- |
| appName | string | "Sondage" | Application Name |
| appDescription | string | "" | Application description, it will be displayed under the title |
| enableKeycloak | boolean | false | If true, keycloak is enabled |
| keycloakUrl | string | "" | Keycloak URL |
| keycloakRealm | string | "" | Keycloak Realm |
| laboiteBlogURL | string | "" | Laboite Blog URL |
| enableBBB | boolean | true | If true, Big Blue Button is enabled |
| BBBUrl | string | "" | Big Blue Button URL |
| minioSSL | boolean | false | If true, minio is SSL |
| minioPort | number | null | Minio port |
| minioEndPoint | string | "" | Minio End Point |
| minioBucket | string | "" | Minio Bucket |
| imageFilesTypes | [string] | ["svg", "png", "jpg", "gif", "jpeg"] | Allowed file extensions for images |
| audioFilesTypes | [string] | ["wav", "mp3", "ogg"] | Allowed file extensions for sounds |
| videoFilesTypes | [string] | ["mp4", "webm", "avi", "wmv"] | Allowed file extensions for videos |
| textFilesTypes | [string] | ["pdf", "odt", "txt", "docx"] | Allowed file extensions for documents |
| otherFilesTypes | [string] | ["csv"] | Allowed file extensions for other files |
| minioFileSize | number | 500000 | Maximum file size when uploading services images in admin space |
| minioStorageFilesSize | number | 3000000 | Maximum file size when uploading media in user space |
| maxMinioDiskPerUser | number | 1000000 | Maximum disk capacity per user |
| NotificationsExpireDays | object | {} | Number of days to keep notications by type (null or 0 for infinite) |
| NotificationsExpireDays.setRole | number | null | Number of days to keep setRole notications (null or 0 for infinite) |
| NotificationsExpireDays.unsetRole | number | null | Number of days to keep unsetRole notications (null or 0 for infinite) |
| NotificationsExpireDays.request | number | null | Number of days to keep request notications (null or 0 for infinite) |
| NotificationsExpireDays.group | number | null | Number of days to keep group notications (null or 0 for infinite) |
| NotificationsExpireDays.default | number | null | Number of days to keep no type notications (null or 0 for infinite) |
| groupPlugins | object | {} | External plugins for group |
| PLUGINNAME | object | {} | General group plugin settings, see below "nextcloud" and "rocketChat" for specific settings |
| groupPlugins.PLUGINNAME.enable | boolean | false | If true, the group plugin is enabled |
| groupPlugins.PLUGINNAME.URL | string | "" | Group plugin URL |
| groupPlugins.PLUGINNAME.groupURL | string | "" | [URL]/group/[GROUPSLUG]" "[URL]/apps/files/?dir=/[GROUPNAME] |
| groupPlugins.PLUGINNAME.enableChangeName | boolean | true | If true, changing the group name for this group plugin is possible |
| Key | Type | Default value | Description |
| :------------------ | ------- | ----------------------- | ------------------------------------------------------------- |
| appName | string | "Sondage" | Application Name |
| appDescription | string | "" | Application description, it will be displayed under the title |
| enableKeycloak | boolean | false | If true, keycloak is enabled |
| keycloakUrl | string | "" | Keycloak URL |
| keycloakRealm | string | "" | Keycloak Realm |
| services | object | {} | Contains services url |
| services.sondageUrl | string | "http://localhost:3010" | The url is necessary for filling in the confirmation emails |
| laboiteHost | string | "http://localhost:3000" | La Boite app url |
## keycloak:
......@@ -74,12 +50,6 @@ Copy `settings-development.json.sample` to `settings-development.json` and updat
## private:
| Key | Type | Default value | Description |
| --------------------- | -------- | ------------------------------------------ | ------------------------------------------------ |
| loginExpirationInDays | number | 90 | Number of days for the token session to expire |
| fillWithFakeData | boolean | false | If true, fake datas are generated at start |
| minioAccess | string | "" | Minio user |
| minioSecret | string | "" | Minio password |
| apiKeys | [string] | [""] | API access keys for external services |
| BBBSecret | string | "" | Big Blue Button Secret |
| whiteDomains | [string] | ["^ac-[a-z-]_\\.fr", "^[a-z-]_\\.gouv.fr"] | Emails white domains for user account activation |
| Key | Type | Default value | Description |
| ------- | -------- | ------------- | ------------------------------------- |
| apiKeys | [string] | [""] | API access keys for external services |
......@@ -5,69 +5,10 @@
"enableKeycloak": false,
"keycloakUrl": "",
"keycloakRealm": "",
"laboiteBlogURL": "",
"enableBBB": false,
"BBBUrl": "",
"minioSSL": false,
"minioPort": null,
"minioEndPoint": "",
"minioBucket": "",
"imageFilesTypes": [
"svg",
"png",
"jpg",
"gif",
"jpeg"
],
"audioFilesTypes": [
"wav",
"mp3",
"ogg"
],
"videoFilesTypes": [
"mp4",
"webm",
"avi",
"wmv"
],
"textFilesTypes": [
"pdf",
"odt",
"txt",
"docx"
],
"otherFilesTypes": [
"csv"
],
"minioFileSize": 500000,
"minioStorageFilesSize": 3000000,
"maxMinioDiskPerUser": 1000000,
"NotificationsExpireDays": {
"setRole": null,
"unsetRole": null,
"request": null,
"group": null,
"default": null
},
"groupPlugins": {
"rocketChat": {
"enable": false,
"URL": "",
"groupURL": "[URL]/group/[GROUPSLUG]",
"enableChangeName": true
},
"nextcloud": {
"enable": false,
"URL": "",
"groupURL": "[URL]/apps/files/?dir=/[GROUPNAME]",
"enableChangeName": false
}
},
"services": {
"agendaUrl": "http://localhost:3030",
"sondagesUrl": "http://localhost:3010",
"mezigUrl": "http://localhost:3020"
}
"sondagesUrl": "http://localhost:3010"
},
"laboiteHost": "http://localhost:3000"
},
"keycloak": {
"pubkey": "",
......@@ -76,32 +17,12 @@
"adminUser": "",
"adminPassword": ""
},
"nextcloud": {
"nextcloudUser": "",
"nextcloudPassword": "",
"nextcloudQuota": "1073741824"
},
"rocketChat": {
"rocketChatUser": "",
"rocketChatPassword": ""
},
"smtp": {
"url": "smtps://USERNAME:PASSWORD@HOST:PORT",
"fromEmail": "",
"toEmail": ""
},
"private": {
"loginExpirationInDays": 90,
"fillWithFakeData": false,
"minioAccess": "",
"minioSecret": "",
"apiKeys": [
""
],
"BBBSecret": "",
"whiteDomains": [
"^ac-[a-z-]*\\.fr",
"^[a-z-]*\\.gouv.fr"
]
"apiKeys": [""]
}
}
/* eslint-env mocha */
/* eslint-disable func-names, prefer-arrow-callback */
import { PublicationCollector } from 'meteor/johanbrook:publication-collector';
import { assert } from 'chai';
import { Factory } from 'meteor/dburles:factory';
import AppSettings from '../appsettings';
import './publications';
import './factories';
describe('appsettings', function () {
describe('mutators', function () {
it('builds correctly from factory', function () {
const appsetting = Factory.create('appsettings');
assert.typeOf(appsetting, 'object');
});
});
describe('publications', function () {
beforeEach(function () {
AppSettings.remove({});
Factory.create('appsettings', { _id: 'settings' });
});
describe('appsettings.all', function () {
it('sends the only complet appsetting object', function (done) {
const collector = new PublicationCollector({});
collector.collect('appsettings.all', {}, (collections) => {
assert.equal(collections.appsettings.length, 1);
const resultAppSettings = collections.appsettings[0];
assert.property(resultAppSettings, 'maintenance');
assert.property(resultAppSettings, 'textMaintenance');
done();
});
});
});
});
});
import { Factory } from 'meteor/dburles:factory';
import AppSettings from '../appsettings';
Factory.define('appsettings', AppSettings, {
maintenance: () => Random.choice([true, false]),
textMaintenance: () => 'Text-Maintenance',
});
......@@ -25,3 +25,22 @@ export const eventTemplate = ({ sender, title, date }) => `
</div>
<br/>
`;
export const adminMeetingTemplate = ({ sender, title, date, url, connected }) => `
<h4>Un créneau a été sélectionné</h4>
<br/>
<div>
${
connected
? ` ${sender.firstName} ${sender.lastName} (${sender.emails[0].address})`
: ` ${sender.name} (${sender.email})`
}
a choisi le créneau du ${date} pour le rendez-vous ${title}
</div>
<br/>
<div>
Vous pouvez valider le créneau avec le liens suivant ${url}
</div>
<br/>
`;
/* eslint-disable func-names */
import { assert } from 'chai';
import { Meteor } from 'meteor/meteor';
import { Factory } from 'meteor/dburles:factory';
import PollsAnswers from '../../polls_answers/polls_answers';
import Polls from '../../polls/polls';
import Groups from '../../groups/groups';
import { createEventAgendaMeeting, createEventAgenda } from './methods';
import { EventsAgenda } from '../events';
import './factories';
import '../../users/server/factories';
import '../../polls/server/factories';
import '../../polls_answers/server/factories';
describe('events', function () {
describe('mutators', function () {
it('builds correctly from factory', function () {
const user = Factory.create('user');
const poll = Factory.create('poll');
const pollAnswer = Factory.create('poll_answer', { email: user.emails[0].address });
const event = Factory.create('eventsAgenda');
assert.typeOf(user, 'object');
assert.typeOf(poll, 'object');
assert.typeOf(pollAnswer, 'object');
assert.typeOf(event, 'object');
});
});
let anotherUser;
let ownerPollUser;
let poll;
let pollAnswer;
beforeEach(function () {
PollsAnswers.remove({});
Polls.remove({});
Meteor.users.remove({});
anotherUser = Factory.create('user');
ownerPollUser = Factory.create('user');
poll = Factory.create('poll', { active: false, public: true, userId: ownerPollUser._id });
pollAnswer = Factory.create('poll_answer', { userId: anotherUser._id, email: anotherUser.emails[0].address });
});
describe('createEventAgendaMeeting', function () {
it('should create an event meeting into agenda with a connected user', function () {
createEventAgendaMeeting._execute({ userId: ownerPollUser._id }, { poll, answer: pollAnswer });
const resultEvent = EventsAgenda.findOne({ title: poll.title });
assert.typeOf(resultEvent, 'object');
assert.typeOf(resultEvent.start, 'date');
assert.typeOf(resultEvent.end, 'date');
assert.notEqual(resultEvent.start, resultEvent.end);
assert.equal(resultEvent.participants[0]._id, anotherUser._id);
assert.isEmpty(resultEvent.guests);
});
it('should create an event meeting into agenda with a guest user', function () {
const anotherPollAnswer = Factory.create('poll_answer', { email: 'toto@test.com' });
createEventAgendaMeeting._execute({ userId: ownerPollUser._id }, { poll, answer: anotherPollAnswer });
const resultEvent = EventsAgenda.findOne({ title: poll.title });
assert.typeOf(resultEvent, 'object');
assert.typeOf(resultEvent.start, 'date');
assert.typeOf(resultEvent.end, 'date');
assert.notEqual(resultEvent.start, resultEvent.end);
assert.isEmpty(resultEvent.participants);
assert.equal(resultEvent.guests[0], 'toto@test.com');
});
});
describe('createEventAgenda', function () {
it('should create an event poll into agenda with a connected user', function () {
Groups.insert({
name: 'testGroup',
slug: 'testgroup',
owner: ownerPollUser._id,
members: [ownerPollUser._id, anotherUser._id],
admins: [],
animators: [],
});
const groupId = Groups.findOne({ name: 'testGroup' })._id;
const pollGroup = Factory.create('poll', {
active: false,
public: true,
userId: ownerPollUser._id,
groups: [groupId],
});
Factory.create('poll_answer', { userId: null, pollId: pollGroup._id, email: 'toto@test.com' });
const date = new Date(Date.now() + 1000 * 60 * 60 * 24);
createEventAgenda._execute({ userId: ownerPollUser._id }, { poll: pollGroup, date });
const resultEvent = EventsAgenda.findOne({ title: pollGroup.title });
assert.typeOf(resultEvent, 'object');
assert.typeOf(resultEvent.start, 'date');
assert.typeOf(resultEvent.end, 'date');
assert.notEqual(resultEvent.start, resultEvent.end);
const participantsIds = resultEvent.participants.map((participant) => participant._id);
assert.isTrue(participantsIds.includes(anotherUser._id));
assert.isTrue(participantsIds.includes(ownerPollUser._id));
assert.equal(resultEvent.guests[0], 'toto@test.com');
});
});
});
import { Random } from 'meteor/random';
import { Factory } from 'meteor/dburles:factory';
import { EventsAgenda } from '../events';
Factory.define('eventsAgenda', EventsAgenda, {
title: () => Random.id(),
start: () => new Date(),
end: () => new Date(Date.now() + 1000 * 60 * 60 * 24),
allDay: () => Math.random() >= 0.5,
recurrent: () => Math.random() >= 0.5,
userId: () => Random.id(),
});
......@@ -7,7 +7,7 @@ import PollsAnswers from '../../polls_answers/polls_answers';
import Polls from '../../polls/polls';
import Groups from '../../groups/groups';
import { DURATIONS_TIME, POLLS_TYPES, ROUTES } from '../../../utils/enums';
import { meetingTemplate, eventTemplate } from './email_template';
import { meetingTemplate, eventTemplate, adminMeetingTemplate } from './email_template';
import { EventsAgenda } from '../events';
export const sendEmail = new ValidatedMethod({
......@@ -42,6 +42,32 @@ export const sendEmail = new ValidatedMethod({
});
},
});
export function sendEmailToCreator(poll, answer, userId) {
const template = adminMeetingTemplate;
const connected = userId !== null && userId !== undefined;
let answerer;
if (connected) {
answerer = Meteor.users.findOne(userId);
} else {
answerer = answer;
}
const admin = Meteor.users.findOne(poll.userId);
const html = template({
title: poll.title,
sender: answerer,
date: moment(answer.meetingSlot).format('LLL'),
url: `${Meteor.settings.public.services.sondagesUrl}/poll/answer/${poll._id} `,
connected,
});
Email.send({
to: admin.emails[0].address,
from: Meteor.settings.smtp.fromEmail,
subject: `Rendez-vous - nouveau créneau sélectionné pour le rendez-vous ${poll.title}`,
inReplyTo: Meteor.settings.smtp.toEmail,
html,
});
}
export const createEventAgendaMeeting = new ValidatedMethod({
name: 'events.createMeeting',
validate: new SimpleSchema({
......
import { Factory } from 'meteor/dburles:factory';
import Groups from '../groups';
Factory.define('group', Groups, {
admins: () => [],
animators: () => [],
members: () => [],
});
/* eslint-disable func-names */
import { assert } from 'chai';
import { Meteor } from 'meteor/meteor';
import { Factory } from 'meteor/dburles:factory';
import { PublicationCollector } from 'meteor/johanbrook:publication-collector';
import Groups from '../groups';
import './publications';
import './factories';
import '../../users/server/factories';
describe('groups', function () {
describe('mutators', function () {
it('builds correctly from factory', function () {
const user = Factory.create('user');
const group = Factory.create('group');
assert.typeOf(group, 'object');
assert.typeOf(user, 'object');
});
});
describe('publications', function () {
describe('groups.memberOf', function () {
let user;
let anotherUser;
beforeEach(function () {
Groups.remove({});
Meteor.users.remove({});
user = Factory.create('user');
anotherUser = Factory.create('user');
Factory.create('group', { admins: [user._id], name: 'test1' });
Factory.create('group', { admins: [anotherUser._id], name: 'test2' });
Factory.create('group', { animators: [user._id], name: 'test3' });
Factory.create('group', { animators: [anotherUser._id], name: 'test4' });
Factory.create('group', { members: [user._id], name: 'test5' });
Factory.create('group', { members: [anotherUser._id], name: 'test6' });
});
it('should return groups user', function (done) {
const collector = new PublicationCollector({ userId: user._id });
collector.collect('groups.memberOf', {}, (collections) => {
assert.equal(collections.groups.length, 3);
assert.containsAllKeys(collections.groups[0], ['name', '_id']);
done();
});
});
});
});
});
......@@ -16,7 +16,7 @@ export const createPoll = new ValidatedMethod({
if (!this.userId) {
throw new Meteor.Error('api.polls.methods.create.notLoggedIn', 'api.errors.notLoggedIn');
}
return Polls.insert(data);
return Polls.insert({ ...data, userId: this.userId });
},
});
export const removePolls = new ValidatedMethod({
......
......@@ -60,9 +60,6 @@ Polls.schema = new SimpleSchema(
type: String,
regEx: SimpleSchema.RegEx.Id,
label: 'Owner',
autoValue() {
return Meteor.userId();
},
},
description: {
type: String,
......
import { Random } from 'meteor/random';
import { Factory } from 'meteor/dburles:factory';
import faker from 'faker';
import Polls from '../polls';
Factory.define('poll', Polls, {
title: () => Random.id(),
description: faker.lorem.sentence(),
userId: () => Random.id(),
groups: () => new Array(Math.floor(Math.random() * 10)).fill(0).map(() => Random.id()),
public: () => Random.choice([true, false]),
allDay: () => Random.choice([true, false]),
dates: () =>
new Array(Math.floor(Math.random() * 10)).fill({
date: new Date(Date.now() + Math.floor(Math.random() * 7) * 1000 * 60 * 60 * 24),
slots: new Array(Math.floor(Math.random() * 4)).fill(
`${Math.floor(Math.random() * 23)}:${Math.floor(Math.random() * 59)}`,
),
}),
});
......@@ -107,7 +107,7 @@ export const validatePollAnswer = new ValidatedMethod({
}
if (poll.groups.length) {
createEventAgenda.call({ poll, date });
createEventAgenda._execute({ userId: this.userId }, { poll, date });
if (!Meteor.isTest) {
// eslint-disable-next-line global-require
const sendnotif = require('../../notifications/server/notifSender').default;
......
This diff is collapsed.
......@@ -45,6 +45,7 @@ Meteor.publish('polls.member', function pollMember({ page, limit }) {
Counts.publish(this, 'polls.member.total', Polls.find(query), { noReady: true });
return Polls.find(query, options);
});
Meteor.publish('polls.meetings.member', function pollMeetingMember({ page, limit }) {
const answers = PollsAnswers.find({ userId: this.userId }, { fields: { pollId: 1 } }).fetch();
const groups = Groups.find(
......
......@@ -49,12 +49,6 @@ PollsAnswers.schema = new SimpleSchema(
regEx: SimpleSchema.RegEx.Id,
label: 'Owner',
optional: true,
autoValue() {
if (this.isInsert || this.isUpsert) {
return this.userId;
}
return this.value;
},
},
email: {
type: String,
......