-
-
- `,
props:{
- autoRenew: { required:false,default:null }, // refresh list data every x seconds
- choices: { type:Array, required:false, default:() => [] }, // processed query choices
- columns: { type:Array, required:true }, // processed list columns
- fieldId: { type:String, required:true },
- filters: { type:Array, required:true }, // processed query filters
- handleError:{ type:Function,required:true },
- iconId: { required:false,default:null },
- layout: { type:String, required:false, default:'table' }, // list layout: table, cards
- query: { type:Object, required:true }, // list query
- resultLimit:{ type:Number, required:false, default:10 }, // default result limit
+ autoRenew: { required:false,default:null }, // refresh list data every x seconds
+ choices: { type:Array, required:false, default:() => [] }, // processed query choices
+ columns: { type:Array, required:true }, // processed list columns
+ fieldId: { type:String, required:true },
+ filters: { type:Array, required:true }, // processed query filters
+ handleError: { type:Function,required:true },
+ iconId: { required:false,default:null },
+ layout: { type:String, required:false, default:'table' }, // list layout: table, cards
+ limitDefault:{ type:Number, required:false, default:10 }, // default list limit
+ query: { type:Object, required:true }, // list query
// toggles
+ allowPaging:{ type:Boolean, required:false, default:true }, // enable paging
csvExport: { type:Boolean, required:false, default:false },
csvImport: { type:Boolean, required:false, default:false },
filterQuick:{ type:Boolean, required:false, default:false }, // enable quick filter
@@ -665,6 +665,13 @@ let MyList = {
}
return out;
},
+ choiceIdDefault:function() {
+ // default is user field option, fallback is first choice in list
+ return this.fieldOptionGet(
+ this.fieldId,'choiceId',
+ this.choices.length === 0 ? null : this.choices[0].id
+ );
+ },
hasBulkActions:function() {
if(this.isInput || this.rows.length === 0)
return false;
@@ -700,6 +707,14 @@ let MyList = {
return this.focused ? '' : this.inputCaption;
},
+ limitOptions:function() {
+ let out = [10,25,50,100,250,500,1000];
+
+ if(!out.includes(this.limitDefault))
+ out.unshift(this.limitDefault);
+
+ return out.sort((a,b) => a-b);
+ },
pageCount:function() {
if(this.count === 0) return 0;
@@ -767,16 +782,15 @@ let MyList = {
},
// simple
- autoSelect: function() { return this.inputIsNew && this.inputAutoSelect !== 0 && !this.inputAutoSelectDone; },
- choiceFilters:function() { return this.getChoiceFilters(this.choices,this.choiceId); },
- expressions: function() { return this.getQueryExpressions(this.columns); },
- joins: function() { return this.fillRelationRecordIds(this.query.joins); },
+ autoSelect: function() { return this.inputIsNew && this.inputAutoSelect !== 0 && !this.inputAutoSelectDone; },
+ choiceFilters: function() { return this.getChoiceFilters(this.choices,this.choiceId); },
+ expressions: function() { return this.getQueryExpressions(this.columns); },
+ joins: function() { return this.fillRelationRecordIds(this.query.joins); },
// stores
relationIdMap: function() { return this.$store.getters['schema/relationIdMap']; },
attributeIdMap: function() { return this.$store.getters['schema/attributeIdMap']; },
iconIdMap: function() { return this.$store.getters['schema/iconIdMap']; },
- fieldIdMapOption:function() { return this.$store.getters['local/fieldIdMapOption']; },
capApp: function() { return this.$store.getters.captions.list; },
capGen: function() { return this.$store.getters.captions.generic; },
isMobile: function() { return this.$store.getters.isMobile; },
@@ -826,20 +840,14 @@ let MyList = {
this.paramsUpdate(false); // overwrite parameters (in case defaults are set)
} else {
// sub lists are initiated once
- this.choiceId = this.choices.length > 0 ? this.choices[0].id : null;
- this.limit = this.resultLimit;
- this.orders = JSON.parse(JSON.stringify(this.query.orders));
+ this.choiceId = this.choiceIdDefault;
+ this.limit = this.limitDefault;
+ this.orders = JSON.parse(JSON.stringify(this.query.orders));
}
// set initial auto renew timer
if(this.autoRenew !== null) {
- this.autoRenewInput = this.autoRenew;
-
- if(typeof this.fieldIdMapOption[this.fieldId] !== 'undefined'
- && typeof this.fieldIdMapOption[this.fieldId]['autoRenew'] !== 'undefined') {
-
- this.autoRenewInput = this.fieldIdMapOption[this.fieldId]['autoRenew'];
- }
+ this.autoRenewInput = this.fieldOptionGet(this.fieldId,'autoRenew',this.autoRenew);
this.setAutoRenewTimer(false);
}
},
@@ -848,6 +856,8 @@ let MyList = {
},
methods:{
// externals
+ fieldOptionGet,
+ fieldOptionSet,
fillRelationRecordIds,
getCaption,
getChoiceFilters,
@@ -902,10 +912,13 @@ let MyList = {
reloadInside:function(entity) {
// inside state has changed, reload list (not relevant for list input)
switch(entity) {
- case 'choice': // fallthrough
case 'dropdown': // fallthrough
case 'filterQuick': // fallthrough
- case 'filtersUser': this.offset = 0; break;
+ case 'filtersUser': this.offset = 0; break;
+ case 'choice':
+ this.offset = 0;
+ this.fieldOptionSet(this.fieldId,'choiceId',this.choiceId);
+ break;
case 'order':
this.offset = 0;
this.orderOverwritten = true;
@@ -953,8 +966,8 @@ let MyList = {
// initial order by parameter follows query order
// if user overwrites order, initial order is empty
let params = {
- choice: { parse:'string', value:this.choices.length > 0 ? this.choices[0].id : null },
- limit: { parse:'int', value:this.resultLimit },
+ choice: { parse:'string', value:this.choiceIdDefault },
+ limit: { parse:'int', value:this.limitDefault },
offset: { parse:'int', value:0 },
orderby: { parse:'listOrder',value:!this.orderOverwritten ? JSON.stringify(this.query.orders) : '[]' },
quickfilter:{ parse:'string', value:'' }
@@ -1212,11 +1225,7 @@ let MyList = {
this.autoRenewTimer = setInterval(this.get,this.autoRenewInput * 1000);
// store timer option for field
- this.$store.commit('local/fieldOptionSet',{
- fieldId:this.fieldId,
- name:'autoRenew',
- value:this.autoRenewInput
- });
+ this.fieldOptionSet(this.fieldId,'autoRenew',this.autoRenewInput);
},
toggleOrderBy:function() {
this.orders[0].ascending = !this.orders[0].ascending;
diff --git a/www/comps/shared/access.js b/www/comps/shared/access.js
index f79ee9f7..30db59be 100644
--- a/www/comps/shared/access.js
+++ b/www/comps/shared/access.js
@@ -25,4 +25,21 @@ export function hasAnyAssignableRole(roles) {
return true;
}
return false;
+};
+
+export function getStartFormId(module,access) {
+
+ // return NULL if no menu access is granted at all
+ if(!hasAccessToAnyMenu(module.menus,access.menu))
+ return null;
+
+ // check role specific start form
+ for(let i = 0, j = module.startForms.length; i < j; i++) {
+
+ if(access.roleIds.includes(module.startForms[i].roleId))
+ return module.startForms[i].formId;
+ }
+
+ // return default start form (NULL is allowed)
+ return module.formId;
};
\ No newline at end of file
diff --git a/www/comps/shared/field.js b/www/comps/shared/field.js
new file mode 100644
index 00000000..739d03a4
--- /dev/null
+++ b/www/comps/shared/field.js
@@ -0,0 +1,18 @@
+import MyStore from '../../stores/store.js';
+
+export function fieldOptionGet(fieldId,optionName,fallbackValue) {
+ let map = MyStore.getters['local/fieldIdMapOption'];
+
+ if(typeof map[fieldId] === 'undefined' || typeof map[fieldId][optionName] === 'undefined')
+ return fallbackValue;
+
+ return map[fieldId][optionName];
+};
+
+export function fieldOptionSet(fieldId,optionName,value) {
+ MyStore.commit('local/fieldOptionSet',{
+ fieldId:fieldId,
+ name:optionName,
+ value:value
+ });
+};
\ No newline at end of file
diff --git a/www/comps/shared/form.js b/www/comps/shared/form.js
index 242de8be..c3d106a7 100644
--- a/www/comps/shared/form.js
+++ b/www/comps/shared/form.js
@@ -136,4 +136,21 @@ export function getResolvedPlaceholders(value) {
case '{CURR_DATE_DD}': return new Date().getDate(); break;
}
return value;
+};
+
+// set getter argument values in array of empty/pre-existing getters
+export function setGetterArgs(argsArray,name,value) {
+
+ if(argsArray.length === 0)
+ return [`${name}=${value}`];
+
+ for(let i = 0, j = argsArray.length; i < j; i++) {
+
+ if(argsArray[i].indexOf(`${name}=`) === 0) {
+ // argument already exists, add new value to it
+ argsArray[i] = `${argsArray[i]},${value}`;
+ break;
+ }
+ }
+ return argsArray;
};
\ No newline at end of file
diff --git a/www/comps/shared/query.js b/www/comps/shared/query.js
index 80f7a10b..773d64f3 100644
--- a/www/comps/shared/query.js
+++ b/www/comps/shared/query.js
@@ -52,6 +52,7 @@ export function getQueryExpressions(columns) {
query:{
queryId:c.query.id,
relationId:c.query.relationId,
+ limit:c.query.fixedLimit,
joins:c.query.joins,
expressions:[getQueryExpressionAttribute(c)],
filters:c.query.filters,
@@ -133,6 +134,15 @@ export function getQueryFiltersProcessed(filters,dataFieldIdMap,joinsIndexMap,jo
case 'login':
s.value = MyStore.getters.loginId;
break;
+ case 'preset':
+ // unprotected presets can be deleted, 0 as fallback
+ s.value = 0;
+
+ let presetIdMap = MyStore.getters['schema/presetIdMapRecordId'];
+
+ if(typeof presetIdMap[s.presetId] !== 'undefined')
+ s.value = presetIdMap[s.presetId];
+ break;
case 'record':
if(typeof joinsIndexMap['0'] !== 'undefined')
s.value = joinsIndexMap['0'].recordId;
@@ -216,7 +226,7 @@ export function getQueryAttributesPkFilter(relationId,recordIds,index,not) {
export function getQueryTemplate() {
return {
id:'00000000-0000-0000-0000-000000000000',
- relationId:null,joins:[],filters:[],orders:[],lookups:[],choices:[]
+ relationId:null,fixedLimit:0,joins:[],filters:[],orders:[],lookups:[],choices:[]
};
};
diff --git a/www/docs/en_us_builder.html b/www/docs/en_us_builder.html
index 6497f306..481f8479 100644
--- a/www/docs/en_us_builder.html
+++ b/www/docs/en_us_builder.html
@@ -1,8 +1,8 @@
-
Updated 2021-09-28
+
Updated 2021-11-07
Table of contents
- Introduction
-- Basics and the Builder
+- The Builder
- Getting started
- Installing applications as reference
@@ -17,6 +17,7 @@ Table of contents
- Indexing
- Functions
- Triggers
+- Policies
- Change logs
- Roles and access management
@@ -67,43 +68,46 @@ Table of contents
- Troubleshooting
Introduction
-
This is the documentation for building applications with the REI3 application platform. REI3 is a server application that runs on-premise or in the cloud, on Windows and Linux. REI3 applications are served as a progressive web app that runs on any modern browser and can be installed on clients directly. To learn how to setup and administrate REI3, please refer to the admin documentation.
-
With REI3, simple applications can be created very quickly, while complex and scalable applications are also facilitated. It can be compared to low-code products, but also offers access to low-level entities such as database triggers, indexes, sub queries, custom functions, complex relationships and so on. In this regard REI3 can be used to create powerful applications, while benefiting greatly from existing knowledge of relational database systems. It is not necessary to have deep knowledge of database systems to build applications in REI3; however by learning about them, more functionality is available to application authors.
-
Building REI3 applications is done exclusively with the integrated, graphical interface, simply called 'the Builder'. The Builder is included in all REI3 releases but must be enabled before it can be accessed.
-
Basics and the Builder
-
Before accessing the Builder there are a few important concepts to understand:
-
-- Instance: An installation of REI3. To differentiate from packaged applications, we use the word 'instance' to refer to running installations of REI3.
-- Schema: A complete description of a REI3 application, containing its data, role, function, UI and other definitions. The Builder exclusively manipulates application schemas.
-- Transfer: A shorthand for the process of exporting a compressed and signed schema file and importing it into other REI3 instances. Transfers can be completed manually (by uploading schema files) or automatically via repositories (the official REI3 repository or self-hosted ones). Executing a transfer means installing or updating an application schema inside another REI3 instance.
-
-
Using the Builder enables full write access to application schemas inside a REI3 instance. Changing an application schema will directly manipulate underlying data structures, potentially destroying existing data in the instance but also in target instances when transferring an application.
-
Important precautions
+
This is the documentation for building REI3 applications. REI3 is a free, open-source business application platform, which can run on-premise or from the cloud, on Windows and Linux. To learn how to setup and administrate REI3, please refer to the admin documentation.
+
REI3´s primary purpose is to fulfill business software requirements. Simple applications, like a system that manages company event attendance, can be built within a single hour. More complex applications, like absence management or a system to execute mail campaigns, require more time but are also easily buildable with REI3. Organizations using REI3 can save time and money by reducing dependency on costly software solutions for simple requirements, while cheaply addressing complex, niche requirements that no fitting solution is available for.
+
Most people with IT administration skills can utilize REI3 to address software requirements; it is however very useful to know and understand relational databases to make use of all features. REI3 provides access to low-level entities, such as database indexes, triggers, sub queries, functions and so on. With some background knowledge, complex relationships, business logic, access control as well as performance tuning can be implemented to build powerful, fast and scalable applications.
+
The Builder
+
Building REI3 applications is done exclusively with an integrated, graphical utility, simply called 'the Builder'. The Builder is included in all REI3 releases.
+
Using the Builder, individuals and organizations can create or change applications. Finished applications can then be exported as file and re-imported into other REI3 instances. This way applications can be built on one system and transferred to another. It is also possible to share applications with others by exchanging REI3 files or using online repositories.
+
Changing an application with the Builder can result in changes to the underlying data structure, potentially deleting data in the instance but also in other instances when transferring an application. Precautions must be taken accordingly:
- NEVER use the Builder in productive instances. To build, use the portable version of REI3 or run a separate REI3 instance. For critical instances, it is smart to run a copy of a productive system to confirm application changes before deploying to the final, productive instance.
-- DO NOT make changes to applications from other authors directly - these are deleted when the application is updated. You can expand existing applications safely by adding custom data & UI on top of them inside your own applications. Please refer to Building on applications to learn more.
+- DO NOT make changes to applications from other authors - these are deleted when the application is updated. You can expand existing applications safely by adding custom data & UI on top of them inside your own applications. Please refer to Building on applications to learn more.
- ALWAYS consider carefully when deciding to delete data structures (relations/attributes). This will affect target instances and, if other applications build on yours, affect other applications as well. Renaming data structures is safe however.
-
Once you understand these concepts, and have a non-productive instance ready, you can enable the Builder by changing the option 'builder' to 'true' within the file 'config.json' inside your REI3 application directory. After a restart of REI3, and enabling the maintenance mode in the admin UI, the Builder icon will appear on the top-left corner of the REI3 page header.
+
To enable the Builder, you need to login into a running REI3 instance with an admin user. After enabling the maintenance mode, which will kick all non-admin users from the system, the Builder can be switched on. Once activated, the Builder can be accessed from its icon on the top-left corner of the main menu.
+
Getting started
-
Going into the Builder UI you will be presented with a list of installed application schemas (empty if none are installed) as well as an application export dialog. To create a new application, you only need to decide on an unique name and click on the save icon. The name is important as it must stay unique even after transferring to another instance.
-
For the official repository of REI3, we decided to use author prefixes to avoid naming conflicts (ours is 'r3_'). You are free to choose any name you wish; an unique prefix is required however if you want to publish applications in the official REI3 repository. You can request a prefix by contacting us.
+
Going into the Builder UI you will be presented with a list of installed applications. To create a new application, you only need to decide on a name and click on the save icon. The name can become important later as application names need to stay unique within a given REI3 instance. It is however no problem the change the name later.
+
Installing applications as reference
To play around and see working examples, you can install applications from the REI3 repository from the admin UI. In a non-productive instance you can install, change, delete and re-install applications at any time. Installing finished applications can help with understanding how these are built and how specific options are used.
+
Creating your own applications
-
Creating a new application is straightforward. After opening the Builder, a list of installed applications is presented to you. The first line is reserved for creating new applications. By entering an unique name for the application and clicking the save icon, a new application is generated. You can then open the application to add things like data storage, menus, forms and much more.
+
Creating a new application is straightforward. After opening the Builder, a list of installed applications is presented to you. The first line is reserved for creating new applications. By entering a unique name for the application and clicking the save icon, a new application is generated. You can then open the application to add things like data storage, menus, forms and so on.
There a few options when creating or updating the application itself:
-- Name: An unique name, representing your application. Only important for transferring your application to other instances - users see a translated application title instead (see below).
+- Name: A unique name, representing your application. Only important for transferring your application to other instances - users see a translated application title instead (see below).
- Depends on (optional): Dependencies to other applications. More details in Building on applications.
- Assigned to (optional): Application which serves as a parent to the selected application. Must be activated in 'depends on' first. This option effectively creates a hierarchy in the start and header menus. Currently the hierarchy is limited to 2 levels (1 parent, multiple children) - these might be expanded in the future.
- Icon (optional): An icon representing the application. Must be added to the application first.
- Color (optional): Freely selectable color. Is shown on the start page and on the header when the application is active. Can be overwritten in instances with customizing. Should be a color with low-brightness to contrast bright header fonts.
- Default position: Order in which application should appear in menus (smaller first). Can be overwritten in instances by admins. When 'assigned to' a parent application, positions affect the order underneath the parent application.
-- Start form (optional but required for menu): Form that opens when clicking on the application from the start or header menu. A form must be created first to be selectable here - a menu entry for the application will only appear once a start form is set.
-- Translations (1 is required): Languages, the application is available in. A language is defined by its 5-letter language code (en_us, de_de, ch_cn, and so on). Be aware: You must provide translations for all languages that you define here - not doing this results in error strings being shown to the end user. Besides the application title, other entities (menus, forms, help pages, ...) require translations when creating them. You can also define a fallback language, which is used if a user selects a language that your application does not offer. To start, you should keep to a single translation and expand when needed.
+- Start form (optional): Depending on assigned roles, a different form can be shown when opening the application. Multiple rules for matching role membership with start form can be set; if no rule matches, the default start form is shown. A valid start form is required for the application to show up in the user´s menu.
+
+
+
+- Languages (1 is required): Languages, the application is available in. A language is defined by its 5-letter language code (en_us, de_de, ch_cn, and so on). Be aware: You must provide translations for all languages that you define here - not doing this results in error strings being shown to users. Besides the application title, other entities (menus, forms, help pages, ...) require translations when creating them. You can also define a fallback language, which is used if a user selects a language that your application does not offer. To start, you should keep to a single translation and expand when needed.
+
+
+
-
Some options depend on other entities being created first (other applications to build on, forms to select as start form, icons to show). When creating an application these can be skipped and added/changed later.
+
Some options depend on other entities being created first (other applications to build on, forms to select as start form, icons to show). When creating an application these can be skipped and updated later.
In addition, a couple of non-editable meta data fields are shown. These are set when an updated application is exported. Please refer to Application transfers for more details.
- Release date: Current date when exported.
@@ -116,13 +120,15 @@ Data storage
Relations and attributes are referenced in forms to display, create or update data in lists, fields, calendars and so on. They are also accessible in custom functions for complex data manipulation, calculations or other data related actions.
Relations
To store data for any entity, you create a relation - this will automatically create a corresponding database table with the same name. REI3 enforces valid names when things like relations are created. Optionally, you can also specify data retention settings for the relation.
-Relations are central to managing data in REI3. They contain all records, their attributes (e. g. their data), indexes (mostly for performance tuning), presets (predefined records) and triggers (automatically executed functions). To store anything, relations must be populated with attributes.
+
+Relations are central to managing data in REI3. They contain all records, their values (following their attributes), indexes (mostly for performance tuning), presets (predefined records), triggers (automatically executed functions) and policies.
Attributes
An attribute is a data field for each record in a relation. When creating an attribute, a database column is added to its relation table with the same name.
+
Adding an attribute has many more options available compared to relations:
-- Name: Attribute name, needs to be unique within the relation.
- Icon: Default icon for this attribute. Used by attribute input fields on data forms if no other icon is defined.
+- Name: Attribute name, needs to be unique within the relation.
- Title: A translated title - displayed in attribute input fields and list columns if not overwritten.
- Content: The content of the attribute - multiple content types exist.
@@ -152,16 +158,18 @@ Relationships
- Relation 2: class (details of class records)
- Relation 3: student_class (N:1 attribute to 'student', N:1 attribute to 'class', integer attribute 'started in year' to describe first year of student attendance)
+
Relationship attributes use foreign keys to enforce the existence of referenced records. When a record that serves as relationship target is deleted, the foreign key becomes invalid. This also occurs, although more rarely, when the referenced key of such a record is changed. To deal with these cases, you can specify a desired behavior for 'ON DELETE' (referenced record is deleted) and 'ON UPDATE' (key of referenced record is changed):
- NO ACTION: Block the deletion of the original record.
- RESTRICT: Similar to NO ACTION, but does not allow deferring in a running transaction. This option is not important in most cases but is available when needed.
- CASCADE: Delete the record which is referencing the invalid relationship target. Often used in parent-to-child relationships - when parent is deleted, delete all children automatically.
-- SET NULL: Replace the invalid value with NULL. Only works if the relationship attribute is nullable.
+- SET NULL: Replace the invalid value with NULL. Only works if the relationship attribute is nullable.
- SET DEFAULT: Replace the invalid value with defined default value. Only works if a default value is set.
Presets
Presets are predefined records that are shipped with your application. A preset consists of a name as well as attribute values. The preset name only serves to reference to the preset inside the Builder - it is not visible to any end user. When your application is transferred to a target instance, one record for each preset is created in the corresponding relation. All defined attribute values are applied to this record.
+
When you define your preset, you can choose to protect it and/or its attribute values. Protecting the preset itself, blocks deletion of its associated record, while protecting attribute values blocks updating these values. The protection settings can be mixed to serve different purposes:
- Protected record, protected values. For records that must stay a certain way for your application to work properly; this can be useful for workflow states.
@@ -186,12 +194,13 @@ Indexing
- As indexes serve to find records by certain criteria, attributes that are used in filters benefit from indexing a lot.
- Having indexes for specific attributes is important for good database performance; creating indexes for all attributes is a bad idea however. The more indexes exist, the slower changes to records become as indexes must be updated as well. In addition, each index has storage overhead, adding to the database size.
- Unique indexes can be used to enforce unique values in all kinds of attributes (unique names/numbers/dates/...). This is one use-case for indexing besides performance.
-- Indexes can be used to enforce unique, composite keys by using multiple attributes to form an unique index.
+- Indexes can be used to enforce unique, composite keys by using multiple attributes to form a unique index.
Functions
Functions in REI3 are PL/pgSQL functions. PL/pgSQL or Procedural Language/PostgreSQL is a programming language of the underlying database system that REI3 is running on (PostgreSQL). To learn more about writing PL/pgSQL, please refer to the official documentation for this language.
By writing functions, complex data manipulation tasks can be achieved. Functions can be executed with relation triggers, called from other functions or regularly by using function schedules.
-REI3 includes dynamic placeholders so that all references to an application schema (relations, attributes and other functions) are upgrade safe - this includes safe renaming and blocking of deletion in case entities are still referenced. As long as these placeholders are used, you cannot easily break your functions when making changes to your applications. Make sure to only ever use placeholders to read or write from/to the database, otherwise the stability of the system can be negatively affected.
+
+REI3 includes dynamic placeholders so that all references to application entities (relations, attributes and other functions) are upgrade safe - this includes safe renaming and blocking of deletion in case entities are still referenced. As long as these placeholders are used, you cannot easily break your functions when making changes to your applications. Make sure to only ever use placeholders to read or write from/to the database, otherwise the stability of the system can be negatively affected.
In addition to functions from applications, instance functions are available to expose data or features from the REI3 instance. These can be used to read configuration settings (like the public hostname), get context information (like the login ID used to access the database) or execute tasks (like sending emails). To learn more about instance functions, please refer to the integrated help texts in the Builder UI for functions.
Triggers
Triggers in REI3 are PostgreSQL database triggers. Database triggers react to events in a database and serve to apply logic, check inputs or block certain actions. When inserting, updating or deleting records in a relation, a trigger executes a defined function. In REI3, triggers also serve to access internal functions, exposed by the current instance. These instance functions are used to get information from the instance (like the ID of the current login) or execute certain actions (like sending emails).
@@ -201,11 +210,16 @@ Triggers
- EACH ROW: Trigger executes function for each affected row or once for the statement. If set per row, NEW/OLD can be used to reference the record state as it was (OLD) and what it would be changed to (NEW).
- BEFORE/AFTER: Trigger executes function before or after the specified event has occured. BEFORE can be used to overwrite missing NULL values, while AFTER can be useful to work with changes over multiple relations ('see DEFERRABLE').
- CONSTRAINT: Similar to a regular trigger, but its timing can be changed (see 'DEFERRABLE').
-- DEFERRABLE: If set, trigger may be executed at the end of a database transaction instead of immediately. As REI3 offers updates of joined relations in forms, a deferred trigger waits until all affected relations are updated and then executes its function. Currently, 'INITIALLY DEFERRED' should always be enabled as well when creating deferred triggers - this might change when more features become available.
+- DEFERRABLE: If set, trigger may be executed at the end of a database transaction instead of immediately. As REI3 offers updates of joined relations in forms, a deferred trigger waits until all affected relations are updated and then executes its function.
- INITIALLY DEFERRED: Trigger will be deferred by default.
- Condition: A SQL condition, must return true for the trigger to execute the function.
- Execute: The function to execute. Must be a function that returns 'trigger'.
+Policies
+A relation policy can limit what specific records are accessible to a logged in user via their role memberships. This is done by setting filter functions for specific actions. While forms, lists, input fields and other frontend elements can also filter records, relation polices are applied globally and cannot be circumvented by changing the frontend. Relation policies are also 'quiet', meaning that users are not informed about missing access - they simply see different records.
+
+To create a policy, a role must be connected to at least one action (display, change, delete). If records should be filtered, an 'allow' (whitelist) or 'block' (blacklist) filter function is used. A filter function is just a regular function that returns an array of record IDs (as in 'INTEGER[]' or 'bigint ARRAY'); the record IDs must match the relation for which the policy is defined. With the help of instance functions, such as 'has_role()' or 'get_login_id()', filter functions limit what records are accessible based on the currently logged in user.
+Polices have a defined order. The first policy matching the attempted action of the logged in user with the selected role, is applied. If the user has multiple roles, only the first matching policy is applied. If no filtering is desired, a role can be assigned all actions and both filter functions kept empty; this enables full access. If no policy is matched, full access is also granted.
Change logs
When data is changed in REI3 you can choose to automatically keep copies of these changes. These change retention settings are defined for relations. If nothing is set, no data changes are kept. Every relation has 2 settings for defining data change retention:
@@ -216,23 +230,27 @@ Change logs
Changes are visible to users in forms that access corresponding relations via the change log window. This will show all changes corresponding to joined relations (see Queries), but only for attributes that are accessible to the user via data input fields. If a user has access to a data field, and changes are available, they will be visible without further permissions being required.
The change retention cleanup job can be configured to run more or less often in the admin UI.
Roles and access management
-Roles serve to grant access within REI3. There are currently two distinct areas of access:
+Roles are used to control what a user can see and do in an application. Roles control:
-- Data: Low-level access to relations and attributes. Regardless what is supposed to be shown in lists and input fields, without proper access, the user cannot get data or make changes. In most cases, defining access on a relation is sufficient; setting attribute access can however be useful for giving access to very specific data. Be aware that when building forms adding lists/input fields that show inaccessible attributes for the logged in user, will result in an error.
-- Menus: Simply what menus are shown to the role member. If menus have a hierarchy, not giving access to the parent menu blocks access to any child menu.
+- Data entity access: Access control for data structures, e. g. relations and attributes.
+- Applied policies: Access control for specific records, which is defined via relation policies.
+- Used start forms: Which form is shown when a user opens an application. Defined for the application.
+- Visible menus: Which menus are shown for a role member.
-Roles in REI3 are always cumulative - the more roles a user has, the more access is available. Following that, there is no 'deny' option. When you need to deny a group of users access, you remove the corresponding access from the currently used role and create a second role granting this specific access. This second role is then only assigned to users that should still receive this access. In general, it serves to have a role concept that is as simple as possible. Complexity will inevitably come to any access definition with time and should therefore be reduced as much as possible early on.
-When creating a role, you need to choose an unique role name within your application. Titles and descriptions serve to explain the role to administrators of other REI3 instances as they will be shown in the admin UI. They are not visible to non-admin users.
+Roles in REI3 are cumulative - the more roles a user has, the more access is available. Following that, there is no 'deny' option. When you need to deny a group of users access, you remove the corresponding access from the role and create a second one, granting this specific access. This second role is then only assigned to users that should still receive this access. In general, it serves to have a role concept that is as simple as possible.
+When creating a role, you need to choose a unique role name within your application. Titles and descriptions serve to explain the role to administrators of other REI3 instances as they will be shown in the admin UI. They are not visible to non-admin users.
Roles can be members of other roles. This enables access inheritance with no fixed limit on how many levels of inheritance are allowed. A user logging in, receives access following assigned roles and the memberships of these roles (and the memberships of those roles, and so on).
-When applications build on other applications (see Building on applications) usually some access to the other application´s data is necessary. This is solved by making your roles members of roles from the other application. In many cases data access is desired but users from one application should not see user interfaces from the other; this is solved by creating data only roles (no menu access specified) with which users only get access to data.
-Lastly, the option 'assignable' controls whether a role is directly assignable to a user. If set to 'false' the specific role is hidden in the admin UI and can only be used via membership of other roles. This can be useful for data only roles or for complex role hierarchies.
+When applications build on other applications (see Building on applications) usually some access to the other application´s data is necessary. This is solved by making your roles members of roles from other applications. In many cases data access is desired but users from one application should not see user interfaces from the other; this is solved by creating 'data only' roles (no menu access specified).
+Lastly, the option 'assignable' controls whether a role is directly assignable to a user. If set to 'false' the specific role is hidden in the admin UI and can only be assigned indirectly via membership of other roles. This can be useful for 'data only' roles or for complex role hierarchies.
Presentation and user interfaces
Besides managing data, most systems require some kind of frontend. For this purpose, REI3 includes forms to display and manipulate data and menus to navigate your application.
-Forms serve to show and manipulate data. Besides menus and complimentary functions (change logs, help pages, etc.), everything you see inside a REI3 application is defined within a form. When creating a form, you can specify the following:
+Forms serve to show and manipulate data. Besides menus and complimentary functions (change logs, help pages, etc.), everything you see inside a REI3 application is defined within a form.
+
+When creating a form, you can specify the following:
- Icon: An icon representing the form. Shown in the form header, if not overwritten by a menu entry.
-- Name: An unique name in your application. Only for your own reference, not shown to users.
+- Name: A unique name in your application. Only for your own reference, not shown to users.
- Title: The translated title of your form (see Translations). Shown to users. Can be overwritten by a menu entry.
- Open preset: Option to open a specific preset record when opening a form. Useful for central configuration records but otherwise not often used.
@@ -246,6 +264,7 @@
- Showing a dashboard of multiple lists and buttons to reach different parts of your application.
+
Independently of what kind of form it is, forms are entirely comprised of fields. Fields serve to create layouts, manipulate data, execute actions and more. Available fields are shown in the sidebar of the form UI inside the Builder, depending on what the form has access to. These types of fields exist:
- Container: These construct the layout of your form. Containers 'contain' other fields, including other containers to create simple or complex layouts.
@@ -389,12 +408,12 @@ List fields
When designing complex lists with many joins, sub queries, groupings, filters and so on, logic errors or badly chosen configuration options can result in non-desirable data sets. To troubleshoot this, a SQL-preview function is available on the list field when inside the builder UI. This function returns the raw SQL that is being used to retrieve the current list data. With this preview, you can directly see how the chosen options affect the final SQL query. Regardless of the chosen configuration, each list retrieval only ever executes a single SQL, including all joins, sub queries and so on.
Calendar fields
Calendar fields work very similar to list fields. They have their own query like list fields and can use the same columns and configuration options. Please refer to list fields for detailed explanations.
-Besides the same options that are available for list fields, calendar fields have a few specific ones; these are used to generate and style the calendar entries:
+Besides options that are also available to list fields, calendar fields have additional options for generating and styling the calendar entries:
-- Date from/to: Attributes that define the 'location' of shown records on the calendar. Given attribute values must be date or datetime and must not be mixed. If date values are used, only full day entries are shown on the calendar; if datetime values are used, either full day or time based entries (example: from 12:00 to 14:30) are shown.
+- Date from/to: Attributes that define the date/time of shown records on the calendar. Given attribute values must be date or datetime and must not be mixed. If date values are used, only full day entries are shown on the calendar; if datetime values are used, both full day as well as non-full day entries are displayed.
- Color: (Optional) attribute value that is used to color in the date entries on the calendar. Should be combined with the 'Color' display option for text attributes to make inputting valid color values easy on data forms.
- Open record (relation 0): Open calendar entry (or create new one) in target form. Records must be compatible with chosen form (relation 0 of form must be the displayed record relation, see Queries).
-- Gantt: A different presentation option for the calendar. All date entries are shown on a horizontal time axis instead of a calendar grid. Once active, the date range must be selected; this is defined by how many days/before after today are on the time axis before needing to go to a new page. This option only becomes useful, when grouping results so that multiple lines can be shown on the time axis; this is done by assigning one or many columns to column batch 1 (see Column batches). The combined value of column batch 1 will group records together. Some use cases:
+
- Gantt: A different presentation option for the calendar. All date entries are shown on a horizontal time axis instead of a calendar grid. This option becomes useful, when date records can be grouped by a common criteria. A group is defined by the value of one or more columns assigned to column batch 1 (see Column batches). Some use cases:
- Date records belonging to specific individuals, with their full names used for grouping.
- Availability time date records for departments, with their department names used for grouping.
@@ -451,7 +470,7 @@ Queries
Besides selecting relations, if more than one record is to be handled (lists/calendar fields), sorting can be applied to the results. Sorting is applied to the selected attribute (chosen via relation index + attribute name) in the defined order; when multiple sorting options are defined, results are sorted by the first, then the second and so on.
Query filters
-As the name implies, query filters filter query results. They are used in list/calendar fields and in relationship inputs (fields showing relationship attributes).
+As the name implies, query filters filter query results. They are used in forms, lists, calendars and in relationship inputs (fields showing relationship attributes).
Filters are made up from filter lines. Each line contains an AND/OR connector (first line is always AND), two filter criteria (to compare them) and an operator (equals, greater than, etc.). Additionally filter lines can be grouped with left and right brackets to create more complex AND/OR conditions. Some operators do not require a second filter criteria - 'IS NULL' for example just checks the first criteria against NULL.
Query filters can be defined in two ways:
@@ -467,13 +486,14 @@ Query filters
- Attribute value: Current record value for the specified attribute. Not affected by changes on a form - updates after changes to a record were saved.
- Field value: Attribute input field value on the current data form. These can be accessed, for example, by list fields inside their own filters, allowing lists to be filtered by input fields on the same form. When the chosen input field value changes, the filter is updated and the connected query filter automatically reloads.
- Fixed value: A fixed text or number value to compare against.
-- Login ID: Integer ID of the logged in user. Can be combined with the field display option Login to connect relation records to REI3 users and then filter relations based on the logged in user. Useful for employee, customer, agent or other person-based records.
+- Login ID: Integer ID of the logged in user. Can be combined with the field display option Login or Login forms to connect relation records to REI3 users and then filter relations based on the logged in user. Useful for employee, customer, agent or other person-based records.
- Login language code: The selected 5-letter language-code ('de_de', 'en_us', etc.) of the logged in user. Can be used to join relations for translating user definable records and then filter these according to the active user interface language.
-- Login has role (TRUE/FALSE): TRUE if the currently logged in user has this role (direct assigment or inheritance are both valid).
-- Record ID ([relation name]): The integer ID of the currently handled record from a data form. Only useful when query is being executed on a data form.
-- Record is new (TRUE/FALSE): TRUE if record currently being handled by a data form is new. Only useful when query is being executed on a data form.
-- True value (TRUE). Always TRUE - used to compare against other TRUE values.
-- JavaScript expression: The return value of the given JavaScript expression. The expression is executed as a function when the filter criteria is being checked. Useful for simple date calculations (like getting the previous year) or similar tasks. Should not be used to access any objects outside of itself.
+- Login has role: TRUE if the currently logged in user has the selected role (directly assigned or inherited).
+- Preset record ID: Integer ID of the record that has been created from a preset. Only protected presets can be selected, as unprotected presets can be deleted.
+- Record ID: Integer ID of the currently handled record from a data form. Only useful when query is being executed on a data form.
+- Record is new: TRUE if record being handled by the current form is new. Only useful when query is being executed on a data form.
+- True value. Always TRUE - used to compare against other TRUE values.
+- JavaScript expression: The return value of the given JavaScript expression. The expression is executed as a function when the filter criteria is being checked. Useful for simple date calculations (like getting the previous year) or similar tasks. Should not be used to access anything outside the function itself.
- Sub query: The value from a sub query. Value type depends on the selected attribute from the defined sub query.
Help pages
@@ -517,9 +537,11 @@
- Main menu: Shown at the top of the page and on the start page for each user. Automatically created by resolving accessible applications for the current login. The header menu shows the top level of applications as well as a second level of assigned to applications (child applications). Currently only 2 levels of hierarchy are supported. The display order can be defined in the application overview (Builder start page) with the default position; this option can be overwritten in a target instance inside the admin panel. An application is only shown in the main menu, when a start form has been defined and a user has access to it; this includes admin users.
- Application menu: This can directly be defined in the Builder with unlimited levels of hierarchy; we recommend to keep the menu hierarchy to at most 3 levels for usability reasons. Each menu entry can optionally open a form and/or contain sub menu entries. When sub entries are defined, you can choose whether these entries are shown by default or must be toggled first by opening the parent menu entry. The translatable menu title as well as menu icon are applied to an opened form if the form does not define its own title/icon.
+
When Building on applications, you can choose to copy the entire menu structure from another application. This is can be useful when you intent to extend another application; you copy the original menu structure, change target forms and add your own, custom menu entries.
Sometimes it is useful to connect the currently logged in user to a record from within your application. To assign a personal, employee or a customer contact record for example.
+
To enable this, an integer attribute can be placed on any of your relations. This attribute will then contain the login ID (which also is an integer). Once the login attribute exists, you have two options to update it:
- Define a login form: By defining a login form, an instance administrator can create/select a record for each login within the admin interface. Available options:
@@ -540,7 +562,7 @@
Building on applications
- Data (relations & attributes): Access to parent relations in relationships within the child application; effectively extending these directly (with 1:1 relationships) or creating new data structures attached to the parent relations. Parent relations & attributes can also be accessed in queries, enabling access to these as if they were part of the child application.
- Forms: Access to forms from the parent application as targets in lists, buttons or within menus. This allows for re-use of existing user interfaces.
- Menus: Access to menus from the parent application. It is also possible to copy an entire menu structure of an application to quickly recreate the parent, while making desired changes to specific menu entries.
-- Functions: Parent functions can be accessed directly, while access to relations & attributes from within functions in the child application is also enabled. To keep applications upgrade safe as much as possible, always use the provided placeholders when referencing entities in functions. This will not protect against deletions (see below) but will keep your application running when entities are renamed.
+- Functions: Parent functions can be accessed directly, while access to relations & attributes from within functions in the child application is also enabled. To keep applications upgrade safe, always use the provided placeholders when referencing entities in functions. This will not protect against deletions (see below) but will keep your application running when entities are renamed.
Extension scenarios
Depending on the specific case, different approaches to application extension are possible.
@@ -566,14 +588,14 @@
Application transfers
Exporting applications
Applications are exported on the Builder start page. To start the export process you select the desired application and run an export check. This check will compare the current state of the application to the last known version. If any changes exist, a new version must be created before an export is possible. By creating a new version target instances understand that there are changes to apply. In order to export any changes, an application must be set to 'export changes' in the admin UI for applications. You should only do this for your own applications.
Important notice: When you build on applications from other authors, these are dependencies that are automatically exported together with your application. To protect against accidentally making changes to applications from others, all applications are in the state 'do not export changes' by default. While in this state, the last imported version of the application is exported and not the potentially changed version, accessible in the Builder. If you decide to export new versions for applications built by others, you potentially risk loss of your changes as well as data loss, when a new version from the original author is installed. You can safely build on other applications without making direct changes.
-
Ultimately, the export will generate a compressed zip file, which includes your application and all its dependencies. Within the zip file, each application is represented as a *.json file, which contains the corresponding schema and a signature, created with your private key. Applications that were not changed are exported with their original schema and signature.
+
Ultimately, the export will generate a compressed zip file, which includes your application and all its dependencies. Within the zip file, each application is represented as a *.json file, which contains the corresponding application structure and a signature, created with your private key. Applications that were not changed are exported as they were with their original signature.
Importing applications
Applications can be imported to REI3 instances in two ways:
- By importing an application package file.
- By importing an application from a repository.
-
When you attempt to import your application into another REI3 instance, the signature of the application schema is checked against a list of trusted public keys. Only applications which signature can be successfully compared to these trusted public keys can be imported. You can add your public key to any REI3 instance within the admin UI.
+
When you attempt to import your application into another REI3 instance, the signature of the application is checked against a list of trusted public keys. Only applications which signature can be successfully compared to these trusted public keys can be imported. You can add your public key to any REI3 instance within the admin UI.
Applications are either installed or updated when they are imported. All changes made to the applications or their dependencies are automatically applied by REI3. Should there be a problem, the entire import process is reverted even if multiple applications were affected. Import issues can be checked inside the admin UI by increasing the log level for 'transfers' and repeating the import.
Hosting a repository
When running many local or offline REI3 instances, it can be sensible to host your own REI3 application repository. It is also possible to host your own repository in the cloud, accessible to anyone. This is done by setting up a REI3 instance and installing the 'REI3 repository' application; this is the same application we use to host the central REI3 repository.
diff --git a/www/docs/en_us_builder_pics/admin_config.png b/www/docs/en_us_builder_pics/admin_config.png
new file mode 100644
index 00000000..66994ca4
Binary files /dev/null and b/www/docs/en_us_builder_pics/admin_config.png differ
diff --git a/www/docs/en_us_builder_pics/attributes.png b/www/docs/en_us_builder_pics/attributes.png
new file mode 100644
index 00000000..526e09b4
Binary files /dev/null and b/www/docs/en_us_builder_pics/attributes.png differ
diff --git a/www/docs/en_us_builder_pics/form.png b/www/docs/en_us_builder_pics/form.png
new file mode 100644
index 00000000..fefdfe0c
Binary files /dev/null and b/www/docs/en_us_builder_pics/form.png differ
diff --git a/www/docs/en_us_builder_pics/forms.png b/www/docs/en_us_builder_pics/forms.png
new file mode 100644
index 00000000..942c5669
Binary files /dev/null and b/www/docs/en_us_builder_pics/forms.png differ
diff --git a/www/docs/en_us_builder_pics/function.png b/www/docs/en_us_builder_pics/function.png
new file mode 100644
index 00000000..25d41db2
Binary files /dev/null and b/www/docs/en_us_builder_pics/function.png differ
diff --git a/www/docs/en_us_builder_pics/login_form.png b/www/docs/en_us_builder_pics/login_form.png
new file mode 100644
index 00000000..8ee50ef6
Binary files /dev/null and b/www/docs/en_us_builder_pics/login_form.png differ
diff --git a/www/docs/en_us_builder_pics/menus.png b/www/docs/en_us_builder_pics/menus.png
new file mode 100644
index 00000000..a21e6da6
Binary files /dev/null and b/www/docs/en_us_builder_pics/menus.png differ
diff --git a/www/docs/en_us_builder_pics/nm_relationship.png b/www/docs/en_us_builder_pics/nm_relationship.png
new file mode 100644
index 00000000..e08b1dce
Binary files /dev/null and b/www/docs/en_us_builder_pics/nm_relationship.png differ
diff --git a/www/docs/en_us_builder_pics/policies.png b/www/docs/en_us_builder_pics/policies.png
new file mode 100644
index 00000000..0f8607a0
Binary files /dev/null and b/www/docs/en_us_builder_pics/policies.png differ
diff --git a/www/docs/en_us_builder_pics/presets.png b/www/docs/en_us_builder_pics/presets.png
new file mode 100644
index 00000000..c5236b2e
Binary files /dev/null and b/www/docs/en_us_builder_pics/presets.png differ
diff --git a/www/docs/en_us_builder_pics/relations.png b/www/docs/en_us_builder_pics/relations.png
new file mode 100644
index 00000000..0e9830a8
Binary files /dev/null and b/www/docs/en_us_builder_pics/relations.png differ
diff --git a/www/docs/en_us_builder_pics/repo_install.png b/www/docs/en_us_builder_pics/repo_install.png
new file mode 100644
index 00000000..81f4aa07
Binary files /dev/null and b/www/docs/en_us_builder_pics/repo_install.png differ
diff --git a/www/docs/en_us_builder_pics/start.png b/www/docs/en_us_builder_pics/start.png
new file mode 100644
index 00000000..7509263e
Binary files /dev/null and b/www/docs/en_us_builder_pics/start.png differ
diff --git a/www/docs/en_us_builder_pics/start_form.png b/www/docs/en_us_builder_pics/start_form.png
new file mode 100644
index 00000000..bb343c42
Binary files /dev/null and b/www/docs/en_us_builder_pics/start_form.png differ
diff --git a/www/docs/en_us_builder_pics/start_language.png b/www/docs/en_us_builder_pics/start_language.png
new file mode 100644
index 00000000..16813c13
Binary files /dev/null and b/www/docs/en_us_builder_pics/start_language.png differ
diff --git a/www/externals/vue.js b/www/externals/vue.js
index 1abc270f..4204a49d 100644
--- a/www/externals/vue.js
+++ b/www/externals/vue.js
@@ -931,8 +931,6 @@ var Vue = (function (exports) {
get: shallowReadonlyGet
});
- const toReactive = (value) => isObject(value) ? reactive(value) : value;
- const toReadonly = (value) => isObject(value) ? readonly(value) : value;
const toShallow = (value) => value;
const getProto = (v) => Reflect.getPrototypeOf(v);
function get$1(target, key, isReadonly = false, isShallow = false) {
@@ -1321,6 +1319,8 @@ var Vue = (function (exports) {
def(value, "__v_skip" /* SKIP */, true);
return value;
}
+ const toReactive = (value) => isObject(value) ? reactive(value) : value;
+ const toReadonly = (value) => isObject(value) ? readonly(value) : value;
function trackRefValue(ref) {
if (isTracking()) {
@@ -1350,7 +1350,6 @@ var Vue = (function (exports) {
}
}
}
- const convert = (val) => isObject(val) ? reactive(val) : val;
function isRef(r) {
return Boolean(r && r.__v_isRef === true);
}
@@ -1360,13 +1359,19 @@ var Vue = (function (exports) {
function shallowRef(value) {
return createRef(value, true);
}
+ function createRef(rawValue, shallow) {
+ if (isRef(rawValue)) {
+ return rawValue;
+ }
+ return new RefImpl(rawValue, shallow);
+ }
class RefImpl {
constructor(value, _shallow) {
this._shallow = _shallow;
this.dep = undefined;
this.__v_isRef = true;
this._rawValue = _shallow ? value : toRaw(value);
- this._value = _shallow ? value : convert(value);
+ this._value = _shallow ? value : toReactive(value);
}
get value() {
trackRefValue(this);
@@ -1376,17 +1381,11 @@ var Vue = (function (exports) {
newVal = this._shallow ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
- this._value = this._shallow ? newVal : convert(newVal);
+ this._value = this._shallow ? newVal : toReactive(newVal);
triggerRefValue(this, newVal);
}
}
}
- function createRef(rawValue, shallow) {
- if (isRef(rawValue)) {
- return rawValue;
- }
- return new RefImpl(rawValue, shallow);
- }
function triggerRef(ref) {
triggerRefValue(ref, ref.value );
}
@@ -1488,7 +1487,8 @@ var Vue = (function (exports) {
function computed(getterOrOptions, debugOptions) {
let getter;
let setter;
- if (isFunction(getterOrOptions)) {
+ const onlyGetter = isFunction(getterOrOptions);
+ if (onlyGetter) {
getter = getterOrOptions;
setter = () => {
console.warn('Write operation failed: computed value is readonly');
@@ -1499,7 +1499,7 @@ var Vue = (function (exports) {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
- const cRef = new ComputedRefImpl(getter, setter, isFunction(getterOrOptions) || !getterOrOptions.set);
+ const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter);
if (debugOptions) {
cRef.effect.onTrack = debugOptions.onTrack;
cRef.effect.onTrigger = debugOptions.onTrigger;
@@ -1516,14 +1516,7 @@ var Vue = (function (exports) {
// Note: for a component to be eligible for HMR it also needs the __hmrId option
// to be set so that its instances can be registered / removed.
{
- const globalObject = typeof global !== 'undefined'
- ? global
- : typeof self !== 'undefined'
- ? self
- : typeof window !== 'undefined'
- ? window
- : {};
- globalObject.__VUE_HMR_RUNTIME__ = {
+ getGlobalThis().__VUE_HMR_RUNTIME__ = {
createRecord: tryWrap(createRecord),
rerender: tryWrap(rerender),
reload: tryWrap(reload)
@@ -1534,19 +1527,22 @@ var Vue = (function (exports) {
const id = instance.type.__hmrId;
let record = map.get(id);
if (!record) {
- createRecord(id);
+ createRecord(id, instance.type);
record = map.get(id);
}
- record.add(instance);
+ record.instances.add(instance);
}
function unregisterHMR(instance) {
- map.get(instance.type.__hmrId).delete(instance);
+ map.get(instance.type.__hmrId).instances.delete(instance);
}
- function createRecord(id) {
+ function createRecord(id, initialDef) {
if (map.has(id)) {
return false;
}
- map.set(id, new Set());
+ map.set(id, {
+ initialDef: normalizeClassComponent(initialDef),
+ instances: new Set()
+ });
return true;
}
function normalizeClassComponent(component) {
@@ -1557,7 +1553,9 @@ var Vue = (function (exports) {
if (!record) {
return;
}
- [...record].forEach(instance => {
+ // update initial record (for not-yet-rendered component)
+ record.initialDef.render = newRender;
+ [...record.instances].forEach(instance => {
if (newRender) {
instance.render = newRender;
normalizeClassComponent(instance.type).render = newRender;
@@ -1574,17 +1572,16 @@ var Vue = (function (exports) {
if (!record)
return;
newComp = normalizeClassComponent(newComp);
+ // update initial def (for not-yet-rendered components)
+ updateComponentDef(record.initialDef, newComp);
// create a snapshot which avoids the set being mutated during updates
- const instances = [...record];
+ const instances = [...record.instances];
for (const instance of instances) {
const oldComp = normalizeClassComponent(instance.type);
if (!hmrDirtyComponents.has(oldComp)) {
// 1. Update existing comp definition to match new one
- extend(oldComp, newComp);
- for (const key in oldComp) {
- if (key !== '__file' && !(key in newComp)) {
- delete oldComp[key];
- }
+ if (oldComp !== record.initialDef) {
+ updateComponentDef(oldComp, newComp);
}
// 2. mark definition dirty. This forces the renderer to replace the
// component on patch.
@@ -1630,6 +1627,14 @@ var Vue = (function (exports) {
}
});
}
+ function updateComponentDef(oldComp, newComp) {
+ extend(oldComp, newComp);
+ for (const key in oldComp) {
+ if (key !== '__file' && !(key in newComp)) {
+ delete oldComp[key];
+ }
+ }
+ }
function tryWrap(fn) {
return (id, arg) => {
try {
@@ -1643,14 +1648,53 @@ var Vue = (function (exports) {
};
}
- function setDevtoolsHook(hook) {
+ let buffer = [];
+ let devtoolsNotInstalled = false;
+ function emit(event, ...args) {
+ if (exports.devtools) {
+ exports.devtools.emit(event, ...args);
+ }
+ else if (!devtoolsNotInstalled) {
+ buffer.push({ event, args });
+ }
+ }
+ function setDevtoolsHook(hook, target) {
exports.devtools = hook;
+ if (exports.devtools) {
+ exports.devtools.enabled = true;
+ buffer.forEach(({ event, args }) => exports.devtools.emit(event, ...args));
+ buffer = [];
+ }
+ else if (
+ // handle late devtools injection - only do this if we are in an actual
+ // browser environment to avoid the timer handle stalling test runner exit
+ // (#4815)
+ // eslint-disable-next-line no-restricted-globals
+ typeof window !== 'undefined' &&
+ !navigator.userAgent.includes('jsdom')) {
+ const replay = (target.__VUE_DEVTOOLS_HOOK_REPLAY__ =
+ target.__VUE_DEVTOOLS_HOOK_REPLAY__ || []);
+ replay.push((newHook) => {
+ setDevtoolsHook(newHook, target);
+ });
+ // clear buffer after 3s - the user probably doesn't have devtools installed
+ // at all, and keeping the buffer will cause memory leaks (#4738)
+ setTimeout(() => {
+ if (!exports.devtools) {
+ target.__VUE_DEVTOOLS_HOOK_REPLAY__ = null;
+ devtoolsNotInstalled = true;
+ buffer = [];
+ }
+ }, 3000);
+ }
+ else {
+ // non-browser env, assume not installed
+ devtoolsNotInstalled = true;
+ buffer = [];
+ }
}
function devtoolsInitApp(app, version) {
- // TODO queue if devtools is undefined
- if (!exports.devtools)
- return;
- exports.devtools.emit("app:init" /* APP_INIT */, app, version, {
+ emit("app:init" /* APP_INIT */, app, version, {
Fragment,
Text,
Comment,
@@ -1658,9 +1702,7 @@ var Vue = (function (exports) {
});
}
function devtoolsUnmountApp(app) {
- if (!exports.devtools)
- return;
- exports.devtools.emit("app:unmount" /* APP_UNMOUNT */, app);
+ emit("app:unmount" /* APP_UNMOUNT */, app);
}
const devtoolsComponentAdded = /*#__PURE__*/ createDevtoolsComponentHook("component:added" /* COMPONENT_ADDED */);
const devtoolsComponentUpdated =
@@ -1669,356 +1711,21 @@ var Vue = (function (exports) {
/*#__PURE__*/ createDevtoolsComponentHook("component:removed" /* COMPONENT_REMOVED */);
function createDevtoolsComponentHook(hook) {
return (component) => {
- if (!exports.devtools)
- return;
- exports.devtools.emit(hook, component.appContext.app, component.uid, component.parent ? component.parent.uid : undefined, component);
+ emit(hook, component.appContext.app, component.uid, component.parent ? component.parent.uid : undefined, component);
};
}
const devtoolsPerfStart = /*#__PURE__*/ createDevtoolsPerformanceHook("perf:start" /* PERFORMANCE_START */);
const devtoolsPerfEnd = /*#__PURE__*/ createDevtoolsPerformanceHook("perf:end" /* PERFORMANCE_END */);
function createDevtoolsPerformanceHook(hook) {
return (component, type, time) => {
- if (!exports.devtools)
- return;
- exports.devtools.emit(hook, component.appContext.app, component.uid, component, type, time);
+ emit(hook, component.appContext.app, component.uid, component, type, time);
};
}
function devtoolsComponentEmit(component, event, params) {
- if (!exports.devtools)
- return;
- exports.devtools.emit("component:emit" /* COMPONENT_EMIT */, component.appContext.app, component, event, params);
+ emit("component:emit" /* COMPONENT_EMIT */, component.appContext.app, component, event, params);
}
- const deprecationData = {
- ["GLOBAL_MOUNT" /* GLOBAL_MOUNT */]: {
- message: `The global app bootstrapping API has changed: vm.$mount() and the "el" ` +
- `option have been removed. Use createApp(RootComponent).mount() instead.`,
- link: `https://v3.vuejs.org/guide/migration/global-api.html#mounting-app-instance`
- },
- ["GLOBAL_MOUNT_CONTAINER" /* GLOBAL_MOUNT_CONTAINER */]: {
- message: `Vue detected directives on the mount container. ` +
- `In Vue 3, the container is no longer considered part of the template ` +
- `and will not be processed/replaced.`,
- link: `https://v3.vuejs.org/guide/migration/mount-changes.html`
- },
- ["GLOBAL_EXTEND" /* GLOBAL_EXTEND */]: {
- message: `Vue.extend() has been removed in Vue 3. ` +
- `Use defineComponent() instead.`,
- link: `https://v3.vuejs.org/api/global-api.html#definecomponent`
- },
- ["GLOBAL_PROTOTYPE" /* GLOBAL_PROTOTYPE */]: {
- message: `Vue.prototype is no longer available in Vue 3. ` +
- `Use app.config.globalProperties instead.`,
- link: `https://v3.vuejs.org/guide/migration/global-api.html#vue-prototype-replaced-by-config-globalproperties`
- },
- ["GLOBAL_SET" /* GLOBAL_SET */]: {
- message: `Vue.set() has been removed as it is no longer needed in Vue 3. ` +
- `Simply use native JavaScript mutations.`
- },
- ["GLOBAL_DELETE" /* GLOBAL_DELETE */]: {
- message: `Vue.delete() has been removed as it is no longer needed in Vue 3. ` +
- `Simply use native JavaScript mutations.`
- },
- ["GLOBAL_OBSERVABLE" /* GLOBAL_OBSERVABLE */]: {
- message: `Vue.observable() has been removed. ` +
- `Use \`import { reactive } from "vue"\` from Composition API instead.`,
- link: `https://v3.vuejs.org/api/basic-reactivity.html`
- },
- ["GLOBAL_PRIVATE_UTIL" /* GLOBAL_PRIVATE_UTIL */]: {
- message: `Vue.util has been removed. Please refactor to avoid its usage ` +
- `since it was an internal API even in Vue 2.`
- },
- ["CONFIG_SILENT" /* CONFIG_SILENT */]: {
- message: `config.silent has been removed because it is not good practice to ` +
- `intentionally suppress warnings. You can use your browser console's ` +
- `filter features to focus on relevant messages.`
- },
- ["CONFIG_DEVTOOLS" /* CONFIG_DEVTOOLS */]: {
- message: `config.devtools has been removed. To enable devtools for ` +
- `production, configure the __VUE_PROD_DEVTOOLS__ compile-time flag.`,
- link: `https://github.com/vuejs/vue-next/tree/master/packages/vue#bundler-build-feature-flags`
- },
- ["CONFIG_KEY_CODES" /* CONFIG_KEY_CODES */]: {
- message: `config.keyCodes has been removed. ` +
- `In Vue 3, you can directly use the kebab-case key names as v-on modifiers.`,
- link: `https://v3.vuejs.org/guide/migration/keycode-modifiers.html`
- },
- ["CONFIG_PRODUCTION_TIP" /* CONFIG_PRODUCTION_TIP */]: {
- message: `config.productionTip has been removed.`,
- link: `https://v3.vuejs.org/guide/migration/global-api.html#config-productiontip-removed`
- },
- ["CONFIG_IGNORED_ELEMENTS" /* CONFIG_IGNORED_ELEMENTS */]: {
- message: () => {
- let msg = `config.ignoredElements has been removed.`;
- if (isRuntimeOnly()) {
- msg += ` Pass the "isCustomElement" option to @vue/compiler-dom instead.`;
- }
- else {
- msg += ` Use config.isCustomElement instead.`;
- }
- return msg;
- },
- link: `https://v3.vuejs.org/guide/migration/global-api.html#config-ignoredelements-is-now-config-iscustomelement`
- },
- ["CONFIG_WHITESPACE" /* CONFIG_WHITESPACE */]: {
- // this warning is only relevant in the full build when using runtime
- // compilation, so it's put in the runtime compatConfig list.
- message: `Vue 3 compiler's whitespace option will default to "condense" instead of ` +
- `"preserve". To suppress this warning, provide an explicit value for ` +
- `\`config.compilerOptions.whitespace\`.`
- },
- ["CONFIG_OPTION_MERGE_STRATS" /* CONFIG_OPTION_MERGE_STRATS */]: {
- message: `config.optionMergeStrategies no longer exposes internal strategies. ` +
- `Use custom merge functions instead.`
- },
- ["INSTANCE_SET" /* INSTANCE_SET */]: {
- message: `vm.$set() has been removed as it is no longer needed in Vue 3. ` +
- `Simply use native JavaScript mutations.`
- },
- ["INSTANCE_DELETE" /* INSTANCE_DELETE */]: {
- message: `vm.$delete() has been removed as it is no longer needed in Vue 3. ` +
- `Simply use native JavaScript mutations.`
- },
- ["INSTANCE_DESTROY" /* INSTANCE_DESTROY */]: {
- message: `vm.$destroy() has been removed. Use app.unmount() instead.`,
- link: `https://v3.vuejs.org/api/application-api.html#unmount`
- },
- ["INSTANCE_EVENT_EMITTER" /* INSTANCE_EVENT_EMITTER */]: {
- message: `vm.$on/$once/$off() have been removed. ` +
- `Use an external event emitter library instead.`,
- link: `https://v3.vuejs.org/guide/migration/events-api.html`
- },
- ["INSTANCE_EVENT_HOOKS" /* INSTANCE_EVENT_HOOKS */]: {
- message: event => `"${event}" lifecycle events are no longer supported. From templates, ` +
- `use the "vnode" prefix instead of "hook:". For example, @${event} ` +
- `should be changed to @vnode-${event.slice(5)}. ` +
- `From JavaScript, use Composition API to dynamically register lifecycle ` +
- `hooks.`,
- link: `https://v3.vuejs.org/guide/migration/vnode-lifecycle-events.html`
- },
- ["INSTANCE_CHILDREN" /* INSTANCE_CHILDREN */]: {
- message: `vm.$children has been removed. Consider refactoring your logic ` +
- `to avoid relying on direct access to child components.`,
- link: `https://v3.vuejs.org/guide/migration/children.html`
- },
- ["INSTANCE_LISTENERS" /* INSTANCE_LISTENERS */]: {
- message: `vm.$listeners has been removed. In Vue 3, parent v-on listeners are ` +
- `included in vm.$attrs and it is no longer necessary to separately use ` +
- `v-on="$listeners" if you are already using v-bind="$attrs". ` +
- `(Note: the Vue 3 behavior only applies if this compat config is disabled)`,
- link: `https://v3.vuejs.org/guide/migration/listeners-removed.html`
- },
- ["INSTANCE_SCOPED_SLOTS" /* INSTANCE_SCOPED_SLOTS */]: {
- message: `vm.$scopedSlots has been removed. Use vm.$slots instead.`,
- link: `https://v3.vuejs.org/guide/migration/slots-unification.html`
- },
- ["INSTANCE_ATTRS_CLASS_STYLE" /* INSTANCE_ATTRS_CLASS_STYLE */]: {
- message: componentName => `Component <${componentName || 'Anonymous'}> has \`inheritAttrs: false\` but is ` +
- `relying on class/style fallthrough from parent. In Vue 3, class/style ` +
- `are now included in $attrs and will no longer fallthrough when ` +
- `inheritAttrs is false. If you are already using v-bind="$attrs" on ` +
- `component root it should render the same end result. ` +
- `If you are binding $attrs to a non-root element and expecting ` +
- `class/style to fallthrough on root, you will need to now manually bind ` +
- `them on root via :class="$attrs.class".`,
- link: `https://v3.vuejs.org/guide/migration/attrs-includes-class-style.html`
- },
- ["OPTIONS_DATA_FN" /* OPTIONS_DATA_FN */]: {
- message: `The "data" option can no longer be a plain object. ` +
- `Always use a function.`,
- link: `https://v3.vuejs.org/guide/migration/data-option.html`
- },
- ["OPTIONS_DATA_MERGE" /* OPTIONS_DATA_MERGE */]: {
- message: (key) => `Detected conflicting key "${key}" when merging data option values. ` +
- `In Vue 3, data keys are merged shallowly and will override one another.`,
- link: `https://v3.vuejs.org/guide/migration/data-option.html#mixin-merge-behavior-change`
- },
- ["OPTIONS_BEFORE_DESTROY" /* OPTIONS_BEFORE_DESTROY */]: {
- message: `\`beforeDestroy\` has been renamed to \`beforeUnmount\`.`
- },
- ["OPTIONS_DESTROYED" /* OPTIONS_DESTROYED */]: {
- message: `\`destroyed\` has been renamed to \`unmounted\`.`
- },
- ["WATCH_ARRAY" /* WATCH_ARRAY */]: {
- message: `"watch" option or vm.$watch on an array value will no longer ` +
- `trigger on array mutation unless the "deep" option is specified. ` +
- `If current usage is intended, you can disable the compat behavior and ` +
- `suppress this warning with:` +
- `\n\n configureCompat({ ${"WATCH_ARRAY" /* WATCH_ARRAY */}: false })\n`,
- link: `https://v3.vuejs.org/guide/migration/watch.html`
- },
- ["PROPS_DEFAULT_THIS" /* PROPS_DEFAULT_THIS */]: {
- message: (key) => `props default value function no longer has access to "this". The compat ` +
- `build only offers access to this.$options.` +
- `(found in prop "${key}")`,
- link: `https://v3.vuejs.org/guide/migration/props-default-this.html`
- },
- ["CUSTOM_DIR" /* CUSTOM_DIR */]: {
- message: (legacyHook, newHook) => `Custom directive hook "${legacyHook}" has been removed. ` +
- `Use "${newHook}" instead.`,
- link: `https://v3.vuejs.org/guide/migration/custom-directives.html`
- },
- ["V_FOR_REF" /* V_FOR_REF */]: {
- message: `Ref usage on v-for no longer creates array ref values in Vue 3. ` +
- `Consider using function refs or refactor to avoid ref usage altogether.`,
- link: `https://v3.vuejs.org/guide/migration/array-refs.html`
- },
- ["V_ON_KEYCODE_MODIFIER" /* V_ON_KEYCODE_MODIFIER */]: {
- message: `Using keyCode as v-on modifier is no longer supported. ` +
- `Use kebab-case key name modifiers instead.`,
- link: `https://v3.vuejs.org/guide/migration/keycode-modifiers.html`
- },
- ["ATTR_FALSE_VALUE" /* ATTR_FALSE_VALUE */]: {
- message: (name) => `Attribute "${name}" with v-bind value \`false\` will render ` +
- `${name}="false" instead of removing it in Vue 3. To remove the attribute, ` +
- `use \`null\` or \`undefined\` instead. If the usage is intended, ` +
- `you can disable the compat behavior and suppress this warning with:` +
- `\n\n configureCompat({ ${"ATTR_FALSE_VALUE" /* ATTR_FALSE_VALUE */}: false })\n`,
- link: `https://v3.vuejs.org/guide/migration/attribute-coercion.html`
- },
- ["ATTR_ENUMERATED_COERCION" /* ATTR_ENUMERATED_COERCION */]: {
- message: (name, value, coerced) => `Enumerated attribute "${name}" with v-bind value \`${value}\` will ` +
- `${value === null ? `be removed` : `render the value as-is`} instead of coercing the value to "${coerced}" in Vue 3. ` +
- `Always use explicit "true" or "false" values for enumerated attributes. ` +
- `If the usage is intended, ` +
- `you can disable the compat behavior and suppress this warning with:` +
- `\n\n configureCompat({ ${"ATTR_ENUMERATED_COERCION" /* ATTR_ENUMERATED_COERCION */}: false })\n`,
- link: `https://v3.vuejs.org/guide/migration/attribute-coercion.html`
- },
- ["TRANSITION_CLASSES" /* TRANSITION_CLASSES */]: {
- message: `` // this feature cannot be runtime-detected
- },
- ["TRANSITION_GROUP_ROOT" /* TRANSITION_GROUP_ROOT */]: {
- message: `
no longer renders a root element by ` +
- `default if no "tag" prop is specified. If you do not rely on the span ` +
- `for styling, you can disable the compat behavior and suppress this ` +
- `warning with:` +
- `\n\n configureCompat({ ${"TRANSITION_GROUP_ROOT" /* TRANSITION_GROUP_ROOT */}: false })\n`,
- link: `https://v3.vuejs.org/guide/migration/transition-group.html`
- },
- ["COMPONENT_ASYNC" /* COMPONENT_ASYNC */]: {
- message: (comp) => {
- const name = getComponentName(comp);
- return (`Async component${name ? ` <${name}>` : `s`} should be explicitly created via \`defineAsyncComponent()\` ` +
- `in Vue 3. Plain functions will be treated as functional components in ` +
- `non-compat build. If you have already migrated all async component ` +
- `usage and intend to use plain functions for functional components, ` +
- `you can disable the compat behavior and suppress this ` +
- `warning with:` +
- `\n\n configureCompat({ ${"COMPONENT_ASYNC" /* COMPONENT_ASYNC */}: false })\n`);
- },
- link: `https://v3.vuejs.org/guide/migration/async-components.html`
- },
- ["COMPONENT_FUNCTIONAL" /* COMPONENT_FUNCTIONAL */]: {
- message: (comp) => {
- const name = getComponentName(comp);
- return (`Functional component${name ? ` <${name}>` : `s`} should be defined as a plain function in Vue 3. The "functional" ` +
- `option has been removed. NOTE: Before migrating to use plain ` +
- `functions for functional components, first make sure that all async ` +
- `components usage have been migrated and its compat behavior has ` +
- `been disabled.`);
- },
- link: `https://v3.vuejs.org/guide/migration/functional-components.html`
- },
- ["COMPONENT_V_MODEL" /* COMPONENT_V_MODEL */]: {
- message: (comp) => {
- const configMsg = `opt-in to ` +
- `Vue 3 behavior on a per-component basis with \`compatConfig: { ${"COMPONENT_V_MODEL" /* COMPONENT_V_MODEL */}: false }\`.`;
- if (comp.props &&
- (isArray(comp.props)
- ? comp.props.includes('modelValue')
- : hasOwn(comp.props, 'modelValue'))) {
- return (`Component delcares "modelValue" prop, which is Vue 3 usage, but ` +
- `is running under Vue 2 compat v-model behavior. You can ${configMsg}`);
- }
- return (`v-model usage on component has changed in Vue 3. Component that expects ` +
- `to work with v-model should now use the "modelValue" prop and emit the ` +
- `"update:modelValue" event. You can update the usage and then ${configMsg}`);
- },
- link: `https://v3.vuejs.org/guide/migration/v-model.html`
- },
- ["RENDER_FUNCTION" /* RENDER_FUNCTION */]: {
- message: `Vue 3's render function API has changed. ` +
- `You can opt-in to the new API with:` +
- `\n\n configureCompat({ ${"RENDER_FUNCTION" /* RENDER_FUNCTION */}: false })\n` +
- `\n (This can also be done per-component via the "compatConfig" option.)`,
- link: `https://v3.vuejs.org/guide/migration/render-function-api.html`
- },
- ["FILTERS" /* FILTERS */]: {
- message: `filters have been removed in Vue 3. ` +
- `The "|" symbol will be treated as native JavaScript bitwise OR operator. ` +
- `Use method calls or computed properties instead.`,
- link: `https://v3.vuejs.org/guide/migration/filters.html`
- },
- ["PRIVATE_APIS" /* PRIVATE_APIS */]: {
- message: name => `"${name}" is a Vue 2 private API that no longer exists in Vue 3. ` +
- `If you are seeing this warning only due to a dependency, you can ` +
- `suppress this warning via { PRIVATE_APIS: 'supress-warning' }.`
- }
- };
- const instanceWarned = Object.create(null);
- const warnCount = Object.create(null);
- function warnDeprecation(key, instance, ...args) {
- instance = instance || getCurrentInstance();
- // check user config
- const config = getCompatConfigForKey(key, instance);
- if (config === 'suppress-warning') {
- return;
- }
- const dupKey = key + args.join('');
- let compId = instance && formatComponentName(instance, instance.type);
- if (compId === 'Anonymous' && instance) {
- compId = instance.uid;
- }
- // skip if the same warning is emitted for the same component type
- const componentDupKey = dupKey + compId;
- if (componentDupKey in instanceWarned) {
- return;
- }
- instanceWarned[componentDupKey] = true;
- // same warning, but different component. skip the long message and just
- // log the key and count.
- if (dupKey in warnCount) {
- warn$1(`(deprecation ${key}) (${++warnCount[dupKey] + 1})`);
- return;
- }
- warnCount[dupKey] = 0;
- const { message, link } = deprecationData[key];
- warn$1(`(deprecation ${key}) ${typeof message === 'function' ? message(...args) : message}${link ? `\n Details: ${link}` : ``}`);
- if (!isCompatEnabled(key, instance, true)) {
- console.error(`^ The above deprecation's compat behavior is disabled and will likely ` +
- `lead to runtime errors.`);
- }
- }
- const globalCompatConfig = {
- MODE: 2
- };
- function getCompatConfigForKey(key, instance) {
- const instanceConfig = instance && instance.type.compatConfig;
- if (instanceConfig && key in instanceConfig) {
- return instanceConfig[key];
- }
- return globalCompatConfig[key];
- }
- function isCompatEnabled(key, instance, enableForBuiltIn = false) {
- // skip compat for built-in components
- if (!enableForBuiltIn && instance && instance.type.__isBuiltIn) {
- return false;
- }
- const rawMode = getCompatConfigForKey('MODE', instance) || 2;
- const val = getCompatConfigForKey(key, instance);
- const mode = isFunction(rawMode)
- ? rawMode(instance && instance.type)
- : rawMode;
- if (mode === 2) {
- return val !== false;
- }
- else {
- return val === true || val === 'suppress-warning';
- }
- }
-
- function emit(instance, event, ...rawArgs) {
+ function emit$1(instance, event, ...rawArgs) {
const props = instance.vnode.props || EMPTY_OBJ;
{
const { emitsOptions, propsOptions: [propsOptions] } = instance;
@@ -2243,12 +1950,12 @@ var Vue = (function (exports) {
function renderComponentRoot(instance) {
const { type: Component, vnode, proxy, withProxy, props, propsOptions: [propsOptions], slots, attrs, emit, render, renderCache, data, setupState, ctx, inheritAttrs } = instance;
let result;
+ let fallthroughAttrs;
const prev = setCurrentRenderingInstance(instance);
{
accessedAttrs = false;
}
try {
- let fallthroughAttrs;
if (vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */) {
// withProxy is a proxy with a different `has` trap only for
// runtime-compiled render functions using `with` block.
@@ -2279,97 +1986,91 @@ var Vue = (function (exports) {
? attrs
: getFunctionalFallthrough(attrs);
}
- // attr merging
- // in dev mode, comments are preserved, and it's possible for a template
- // to have comments along side the root element which makes it a fragment
- let root = result;
- let setRoot = undefined;
- if (true &&
- result.patchFlag > 0 &&
- result.patchFlag & 2048 /* DEV_ROOT_FRAGMENT */) {
- ;
- [root, setRoot] = getChildRoot(result);
- }
- if (fallthroughAttrs && inheritAttrs !== false) {
- const keys = Object.keys(fallthroughAttrs);
- const { shapeFlag } = root;
- if (keys.length) {
- if (shapeFlag & (1 /* ELEMENT */ | 6 /* COMPONENT */)) {
- if (propsOptions && keys.some(isModelListener)) {
- // If a v-model listener (onUpdate:xxx) has a corresponding declared
- // prop, it indicates this component expects to handle v-model and
- // it should not fallthrough.
- // related: #1543, #1643, #1989
- fallthroughAttrs = filterModelListeners(fallthroughAttrs, propsOptions);
- }
- root = cloneVNode(root, fallthroughAttrs);
- }
- else if (true && !accessedAttrs && root.type !== Comment) {
- const allAttrs = Object.keys(attrs);
- const eventAttrs = [];
- const extraAttrs = [];
- for (let i = 0, l = allAttrs.length; i < l; i++) {
- const key = allAttrs[i];
- if (isOn(key)) {
- // ignore v-model handlers when they fail to fallthrough
- if (!isModelListener(key)) {
- // remove `on`, lowercase first letter to reflect event casing
- // accurately
- eventAttrs.push(key[2].toLowerCase() + key.slice(3));
- }
- }
- else {
- extraAttrs.push(key);
+ }
+ catch (err) {
+ blockStack.length = 0;
+ handleError(err, instance, 1 /* RENDER_FUNCTION */);
+ result = createVNode(Comment);
+ }
+ // attr merging
+ // in dev mode, comments are preserved, and it's possible for a template
+ // to have comments along side the root element which makes it a fragment
+ let root = result;
+ let setRoot = undefined;
+ if (result.patchFlag > 0 &&
+ result.patchFlag & 2048 /* DEV_ROOT_FRAGMENT */) {
+ [root, setRoot] = getChildRoot(result);
+ }
+ if (fallthroughAttrs && inheritAttrs !== false) {
+ const keys = Object.keys(fallthroughAttrs);
+ const { shapeFlag } = root;
+ if (keys.length) {
+ if (shapeFlag & (1 /* ELEMENT */ | 6 /* COMPONENT */)) {
+ if (propsOptions && keys.some(isModelListener)) {
+ // If a v-model listener (onUpdate:xxx) has a corresponding declared
+ // prop, it indicates this component expects to handle v-model and
+ // it should not fallthrough.
+ // related: #1543, #1643, #1989
+ fallthroughAttrs = filterModelListeners(fallthroughAttrs, propsOptions);
+ }
+ root = cloneVNode(root, fallthroughAttrs);
+ }
+ else if (!accessedAttrs && root.type !== Comment) {
+ const allAttrs = Object.keys(attrs);
+ const eventAttrs = [];
+ const extraAttrs = [];
+ for (let i = 0, l = allAttrs.length; i < l; i++) {
+ const key = allAttrs[i];
+ if (isOn(key)) {
+ // ignore v-model handlers when they fail to fallthrough
+ if (!isModelListener(key)) {
+ // remove `on`, lowercase first letter to reflect event casing
+ // accurately
+ eventAttrs.push(key[2].toLowerCase() + key.slice(3));
}
}
- if (extraAttrs.length) {
- warn$1(`Extraneous non-props attributes (` +
- `${extraAttrs.join(', ')}) ` +
- `were passed to component but could not be automatically inherited ` +
- `because component renders fragment or text root nodes.`);
- }
- if (eventAttrs.length) {
- warn$1(`Extraneous non-emits event listeners (` +
- `${eventAttrs.join(', ')}) ` +
- `were passed to component but could not be automatically inherited ` +
- `because component renders fragment or text root nodes. ` +
- `If the listener is intended to be a component custom event listener only, ` +
- `declare it using the "emits" option.`);
+ else {
+ extraAttrs.push(key);
}
}
+ if (extraAttrs.length) {
+ warn$1(`Extraneous non-props attributes (` +
+ `${extraAttrs.join(', ')}) ` +
+ `were passed to component but could not be automatically inherited ` +
+ `because component renders fragment or text root nodes.`);
+ }
+ if (eventAttrs.length) {
+ warn$1(`Extraneous non-emits event listeners (` +
+ `${eventAttrs.join(', ')}) ` +
+ `were passed to component but could not be automatically inherited ` +
+ `because component renders fragment or text root nodes. ` +
+ `If the listener is intended to be a component custom event listener only, ` +
+ `declare it using the "emits" option.`);
+ }
}
}
- if (false &&
- isCompatEnabled("INSTANCE_ATTRS_CLASS_STYLE" /* INSTANCE_ATTRS_CLASS_STYLE */, instance) &&
- vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */ &&
- root.shapeFlag & (1 /* ELEMENT */ | 6 /* COMPONENT */)) ;
- // inherit directives
- if (vnode.dirs) {
- if (true && !isElementRoot(root)) {
- warn$1(`Runtime directive used on component with non-element root node. ` +
- `The directives will not function as intended.`);
- }
- root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs;
- }
- // inherit transition data
- if (vnode.transition) {
- if (true && !isElementRoot(root)) {
- warn$1(`Component inside renders non-element root node ` +
- `that cannot be animated.`);
- }
- root.transition = vnode.transition;
- }
- if (true && setRoot) {
- setRoot(root);
+ }
+ // inherit directives
+ if (vnode.dirs) {
+ if (!isElementRoot(root)) {
+ warn$1(`Runtime directive used on component with non-element root node. ` +
+ `The directives will not function as intended.`);
}
- else {
- result = root;
+ root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs;
+ }
+ // inherit transition data
+ if (vnode.transition) {
+ if (!isElementRoot(root)) {
+ warn$1(`Component inside renders non-element root node ` +
+ `that cannot be animated.`);
}
+ root.transition = vnode.transition;
}
- catch (err) {
- blockStack.length = 0;
- handleError(err, instance, 1 /* RENDER_FUNCTION */);
- result = createVNode(Comment);
+ if (setRoot) {
+ setRoot(root);
+ }
+ else {
+ result = root;
}
setCurrentRenderingInstance(prev);
return result;
@@ -2904,8 +2605,8 @@ var Vue = (function (exports) {
function normalizeSuspenseSlot(s) {
let block;
if (isFunction(s)) {
- const isCompiledSlot = s._c;
- if (isCompiledSlot) {
+ const trackBlock = isBlockTreeEnabled && s._c;
+ if (trackBlock) {
// disableTracking: false
// allow block tracking for compiled slots
// (see ./componentRenderContext.ts)
@@ -2913,7 +2614,7 @@ var Vue = (function (exports) {
openBlock();
}
s = s();
- if (isCompiledSlot) {
+ if (trackBlock) {
s._d = true;
block = currentBlock;
closeBlock();
@@ -4778,7 +4479,7 @@ var Vue = (function (exports) {
[bar, this.y]
])
*/
- const isBuiltInDirective = /*#__PURE__*/ makeMap('bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text');
+ const isBuiltInDirective = /*#__PURE__*/ makeMap('bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo');
function validateDirectiveName(name) {
if (isBuiltInDirective(name)) {
warn$1('Do not use built-in directive ids as custom directive id: ' + name);
@@ -4969,7 +4670,7 @@ var Vue = (function (exports) {
app._instance = vnode.component;
devtoolsInitApp(app, version);
}
- return vnode.component.proxy;
+ return getExposeProxy(vnode.component) || vnode.component.proxy;
}
else {
warn$1(`App has already been mounted.\n` +
@@ -5175,14 +4876,14 @@ var Vue = (function (exports) {
for (const key in props) {
if ((forcePatchValue && key.endsWith('value')) ||
(isOn(key) && !isReservedProp(key))) {
- patchProp(el, key, null, props[key]);
+ patchProp(el, key, null, props[key], false, undefined, parentComponent);
}
}
}
else if (props.onClick) {
// Fast path for click listeners (which is most often) to avoid
// iterating through props.
- patchProp(el, 'onClick', null, props.onClick);
+ patchProp(el, 'onClick', null, props.onClick, false, undefined, parentComponent);
}
}
// vnode / directive hooks
@@ -5395,10 +5096,10 @@ var Vue = (function (exports) {
}
// implementation
function baseCreateRenderer(options, createHydrationFns) {
+ const target = getGlobalThis();
+ target.__VUE__ = true;
{
- const target = getGlobalThis();
- target.__VUE__ = true;
- setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__);
+ setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__, target);
}
const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, createElement: hostCreateElement, createText: hostCreateText, createComment: hostCreateComment, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, setScopeId: hostSetScopeId = NOOP, cloneNode: hostCloneNode, insertStaticContent: hostInsertStaticContent } = options;
// Note: functions inside this closure should use `const xxx = () => {}`
@@ -7009,7 +6710,11 @@ var Vue = (function (exports) {
return Component;
}
if (warnMissing && !res) {
- warn$1(`Failed to resolve ${type.slice(0, -1)}: ${name}`);
+ const extra = type === COMPONENTS
+ ? `\nIf this is a native custom element, make sure to exclude it from ` +
+ `component resolution via compilerOptions.isCustomElement.`
+ : ``;
+ warn$1(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`);
}
return res;
}
@@ -7864,17 +7569,19 @@ var Vue = (function (exports) {
function exposeSetupStateOnRenderContext(instance) {
const { ctx, setupState } = instance;
Object.keys(toRaw(setupState)).forEach(key => {
- if (!setupState.__isScriptSetup && (key[0] === '$' || key[0] === '_')) {
- warn$1(`setup() return property ${JSON.stringify(key)} should not start with "$" or "_" ` +
- `which are reserved prefixes for Vue internals.`);
- return;
+ if (!setupState.__isScriptSetup) {
+ if (key[0] === '$' || key[0] === '_') {
+ warn$1(`setup() return property ${JSON.stringify(key)} should not start with "$" or "_" ` +
+ `which are reserved prefixes for Vue internals.`);
+ return;
+ }
+ Object.defineProperty(ctx, key, {
+ enumerable: true,
+ configurable: true,
+ get: () => setupState[key],
+ set: NOOP
+ });
}
- Object.defineProperty(ctx, key, {
- enumerable: true,
- configurable: true,
- get: () => setupState[key],
- set: NOOP
- });
});
}
@@ -7954,7 +7661,7 @@ var Vue = (function (exports) {
instance.ctx = createDevRenderContext(instance);
}
instance.root = parent ? parent.root : instance;
- instance.emit = emit.bind(null, instance);
+ instance.emit = emit$1.bind(null, instance);
// apply custom element special handling
if (vnode.ce) {
vnode.ce(instance);
@@ -8108,9 +7815,11 @@ var Vue = (function (exports) {
function finishComponentSetup(instance, isSSR, skipOptions) {
const Component = instance.type;
// template / render function normalization
+ // could be already set when returned from setup()
if (!instance.render) {
- // could be set from setup()
- if (compile && !Component.render) {
+ // only do on-the-fly compile if not in SSR - SSR on-the-fly compliation
+ // is done by server-renderer
+ if (!isSSR && compile && !Component.render) {
const template = Component.template;
if (template) {
{
@@ -8618,11 +8327,18 @@ var Vue = (function (exports) {
// 2. If a component is unmounted during a parent component's update,
// its update can be skipped.
queue.sort((a, b) => getId(a) - getId(b));
+ // conditional usage of checkRecursiveUpdate must be determined out of
+ // try ... catch block since Rollup by default de-optimizes treeshaking
+ // inside try-catch. This can leave all warning code unshaked. Although
+ // they would get eventually shaken by a minifier like terser, some minifiers
+ // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
+ const check = (job) => checkRecursiveUpdates(seen, job)
+ ;
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex];
if (job && job.active !== false) {
- if (true && checkRecursiveUpdates(seen, job)) {
+ if (true && check(job)) {
continue;
}
// console.log(`running:`, job.id)
@@ -8886,7 +8602,7 @@ var Vue = (function (exports) {
return cur;
};
}
- function traverse(value, seen = new Set()) {
+ function traverse(value, seen) {
if (!isObject(value) || value["__v_skip" /* SKIP */]) {
return value;
}
@@ -8993,15 +8709,21 @@ var Vue = (function (exports) {
* only.
* @internal
*/
- function mergeDefaults(
- // the base props is compiler-generated and guaranteed to be in this shape.
- props, defaults) {
+ function mergeDefaults(raw, defaults) {
+ const props = isArray(raw)
+ ? raw.reduce((normalized, p) => ((normalized[p] = {}), normalized), {})
+ : raw;
for (const key in defaults) {
- const val = props[key];
- if (val) {
- val.default = defaults[key];
+ const opt = props[key];
+ if (opt) {
+ if (isArray(opt) || isFunction(opt)) {
+ props[key] = { type: opt, default: defaults[key] };
+ }
+ else {
+ opt.default = defaults[key];
+ }
}
- else if (val === null) {
+ else if (opt === null) {
props[key] = { default: defaults[key] };
}
else {
@@ -9010,6 +8732,23 @@ var Vue = (function (exports) {
}
return props;
}
+ /**
+ * Used to create a proxy for the rest element when destructuring props with
+ * defineProps().
+ * @internal
+ */
+ function createPropsRestProxy(props, excludedKeys) {
+ const ret = {};
+ for (const key in props) {
+ if (!excludedKeys.includes(key)) {
+ Object.defineProperty(ret, key, {
+ enumerable: true,
+ get: () => props[key]
+ });
+ }
+ }
+ return ret;
+ }
/**
* `