@ -0,0 +1,7 @@ |
|||
.DS_Store |
|||
node_modules |
|||
node_modules_win |
|||
package-lock.json |
|||
yarn.lock |
|||
release/ |
|||
dist/ |
|||
@ -0,0 +1,4 @@ |
|||
Contributors |
|||
=== |
|||
|
|||
* Michael Shanks - [@mikebudi](https://github.com/mikebudi) |
|||
@ -0,0 +1,22 @@ |
|||
### Contributing to budibase-core |
|||
|
|||
* The contributors are listed in [AUTHORS.md](https://github.com/budibase/builder/blob/master/AUTHORS.md) (add yourself). |
|||
|
|||
* This project uses a modified version of the MPLv2 license, see [LICENSE](https://github.com/budibase/builder/blob/master/LICENSE.md). |
|||
|
|||
* We use the [C4 (Collective Code Construction Contract)](https://rfc.zeromq.org/spec:42/C4/) process for contributions. |
|||
Please read this if you are unfamiliar with it. |
|||
|
|||
* Please maintain the existing code style. |
|||
|
|||
* Please try to keep your commits small and focussed. |
|||
|
|||
* If the project diverges from your branch, please rebase instead of merging. This makes the commit graph easier to read. |
|||
|
|||
#### p.S... |
|||
|
|||
I am using contribution guidelines from the fantastic [ZeroMQ](https://github.com/zeromq) community. If you are interested why, it's because I believe in the ethos laid out by this community, and written about in depth in the book ["Social Architecture"](https://www.amazon.com/Social-Architecture-Building-line-Communities/dp/1533112452) by Pieter Hintjens. |
|||
|
|||
I am very much open to evolving this to suit our needs. |
|||
|
|||
Love from [Mike](https://github.com/mikebudi). |
|||
@ -0,0 +1,661 @@ |
|||
GNU AFFERO GENERAL PUBLIC LICENSE |
|||
Version 3, 19 November 2007 |
|||
|
|||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> |
|||
Everyone is permitted to copy and distribute verbatim copies |
|||
of this license document, but changing it is not allowed. |
|||
|
|||
Preamble |
|||
|
|||
The GNU Affero General Public License is a free, copyleft license for |
|||
software and other kinds of works, specifically designed to ensure |
|||
cooperation with the community in the case of network server software. |
|||
|
|||
The licenses for most software and other practical works are designed |
|||
to take away your freedom to share and change the works. By contrast, |
|||
our General Public Licenses are intended to guarantee your freedom to |
|||
share and change all versions of a program--to make sure it remains free |
|||
software for all its users. |
|||
|
|||
When we speak of free software, we are referring to freedom, not |
|||
price. Our General Public Licenses are designed to make sure that you |
|||
have the freedom to distribute copies of free software (and charge for |
|||
them if you wish), that you receive source code or can get it if you |
|||
want it, that you can change the software or use pieces of it in new |
|||
free programs, and that you know you can do these things. |
|||
|
|||
Developers that use our General Public Licenses protect your rights |
|||
with two steps: (1) assert copyright on the software, and (2) offer |
|||
you this License which gives you legal permission to copy, distribute |
|||
and/or modify the software. |
|||
|
|||
A secondary benefit of defending all users' freedom is that |
|||
improvements made in alternate versions of the program, if they |
|||
receive widespread use, become available for other developers to |
|||
incorporate. Many developers of free software are heartened and |
|||
encouraged by the resulting cooperation. However, in the case of |
|||
software used on network servers, this result may fail to come about. |
|||
The GNU General Public License permits making a modified version and |
|||
letting the public access it on a server without ever releasing its |
|||
source code to the public. |
|||
|
|||
The GNU Affero General Public License is designed specifically to |
|||
ensure that, in such cases, the modified source code becomes available |
|||
to the community. It requires the operator of a network server to |
|||
provide the source code of the modified version running there to the |
|||
users of that server. Therefore, public use of a modified version, on |
|||
a publicly accessible server, gives the public access to the source |
|||
code of the modified version. |
|||
|
|||
An older license, called the Affero General Public License and |
|||
published by Affero, was designed to accomplish similar goals. This is |
|||
a different license, not a version of the Affero GPL, but Affero has |
|||
released a new version of the Affero GPL which permits relicensing under |
|||
this license. |
|||
|
|||
The precise terms and conditions for copying, distribution and |
|||
modification follow. |
|||
|
|||
TERMS AND CONDITIONS |
|||
|
|||
0. Definitions. |
|||
|
|||
"This License" refers to version 3 of the GNU Affero General Public License. |
|||
|
|||
"Copyright" also means copyright-like laws that apply to other kinds of |
|||
works, such as semiconductor masks. |
|||
|
|||
"The Program" refers to any copyrightable work licensed under this |
|||
License. Each licensee is addressed as "you". "Licensees" and |
|||
"recipients" may be individuals or organizations. |
|||
|
|||
To "modify" a work means to copy from or adapt all or part of the work |
|||
in a fashion requiring copyright permission, other than the making of an |
|||
exact copy. The resulting work is called a "modified version" of the |
|||
earlier work or a work "based on" the earlier work. |
|||
|
|||
A "covered work" means either the unmodified Program or a work based |
|||
on the Program. |
|||
|
|||
To "propagate" a work means to do anything with it that, without |
|||
permission, would make you directly or secondarily liable for |
|||
infringement under applicable copyright law, except executing it on a |
|||
computer or modifying a private copy. Propagation includes copying, |
|||
distribution (with or without modification), making available to the |
|||
public, and in some countries other activities as well. |
|||
|
|||
To "convey" a work means any kind of propagation that enables other |
|||
parties to make or receive copies. Mere interaction with a user through |
|||
a computer network, with no transfer of a copy, is not conveying. |
|||
|
|||
An interactive user interface displays "Appropriate Legal Notices" |
|||
to the extent that it includes a convenient and prominently visible |
|||
feature that (1) displays an appropriate copyright notice, and (2) |
|||
tells the user that there is no warranty for the work (except to the |
|||
extent that warranties are provided), that licensees may convey the |
|||
work under this License, and how to view a copy of this License. If |
|||
the interface presents a list of user commands or options, such as a |
|||
menu, a prominent item in the list meets this criterion. |
|||
|
|||
1. Source Code. |
|||
|
|||
The "source code" for a work means the preferred form of the work |
|||
for making modifications to it. "Object code" means any non-source |
|||
form of a work. |
|||
|
|||
A "Standard Interface" means an interface that either is an official |
|||
standard defined by a recognized standards body, or, in the case of |
|||
interfaces specified for a particular programming language, one that |
|||
is widely used among developers working in that language. |
|||
|
|||
The "System Libraries" of an executable work include anything, other |
|||
than the work as a whole, that (a) is included in the normal form of |
|||
packaging a Major Component, but which is not part of that Major |
|||
Component, and (b) serves only to enable use of the work with that |
|||
Major Component, or to implement a Standard Interface for which an |
|||
implementation is available to the public in source code form. A |
|||
"Major Component", in this context, means a major essential component |
|||
(kernel, window system, and so on) of the specific operating system |
|||
(if any) on which the executable work runs, or a compiler used to |
|||
produce the work, or an object code interpreter used to run it. |
|||
|
|||
The "Corresponding Source" for a work in object code form means all |
|||
the source code needed to generate, install, and (for an executable |
|||
work) run the object code and to modify the work, including scripts to |
|||
control those activities. However, it does not include the work's |
|||
System Libraries, or general-purpose tools or generally available free |
|||
programs which are used unmodified in performing those activities but |
|||
which are not part of the work. For example, Corresponding Source |
|||
includes interface definition files associated with source files for |
|||
the work, and the source code for shared libraries and dynamically |
|||
linked subprograms that the work is specifically designed to require, |
|||
such as by intimate data communication or control flow between those |
|||
subprograms and other parts of the work. |
|||
|
|||
The Corresponding Source need not include anything that users |
|||
can regenerate automatically from other parts of the Corresponding |
|||
Source. |
|||
|
|||
The Corresponding Source for a work in source code form is that |
|||
same work. |
|||
|
|||
2. Basic Permissions. |
|||
|
|||
All rights granted under this License are granted for the term of |
|||
copyright on the Program, and are irrevocable provided the stated |
|||
conditions are met. This License explicitly affirms your unlimited |
|||
permission to run the unmodified Program. The output from running a |
|||
covered work is covered by this License only if the output, given its |
|||
content, constitutes a covered work. This License acknowledges your |
|||
rights of fair use or other equivalent, as provided by copyright law. |
|||
|
|||
You may make, run and propagate covered works that you do not |
|||
convey, without conditions so long as your license otherwise remains |
|||
in force. You may convey covered works to others for the sole purpose |
|||
of having them make modifications exclusively for you, or provide you |
|||
with facilities for running those works, provided that you comply with |
|||
the terms of this License in conveying all material for which you do |
|||
not control copyright. Those thus making or running the covered works |
|||
for you must do so exclusively on your behalf, under your direction |
|||
and control, on terms that prohibit them from making any copies of |
|||
your copyrighted material outside their relationship with you. |
|||
|
|||
Conveying under any other circumstances is permitted solely under |
|||
the conditions stated below. Sublicensing is not allowed; section 10 |
|||
makes it unnecessary. |
|||
|
|||
3. Protecting Users' Legal Rights From Anti-Circumvention Law. |
|||
|
|||
No covered work shall be deemed part of an effective technological |
|||
measure under any applicable law fulfilling obligations under article |
|||
11 of the WIPO copyright treaty adopted on 20 December 1996, or |
|||
similar laws prohibiting or restricting circumvention of such |
|||
measures. |
|||
|
|||
When you convey a covered work, you waive any legal power to forbid |
|||
circumvention of technological measures to the extent such circumvention |
|||
is effected by exercising rights under this License with respect to |
|||
the covered work, and you disclaim any intention to limit operation or |
|||
modification of the work as a means of enforcing, against the work's |
|||
users, your or third parties' legal rights to forbid circumvention of |
|||
technological measures. |
|||
|
|||
4. Conveying Verbatim Copies. |
|||
|
|||
You may convey verbatim copies of the Program's source code as you |
|||
receive it, in any medium, provided that you conspicuously and |
|||
appropriately publish on each copy an appropriate copyright notice; |
|||
keep intact all notices stating that this License and any |
|||
non-permissive terms added in accord with section 7 apply to the code; |
|||
keep intact all notices of the absence of any warranty; and give all |
|||
recipients a copy of this License along with the Program. |
|||
|
|||
You may charge any price or no price for each copy that you convey, |
|||
and you may offer support or warranty protection for a fee. |
|||
|
|||
5. Conveying Modified Source Versions. |
|||
|
|||
You may convey a work based on the Program, or the modifications to |
|||
produce it from the Program, in the form of source code under the |
|||
terms of section 4, provided that you also meet all of these conditions: |
|||
|
|||
a) The work must carry prominent notices stating that you modified |
|||
it, and giving a relevant date. |
|||
|
|||
b) The work must carry prominent notices stating that it is |
|||
released under this License and any conditions added under section |
|||
7. This requirement modifies the requirement in section 4 to |
|||
"keep intact all notices". |
|||
|
|||
c) You must license the entire work, as a whole, under this |
|||
License to anyone who comes into possession of a copy. This |
|||
License will therefore apply, along with any applicable section 7 |
|||
additional terms, to the whole of the work, and all its parts, |
|||
regardless of how they are packaged. This License gives no |
|||
permission to license the work in any other way, but it does not |
|||
invalidate such permission if you have separately received it. |
|||
|
|||
d) If the work has interactive user interfaces, each must display |
|||
Appropriate Legal Notices; however, if the Program has interactive |
|||
interfaces that do not display Appropriate Legal Notices, your |
|||
work need not make them do so. |
|||
|
|||
A compilation of a covered work with other separate and independent |
|||
works, which are not by their nature extensions of the covered work, |
|||
and which are not combined with it such as to form a larger program, |
|||
in or on a volume of a storage or distribution medium, is called an |
|||
"aggregate" if the compilation and its resulting copyright are not |
|||
used to limit the access or legal rights of the compilation's users |
|||
beyond what the individual works permit. Inclusion of a covered work |
|||
in an aggregate does not cause this License to apply to the other |
|||
parts of the aggregate. |
|||
|
|||
6. Conveying Non-Source Forms. |
|||
|
|||
You may convey a covered work in object code form under the terms |
|||
of sections 4 and 5, provided that you also convey the |
|||
machine-readable Corresponding Source under the terms of this License, |
|||
in one of these ways: |
|||
|
|||
a) Convey the object code in, or embodied in, a physical product |
|||
(including a physical distribution medium), accompanied by the |
|||
Corresponding Source fixed on a durable physical medium |
|||
customarily used for software interchange. |
|||
|
|||
b) Convey the object code in, or embodied in, a physical product |
|||
(including a physical distribution medium), accompanied by a |
|||
written offer, valid for at least three years and valid for as |
|||
long as you offer spare parts or customer support for that product |
|||
model, to give anyone who possesses the object code either (1) a |
|||
copy of the Corresponding Source for all the software in the |
|||
product that is covered by this License, on a durable physical |
|||
medium customarily used for software interchange, for a price no |
|||
more than your reasonable cost of physically performing this |
|||
conveying of source, or (2) access to copy the |
|||
Corresponding Source from a network server at no charge. |
|||
|
|||
c) Convey individual copies of the object code with a copy of the |
|||
written offer to provide the Corresponding Source. This |
|||
alternative is allowed only occasionally and noncommercially, and |
|||
only if you received the object code with such an offer, in accord |
|||
with subsection 6b. |
|||
|
|||
d) Convey the object code by offering access from a designated |
|||
place (gratis or for a charge), and offer equivalent access to the |
|||
Corresponding Source in the same way through the same place at no |
|||
further charge. You need not require recipients to copy the |
|||
Corresponding Source along with the object code. If the place to |
|||
copy the object code is a network server, the Corresponding Source |
|||
may be on a different server (operated by you or a third party) |
|||
that supports equivalent copying facilities, provided you maintain |
|||
clear directions next to the object code saying where to find the |
|||
Corresponding Source. Regardless of what server hosts the |
|||
Corresponding Source, you remain obligated to ensure that it is |
|||
available for as long as needed to satisfy these requirements. |
|||
|
|||
e) Convey the object code using peer-to-peer transmission, provided |
|||
you inform other peers where the object code and Corresponding |
|||
Source of the work are being offered to the general public at no |
|||
charge under subsection 6d. |
|||
|
|||
A separable portion of the object code, whose source code is excluded |
|||
from the Corresponding Source as a System Library, need not be |
|||
included in conveying the object code work. |
|||
|
|||
A "User Product" is either (1) a "consumer product", which means any |
|||
tangible personal property which is normally used for personal, family, |
|||
or household purposes, or (2) anything designed or sold for incorporation |
|||
into a dwelling. In determining whether a product is a consumer product, |
|||
doubtful cases shall be resolved in favor of coverage. For a particular |
|||
product received by a particular user, "normally used" refers to a |
|||
typical or common use of that class of product, regardless of the status |
|||
of the particular user or of the way in which the particular user |
|||
actually uses, or expects or is expected to use, the product. A product |
|||
is a consumer product regardless of whether the product has substantial |
|||
commercial, industrial or non-consumer uses, unless such uses represent |
|||
the only significant mode of use of the product. |
|||
|
|||
"Installation Information" for a User Product means any methods, |
|||
procedures, authorization keys, or other information required to install |
|||
and execute modified versions of a covered work in that User Product from |
|||
a modified version of its Corresponding Source. The information must |
|||
suffice to ensure that the continued functioning of the modified object |
|||
code is in no case prevented or interfered with solely because |
|||
modification has been made. |
|||
|
|||
If you convey an object code work under this section in, or with, or |
|||
specifically for use in, a User Product, and the conveying occurs as |
|||
part of a transaction in which the right of possession and use of the |
|||
User Product is transferred to the recipient in perpetuity or for a |
|||
fixed term (regardless of how the transaction is characterized), the |
|||
Corresponding Source conveyed under this section must be accompanied |
|||
by the Installation Information. But this requirement does not apply |
|||
if neither you nor any third party retains the ability to install |
|||
modified object code on the User Product (for example, the work has |
|||
been installed in ROM). |
|||
|
|||
The requirement to provide Installation Information does not include a |
|||
requirement to continue to provide support service, warranty, or updates |
|||
for a work that has been modified or installed by the recipient, or for |
|||
the User Product in which it has been modified or installed. Access to a |
|||
network may be denied when the modification itself materially and |
|||
adversely affects the operation of the network or violates the rules and |
|||
protocols for communication across the network. |
|||
|
|||
Corresponding Source conveyed, and Installation Information provided, |
|||
in accord with this section must be in a format that is publicly |
|||
documented (and with an implementation available to the public in |
|||
source code form), and must require no special password or key for |
|||
unpacking, reading or copying. |
|||
|
|||
7. Additional Terms. |
|||
|
|||
"Additional permissions" are terms that supplement the terms of this |
|||
License by making exceptions from one or more of its conditions. |
|||
Additional permissions that are applicable to the entire Program shall |
|||
be treated as though they were included in this License, to the extent |
|||
that they are valid under applicable law. If additional permissions |
|||
apply only to part of the Program, that part may be used separately |
|||
under those permissions, but the entire Program remains governed by |
|||
this License without regard to the additional permissions. |
|||
|
|||
When you convey a copy of a covered work, you may at your option |
|||
remove any additional permissions from that copy, or from any part of |
|||
it. (Additional permissions may be written to require their own |
|||
removal in certain cases when you modify the work.) You may place |
|||
additional permissions on material, added by you to a covered work, |
|||
for which you have or can give appropriate copyright permission. |
|||
|
|||
Notwithstanding any other provision of this License, for material you |
|||
add to a covered work, you may (if authorized by the copyright holders of |
|||
that material) supplement the terms of this License with terms: |
|||
|
|||
a) Disclaiming warranty or limiting liability differently from the |
|||
terms of sections 15 and 16 of this License; or |
|||
|
|||
b) Requiring preservation of specified reasonable legal notices or |
|||
author attributions in that material or in the Appropriate Legal |
|||
Notices displayed by works containing it; or |
|||
|
|||
c) Prohibiting misrepresentation of the origin of that material, or |
|||
requiring that modified versions of such material be marked in |
|||
reasonable ways as different from the original version; or |
|||
|
|||
d) Limiting the use for publicity purposes of names of licensors or |
|||
authors of the material; or |
|||
|
|||
e) Declining to grant rights under trademark law for use of some |
|||
trade names, trademarks, or service marks; or |
|||
|
|||
f) Requiring indemnification of licensors and authors of that |
|||
material by anyone who conveys the material (or modified versions of |
|||
it) with contractual assumptions of liability to the recipient, for |
|||
any liability that these contractual assumptions directly impose on |
|||
those licensors and authors. |
|||
|
|||
All other non-permissive additional terms are considered "further |
|||
restrictions" within the meaning of section 10. If the Program as you |
|||
received it, or any part of it, contains a notice stating that it is |
|||
governed by this License along with a term that is a further |
|||
restriction, you may remove that term. If a license document contains |
|||
a further restriction but permits relicensing or conveying under this |
|||
License, you may add to a covered work material governed by the terms |
|||
of that license document, provided that the further restriction does |
|||
not survive such relicensing or conveying. |
|||
|
|||
If you add terms to a covered work in accord with this section, you |
|||
must place, in the relevant source files, a statement of the |
|||
additional terms that apply to those files, or a notice indicating |
|||
where to find the applicable terms. |
|||
|
|||
Additional terms, permissive or non-permissive, may be stated in the |
|||
form of a separately written license, or stated as exceptions; |
|||
the above requirements apply either way. |
|||
|
|||
8. Termination. |
|||
|
|||
You may not propagate or modify a covered work except as expressly |
|||
provided under this License. Any attempt otherwise to propagate or |
|||
modify it is void, and will automatically terminate your rights under |
|||
this License (including any patent licenses granted under the third |
|||
paragraph of section 11). |
|||
|
|||
However, if you cease all violation of this License, then your |
|||
license from a particular copyright holder is reinstated (a) |
|||
provisionally, unless and until the copyright holder explicitly and |
|||
finally terminates your license, and (b) permanently, if the copyright |
|||
holder fails to notify you of the violation by some reasonable means |
|||
prior to 60 days after the cessation. |
|||
|
|||
Moreover, your license from a particular copyright holder is |
|||
reinstated permanently if the copyright holder notifies you of the |
|||
violation by some reasonable means, this is the first time you have |
|||
received notice of violation of this License (for any work) from that |
|||
copyright holder, and you cure the violation prior to 30 days after |
|||
your receipt of the notice. |
|||
|
|||
Termination of your rights under this section does not terminate the |
|||
licenses of parties who have received copies or rights from you under |
|||
this License. If your rights have been terminated and not permanently |
|||
reinstated, you do not qualify to receive new licenses for the same |
|||
material under section 10. |
|||
|
|||
9. Acceptance Not Required for Having Copies. |
|||
|
|||
You are not required to accept this License in order to receive or |
|||
run a copy of the Program. Ancillary propagation of a covered work |
|||
occurring solely as a consequence of using peer-to-peer transmission |
|||
to receive a copy likewise does not require acceptance. However, |
|||
nothing other than this License grants you permission to propagate or |
|||
modify any covered work. These actions infringe copyright if you do |
|||
not accept this License. Therefore, by modifying or propagating a |
|||
covered work, you indicate your acceptance of this License to do so. |
|||
|
|||
10. Automatic Licensing of Downstream Recipients. |
|||
|
|||
Each time you convey a covered work, the recipient automatically |
|||
receives a license from the original licensors, to run, modify and |
|||
propagate that work, subject to this License. You are not responsible |
|||
for enforcing compliance by third parties with this License. |
|||
|
|||
An "entity transaction" is a transaction transferring control of an |
|||
organization, or substantially all assets of one, or subdividing an |
|||
organization, or merging organizations. If propagation of a covered |
|||
work results from an entity transaction, each party to that |
|||
transaction who receives a copy of the work also receives whatever |
|||
licenses to the work the party's predecessor in interest had or could |
|||
give under the previous paragraph, plus a right to possession of the |
|||
Corresponding Source of the work from the predecessor in interest, if |
|||
the predecessor has it or can get it with reasonable efforts. |
|||
|
|||
You may not impose any further restrictions on the exercise of the |
|||
rights granted or affirmed under this License. For example, you may |
|||
not impose a license fee, royalty, or other charge for exercise of |
|||
rights granted under this License, and you may not initiate litigation |
|||
(including a cross-claim or counterclaim in a lawsuit) alleging that |
|||
any patent claim is infringed by making, using, selling, offering for |
|||
sale, or importing the Program or any portion of it. |
|||
|
|||
11. Patents. |
|||
|
|||
A "contributor" is a copyright holder who authorizes use under this |
|||
License of the Program or a work on which the Program is based. The |
|||
work thus licensed is called the contributor's "contributor version". |
|||
|
|||
A contributor's "essential patent claims" are all patent claims |
|||
owned or controlled by the contributor, whether already acquired or |
|||
hereafter acquired, that would be infringed by some manner, permitted |
|||
by this License, of making, using, or selling its contributor version, |
|||
but do not include claims that would be infringed only as a |
|||
consequence of further modification of the contributor version. For |
|||
purposes of this definition, "control" includes the right to grant |
|||
patent sublicenses in a manner consistent with the requirements of |
|||
this License. |
|||
|
|||
Each contributor grants you a non-exclusive, worldwide, royalty-free |
|||
patent license under the contributor's essential patent claims, to |
|||
make, use, sell, offer for sale, import and otherwise run, modify and |
|||
propagate the contents of its contributor version. |
|||
|
|||
In the following three paragraphs, a "patent license" is any express |
|||
agreement or commitment, however denominated, not to enforce a patent |
|||
(such as an express permission to practice a patent or covenant not to |
|||
sue for patent infringement). To "grant" such a patent license to a |
|||
party means to make such an agreement or commitment not to enforce a |
|||
patent against the party. |
|||
|
|||
If you convey a covered work, knowingly relying on a patent license, |
|||
and the Corresponding Source of the work is not available for anyone |
|||
to copy, free of charge and under the terms of this License, through a |
|||
publicly available network server or other readily accessible means, |
|||
then you must either (1) cause the Corresponding Source to be so |
|||
available, or (2) arrange to deprive yourself of the benefit of the |
|||
patent license for this particular work, or (3) arrange, in a manner |
|||
consistent with the requirements of this License, to extend the patent |
|||
license to downstream recipients. "Knowingly relying" means you have |
|||
actual knowledge that, but for the patent license, your conveying the |
|||
covered work in a country, or your recipient's use of the covered work |
|||
in a country, would infringe one or more identifiable patents in that |
|||
country that you have reason to believe are valid. |
|||
|
|||
If, pursuant to or in connection with a single transaction or |
|||
arrangement, you convey, or propagate by procuring conveyance of, a |
|||
covered work, and grant a patent license to some of the parties |
|||
receiving the covered work authorizing them to use, propagate, modify |
|||
or convey a specific copy of the covered work, then the patent license |
|||
you grant is automatically extended to all recipients of the covered |
|||
work and works based on it. |
|||
|
|||
A patent license is "discriminatory" if it does not include within |
|||
the scope of its coverage, prohibits the exercise of, or is |
|||
conditioned on the non-exercise of one or more of the rights that are |
|||
specifically granted under this License. You may not convey a covered |
|||
work if you are a party to an arrangement with a third party that is |
|||
in the business of distributing software, under which you make payment |
|||
to the third party based on the extent of your activity of conveying |
|||
the work, and under which the third party grants, to any of the |
|||
parties who would receive the covered work from you, a discriminatory |
|||
patent license (a) in connection with copies of the covered work |
|||
conveyed by you (or copies made from those copies), or (b) primarily |
|||
for and in connection with specific products or compilations that |
|||
contain the covered work, unless you entered into that arrangement, |
|||
or that patent license was granted, prior to 28 March 2007. |
|||
|
|||
Nothing in this License shall be construed as excluding or limiting |
|||
any implied license or other defenses to infringement that may |
|||
otherwise be available to you under applicable patent law. |
|||
|
|||
12. No Surrender of Others' Freedom. |
|||
|
|||
If conditions are imposed on you (whether by court order, agreement or |
|||
otherwise) that contradict the conditions of this License, they do not |
|||
excuse you from the conditions of this License. If you cannot convey a |
|||
covered work so as to satisfy simultaneously your obligations under this |
|||
License and any other pertinent obligations, then as a consequence you may |
|||
not convey it at all. For example, if you agree to terms that obligate you |
|||
to collect a royalty for further conveying from those to whom you convey |
|||
the Program, the only way you could satisfy both those terms and this |
|||
License would be to refrain entirely from conveying the Program. |
|||
|
|||
13. Remote Network Interaction; Use with the GNU General Public License. |
|||
|
|||
Notwithstanding any other provision of this License, if you modify the |
|||
Program, your modified version must prominently offer all users |
|||
interacting with it remotely through a computer network (if your version |
|||
supports such interaction) an opportunity to receive the Corresponding |
|||
Source of your version by providing access to the Corresponding Source |
|||
from a network server at no charge, through some standard or customary |
|||
means of facilitating copying of software. This Corresponding Source |
|||
shall include the Corresponding Source for any work covered by version 3 |
|||
of the GNU General Public License that is incorporated pursuant to the |
|||
following paragraph. |
|||
|
|||
Notwithstanding any other provision of this License, you have |
|||
permission to link or combine any covered work with a work licensed |
|||
under version 3 of the GNU General Public License into a single |
|||
combined work, and to convey the resulting work. The terms of this |
|||
License will continue to apply to the part which is the covered work, |
|||
but the work with which it is combined will remain governed by version |
|||
3 of the GNU General Public License. |
|||
|
|||
14. Revised Versions of this License. |
|||
|
|||
The Free Software Foundation may publish revised and/or new versions of |
|||
the GNU Affero General Public License from time to time. Such new versions |
|||
will be similar in spirit to the present version, but may differ in detail to |
|||
address new problems or concerns. |
|||
|
|||
Each version is given a distinguishing version number. If the |
|||
Program specifies that a certain numbered version of the GNU Affero General |
|||
Public License "or any later version" applies to it, you have the |
|||
option of following the terms and conditions either of that numbered |
|||
version or of any later version published by the Free Software |
|||
Foundation. If the Program does not specify a version number of the |
|||
GNU Affero General Public License, you may choose any version ever published |
|||
by the Free Software Foundation. |
|||
|
|||
If the Program specifies that a proxy can decide which future |
|||
versions of the GNU Affero General Public License can be used, that proxy's |
|||
public statement of acceptance of a version permanently authorizes you |
|||
to choose that version for the Program. |
|||
|
|||
Later license versions may give you additional or different |
|||
permissions. However, no additional obligations are imposed on any |
|||
author or copyright holder as a result of your choosing to follow a |
|||
later version. |
|||
|
|||
15. Disclaimer of Warranty. |
|||
|
|||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY |
|||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT |
|||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY |
|||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, |
|||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM |
|||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF |
|||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION. |
|||
|
|||
16. Limitation of Liability. |
|||
|
|||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |
|||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS |
|||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY |
|||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE |
|||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF |
|||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD |
|||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), |
|||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF |
|||
SUCH DAMAGES. |
|||
|
|||
17. Interpretation of Sections 15 and 16. |
|||
|
|||
If the disclaimer of warranty and limitation of liability provided |
|||
above cannot be given local legal effect according to their terms, |
|||
reviewing courts shall apply local law that most closely approximates |
|||
an absolute waiver of all civil liability in connection with the |
|||
Program, unless a warranty or assumption of liability accompanies a |
|||
copy of the Program in return for a fee. |
|||
|
|||
END OF TERMS AND CONDITIONS |
|||
|
|||
How to Apply These Terms to Your New Programs |
|||
|
|||
If you develop a new program, and you want it to be of the greatest |
|||
possible use to the public, the best way to achieve this is to make it |
|||
free software which everyone can redistribute and change under these terms. |
|||
|
|||
To do so, attach the following notices to the program. It is safest |
|||
to attach them to the start of each source file to most effectively |
|||
state the exclusion of warranty; and each file should have at least |
|||
the "copyright" line and a pointer to where the full notice is found. |
|||
|
|||
<one line to give the program's name and a brief idea of what it does.> |
|||
Copyright (C) <year> <name of author> |
|||
|
|||
This program is free software: you can redistribute it and/or modify |
|||
it under the terms of the GNU Affero General Public License as published |
|||
by the Free Software Foundation, either version 3 of the License, or |
|||
(at your option) any later version. |
|||
|
|||
This program is distributed in the hope that it will be useful, |
|||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
GNU Affero General Public License for more details. |
|||
|
|||
You should have received a copy of the GNU Affero General Public License |
|||
along with this program. If not, see <https://www.gnu.org/licenses/>. |
|||
|
|||
Also add information on how to contact you by electronic and paper mail. |
|||
|
|||
If your software can interact with users remotely through a computer |
|||
network, you should also make sure that it provides a way for users to |
|||
get its source. For example, if your program is a web application, its |
|||
interface could display a "Source" link that leads users to an archive |
|||
of the code. There are many ways you could offer source, and different |
|||
solutions will be better for different programs; see section 13 for the |
|||
specific requirements. |
|||
|
|||
You should also get your employer (if you work as a programmer) or school, |
|||
if any, to sign a "copyright disclaimer" for the program, if necessary. |
|||
For more information on this, and how to apply and follow the GNU AGPL, see |
|||
<https://www.gnu.org/licenses/>. |
|||
@ -0,0 +1,6 @@ |
|||
## Get Started |
|||
|
|||
`npm install` |
|||
|
|||
Webpack dev server (port 8080): `npm run dev` |
|||
Build `npm run build` |
|||
@ -0,0 +1,40 @@ |
|||
{ |
|||
"name": "budibase-builder", |
|||
"version": "0.0.1", |
|||
"license": "AGPL-3.0", |
|||
"scripts": { |
|||
"build": "rollup -c", |
|||
"start": "rollup -c -w" |
|||
}, |
|||
"dependencies": { |
|||
"@nx-js/compiler-util": "^2.0.0", |
|||
"budibase-core": "file:../core/dist", |
|||
"date-fns": "^1.29.0", |
|||
"feather-icons": "^4.21.0", |
|||
"flatpickr": "^4.5.7", |
|||
"lodash": "^4.17.11", |
|||
"lunr": "^2.3.5", |
|||
"safe-buffer": "^5.1.2", |
|||
"shortid": "^2.2.8", |
|||
"string_decoder": "^1.2.0", |
|||
"uikit": "^3.1.5" |
|||
}, |
|||
"devDependencies": { |
|||
"browser-sync": "^2.26.7", |
|||
"http-proxy-middleware": "^0.19.1", |
|||
"npm-run-all": "^4.1.5", |
|||
"rollup": "^1.12.0", |
|||
"rollup-plugin-alias": "^1.5.2", |
|||
"rollup-plugin-browsersync": "^1.0.0", |
|||
"rollup-plugin-commonjs": "^10.0.0", |
|||
"rollup-plugin-copy": "^3.0.0", |
|||
"rollup-plugin-livereload": "^1.0.0", |
|||
"rollup-plugin-node-builtins": "^2.1.2", |
|||
"rollup-plugin-node-globals": "^1.4.0", |
|||
"rollup-plugin-node-resolve": "^5.2.0", |
|||
"rollup-plugin-svelte": "^5.0.3", |
|||
"rollup-plugin-terser": "^4.0.4", |
|||
"rollup-plugin-url": "^2.2.2", |
|||
"svelte": "^3.0.0" |
|||
} |
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
import svelte from 'rollup-plugin-svelte'; |
|||
import resolve from 'rollup-plugin-node-resolve'; |
|||
import commonjs from 'rollup-plugin-commonjs'; |
|||
import url from 'rollup-plugin-url'; |
|||
import livereload from 'rollup-plugin-livereload'; |
|||
import { terser } from 'rollup-plugin-terser'; |
|||
import builtins from 'rollup-plugin-node-builtins'; |
|||
import nodeglobals from 'rollup-plugin-node-globals'; |
|||
import copy from 'rollup-plugin-copy'; |
|||
import browsersync from "rollup-plugin-browsersync"; |
|||
import proxy from "http-proxy-middleware"; |
|||
|
|||
const apiProxy = proxy('/api', { |
|||
target: 'http://localhost:4001/_builder', |
|||
logLevel: "debug", |
|||
changeOrigin: true, |
|||
cookieDomainRewrite: true, |
|||
onProxyReq(proxyReq) { |
|||
if (proxyReq.getHeader("origin")) { |
|||
proxyReq.setHeader("origin", target) |
|||
} |
|||
} |
|||
}); |
|||
|
|||
const production = !process.env.ROLLUP_WATCH; |
|||
|
|||
const lodash_fp_exports = ["union", "reduce", "isUndefined", "cloneDeep", "split", "some", "map", "filter", "isEmpty", "countBy", "includes", "last", "find", "constant", |
|||
"take", "first", "intersection", "mapValues", "isNull", "has", "isNumber", "isString", "isBoolean", "isDate", "isArray", "isObject", "clone", "values", "keyBy", |
|||
"keys", "orderBy", "concat", "reverse", "difference", "merge", "flatten", "each", "pull", "join", "defaultCase", "uniqBy", "every", "uniqWith", "isFunction", "groupBy", |
|||
"differenceBy", "intersectionBy", "isEqual", "max", "sortBy", "assign"]; |
|||
|
|||
const lodash_exports = ["toNumber", "flow", "isArray", "join", "replace", "trim", "dropRight", "takeRight", "head", "isUndefined", "isNull", "isNaN", "reduce", "isEmpty", |
|||
"constant", "tail", "includes", "startsWith", "findIndex", "isInteger", "isDate", "isString", "split", "clone", "keys", "isFunction", "merge", "has", "isBoolean", "isNumber", |
|||
"isObjectLike", "assign", "some", "each", "find", "orderBy", "union", "cloneDeep"]; |
|||
|
|||
const writeoptions = {dest: "output/output.js"}; |
|||
|
|||
export default { |
|||
input: 'src/main.js', |
|||
output: { |
|||
sourcemap: true, |
|||
format: 'iife', |
|||
name: 'app', |
|||
file: 'dist/bundle.js' |
|||
}, |
|||
plugins: [ |
|||
copy({ |
|||
targets: [ |
|||
{ src: 'src/index.html', dest: 'dist' }, |
|||
{ src: 'src/favicon.png', dest: 'dist' } |
|||
] |
|||
}), |
|||
|
|||
svelte({ |
|||
// enable run-time checks when not in production
|
|||
dev: !production, |
|||
include: 'src/**/*.svelte', |
|||
// we'll extract any component CSS out into
|
|||
// a separate file — better for performance
|
|||
css: css => { |
|||
css.write('dist/bundle.css'); |
|||
} |
|||
}), |
|||
|
|||
// If you have external dependencies installed from
|
|||
// npm, you'll most likely need these plugins. In
|
|||
// some cases you'll need additional configuration —
|
|||
// consult the documentation for details:
|
|||
// https://github.com/rollup/rollup-plugin-commonjs
|
|||
resolve({ |
|||
browser: true, |
|||
dedupe: importee => importee === 'svelte' || importee.startsWith('svelte/') |
|||
|
|||
}), |
|||
commonjs({ |
|||
namedExports: { |
|||
"lodash/fp": lodash_fp_exports, |
|||
"lodash":lodash_exports, |
|||
"shortid": ["generate"] |
|||
}, |
|||
include: /node_modules/ |
|||
}), |
|||
url({ |
|||
limit: 0, |
|||
include: ["**/*.woff2", "**/*.png"], |
|||
fileName: "[dirname][name][extname]", |
|||
emitFiles: true |
|||
}), |
|||
url({ |
|||
limit: 0, |
|||
include: ["**/*.css"], |
|||
fileName: "[name][extname]", |
|||
emitFiles: true |
|||
}), |
|||
builtins(), |
|||
nodeglobals(), |
|||
|
|||
// Watch the `dist` directory and refresh the
|
|||
// browser on changes when not in production
|
|||
!production && livereload('dist'), |
|||
!production && browsersync({ |
|||
server: 'dist', |
|||
middleware: [apiProxy] |
|||
}), |
|||
|
|||
// If we're building for production (npm run build
|
|||
// instead of npm run dev), minify
|
|||
production && terser() |
|||
], |
|||
watch: { |
|||
clearScreen: false |
|||
} |
|||
}; |
|||
@ -0,0 +1,38 @@ |
|||
<script> |
|||
|
|||
import NoPackage from "./NoPackage.svelte"; |
|||
import PackageRoot from "./PackageRoot.svelte"; |
|||
import {database, initialise} from "./builderStore"; |
|||
import { onMount } from 'svelte'; |
|||
|
|||
let init = initialise(); |
|||
|
|||
</script> |
|||
|
|||
<main> |
|||
|
|||
{#await init} |
|||
|
|||
<h1>loading</h1> |
|||
|
|||
{:then result} |
|||
{#if $database.hasAppPackage} |
|||
<PackageRoot /> |
|||
{/if} |
|||
|
|||
{#if !$database.hasAppPackage} |
|||
<NoPackage /> |
|||
{/if} |
|||
|
|||
{:catch err} |
|||
<h1 style="color:red">{err}</h1> |
|||
{/await} |
|||
</main> |
|||
|
|||
<style> |
|||
main { |
|||
height: 100%; |
|||
width: 100%; |
|||
font-family: "Lato", Helvetica, Arial, sans-serif; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,55 @@ |
|||
<script> |
|||
|
|||
import Button from "./common/Button.svelte" |
|||
|
|||
let errors = []; |
|||
|
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<div class="inner"> |
|||
<img src="./assets/budibase-logo.png" class="logo" alt="budibase logo"/> |
|||
<div> |
|||
|
|||
<div> |
|||
<h4 style="margin-bottom: 20px">What would you like not to do?</h4> |
|||
<Button color="primary" |
|||
class="option"> |
|||
Create a New Package |
|||
</Button> |
|||
<Button color="primary-outline" |
|||
class="option"> |
|||
Import a Package |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
.root { |
|||
position: fixed; |
|||
margin: 0 auto; |
|||
text-align: center; |
|||
top: 20%; |
|||
/*color: #333333; |
|||
background-color: #fdfdfd;*/ |
|||
width:100%; |
|||
} |
|||
|
|||
.inner { |
|||
display:inline-block; |
|||
margin: auto; |
|||
} |
|||
|
|||
.logo { |
|||
width: 300px; |
|||
margin-bottom: 40px; |
|||
} |
|||
|
|||
.root :global(.option) { |
|||
width:250px; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,49 @@ |
|||
<script> |
|||
|
|||
import Nav from "./nav/Nav.svelte"; |
|||
import Database from "./database/DatabaseRoot.svelte" ; |
|||
import UserInterface from "./userInterface/UserInterfaceRoot.svelte" ; |
|||
import ActionsAndTriggers from "./actionsAndTriggers/ActionsAndTriggersRoot.svelte" ; |
|||
import AccessLevels from "./accessLevels/AccessLevelsRoot.svelte" ; |
|||
import ComingSoon from "./common/ComingSoon.svelte"; |
|||
|
|||
import {database} from "./builderStore"; |
|||
|
|||
export let navWidth = "50px"; |
|||
|
|||
|
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<Nav width={navWidth} /> |
|||
<div class="content" |
|||
style="width: calc(100% - {navWidth}); left: {navWidth}"> |
|||
{#if $database.activeNav === "database"} |
|||
<Database /> |
|||
{:else if $database.activeNav === "actions"} |
|||
<ActionsAndTriggers /> |
|||
{:else if $database.activeNav === "access levels"} |
|||
<AccessLevels /> |
|||
{:else if $database.activeNav === "user interface"} |
|||
<UserInterface /> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
|
|||
<style> |
|||
.root { |
|||
height: 100%; |
|||
} |
|||
|
|||
|
|||
.content { |
|||
position: fixed; |
|||
height: 100%; |
|||
background-color: var(--white); |
|||
margin:0; |
|||
} |
|||
|
|||
|
|||
</style> |
|||
@ -0,0 +1,96 @@ |
|||
<script> |
|||
|
|||
import {cloneDeep, map, some, filter} from "lodash/fp"; |
|||
import Textbox from "../common/Textbox.svelte"; |
|||
import Checkbox from "../common/Checkbox.svelte"; |
|||
import ButtonGroup from "../common/ButtonGroup.svelte"; |
|||
import Button from "../common/Button.svelte"; |
|||
import {validateAccessLevels} from "../common/core"; |
|||
import ErrorsBox from "../common/ErrorsBox.svelte"; |
|||
|
|||
export let level; |
|||
export let allPermissions; |
|||
export let onFinished; |
|||
export let isNew; |
|||
export let allLevels; |
|||
export let hierarchy; |
|||
export let actions; |
|||
|
|||
let errors = []; |
|||
let clonedLevel = cloneDeep(level); |
|||
|
|||
const matchPermissions = (p1, p2) => |
|||
p1.type === p2.type |
|||
&& |
|||
((!p2.nodeKey && !p1.nodeKey) |
|||
|| p2.nodeKey === p1.nodeKey); |
|||
|
|||
const hasPermission = hasPerm => |
|||
some(p => matchPermissions(p, hasPerm)) |
|||
(clonedLevel.permissions); |
|||
|
|||
$: permissionMatrix = |
|||
map(p => ({permission:p, hasPermission: hasPermission(p)})) |
|||
(allPermissions) |
|||
|
|||
const getPermissionName = perm => |
|||
perm.nodeKey |
|||
? `${perm.type} - ${perm.nodeKey}` |
|||
: perm.type; |
|||
|
|||
const save = () => { |
|||
|
|||
const newLevels = |
|||
isNew |
|||
? [...allLevels, clonedLevel] |
|||
: [...filter(l => l.name !== level.name)(allLevels), clonedLevel]; |
|||
|
|||
errors = validateAccessLevels( |
|||
hierarchy, |
|||
actions, |
|||
newLevels |
|||
); |
|||
|
|||
if(errors.length > 0) return; |
|||
|
|||
onFinished(clonedLevel); |
|||
} |
|||
|
|||
const permissionChanged = perm => ev => { |
|||
const hasPermission = ev.target.checked; |
|||
|
|||
if(hasPermission) { |
|||
clonedLevel.permissions.push(perm); |
|||
} else { |
|||
clonedLevel.permissions = filter(p => !matchPermissions(p, perm)); |
|||
} |
|||
} |
|||
|
|||
</script> |
|||
|
|||
<div> |
|||
|
|||
<ErrorsBox {errors} /> |
|||
|
|||
<Textbox label="Name" bind:text={clonedLevel.name} /> |
|||
|
|||
{#each permissionMatrix as permission} |
|||
<div> |
|||
<Checkbox label={getPermissionName(permission.permission)} |
|||
checked={permission.hasPermission} |
|||
on:change={permissionChanged(permission.permission)} /> |
|||
</div> |
|||
{/each} |
|||
|
|||
|
|||
<ButtonGroup style="margin-top: 10px"> |
|||
<Button color="primary" grouped on:click={save}>Save</Button> |
|||
<Button color="secondary" grouped on:click={() => onFinished()}>Cancel</Button> |
|||
</ButtonGroup> |
|||
|
|||
|
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
</style> |
|||
@ -0,0 +1,116 @@ |
|||
<script> |
|||
|
|||
import ButtonGroup from "../common/ButtonGroup.svelte"; |
|||
import Button from "../common/Button.svelte"; |
|||
import {database} from "../builderStore"; |
|||
import {generateFullPermissions, getNewAccessLevel} from "../common/core"; |
|||
import getIcon from "../common/icon"; |
|||
import AccessLevelView from "./AccessLevelView.svelte"; |
|||
import Modal from "../common/Modal.svelte"; |
|||
|
|||
let editingLevel = null; |
|||
let editingLevelIsNew = false; |
|||
$: isEditing = (editingLevel !== null); |
|||
|
|||
let allPermissions = []; |
|||
database.subscribe(db => { |
|||
allPermissions = generateFullPermissions(db.hierarchy, db.actions); |
|||
}) |
|||
|
|||
let onLevelEdit = (level) => { |
|||
editingLevel = level; |
|||
editingLevelIsNew = false; |
|||
}; |
|||
|
|||
let onLevelCancel = () => { |
|||
editingAction = null; |
|||
}; |
|||
|
|||
let onLevelDelete = (level) => { |
|||
database.deleteLevel(level); |
|||
}; |
|||
|
|||
|
|||
let createNewLevel = () => { |
|||
editingLevelIsNew = true; |
|||
editingLevel = getNewAccessLevel(); |
|||
} |
|||
|
|||
let onEditingFinished = (level) => { |
|||
if(level) { |
|||
database.saveLevel(level, editingLevelIsNew, editingLevel); |
|||
} |
|||
editingLevel = null; |
|||
} |
|||
|
|||
const getPermissionsString = perms => { |
|||
return `${perms.length} / ${allPermissions.length}`; |
|||
} |
|||
|
|||
</script> |
|||
|
|||
<div class="root"> |
|||
|
|||
<ButtonGroup> |
|||
<Button grouped color="secondary" on:click={createNewLevel}>Create New Access Level</Button> |
|||
</ButtonGroup> |
|||
|
|||
{#if $database.accessLevels} |
|||
<table class="fields-table uk-table uk-table-small"> |
|||
<thead> |
|||
<tr> |
|||
<th>Name</th> |
|||
<th>Permissions</th> |
|||
<th></th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
{#each $database.accessLevels as level} |
|||
<tr> |
|||
<td >{level.name}</td> |
|||
<td >{getPermissionsString(level.permissions)}</td> |
|||
<td class="edit-button"> |
|||
<span on:click={() => onLevelEdit(level)}>{@html getIcon("edit")}</span> |
|||
<span on:click={() => onLevelDelete(level)}>{@html getIcon("trash")}</span> |
|||
</td> |
|||
</tr> |
|||
{/each} |
|||
</tbody> |
|||
</table> |
|||
{:else} |
|||
(no actions added) |
|||
{/if} |
|||
|
|||
|
|||
<Modal bind:isOpen={isEditing}> |
|||
{#if isEditing} |
|||
<AccessLevelView level={editingLevel} |
|||
allPermissions={allPermissions} |
|||
onFinished={onEditingFinished} |
|||
isNew={editingLevelIsNew} |
|||
allLevels={$database.accessLevels} |
|||
hierarchy={$database.hierarchy} |
|||
actions={$database.actions} /> |
|||
{/if} |
|||
</Modal> |
|||
|
|||
|
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
.root { |
|||
padding:10px; |
|||
} |
|||
|
|||
.edit-button { |
|||
cursor:pointer; |
|||
color: var(--white); |
|||
} |
|||
|
|||
tr:hover .edit-button { |
|||
color: var(--secondary75); |
|||
} |
|||
|
|||
|
|||
</style> |
|||
@ -0,0 +1,115 @@ |
|||
<script> |
|||
|
|||
import Textbox from "../common/Textbox.svelte"; |
|||
import Button from "../common/Button.svelte"; |
|||
import ButtonGroup from "../common/ButtonGroup.svelte"; |
|||
import {cloneDeep, filter, keys, |
|||
map, isUndefined} from "lodash/fp"; |
|||
import ErrorsBox from "../common/ErrorsBox.svelte"; |
|||
import {validateActions, chain} from "../common/core"; |
|||
import getIcon from "../common/icon"; |
|||
|
|||
export let action; |
|||
export let onFinished = (action) => {}; |
|||
export let allActions; |
|||
export let isNew = true; |
|||
|
|||
let optKey = ""; |
|||
let optValue = ""; |
|||
|
|||
let clonedAction = cloneDeep(action); |
|||
let initialOptions = chain(action.initialOptions, [ |
|||
keys, |
|||
map(k => ({key:k, value:action.initialOptions[k]})) |
|||
]); |
|||
let errors = []; |
|||
|
|||
const addNewOption = () => { |
|||
|
|||
if(optKey && optValue && isUndefined(clonedAction.initialOptions[optKey])) { |
|||
clonedAction.initialOptions[optKey] = optValue; |
|||
initialOptions = [...initialOptions, { |
|||
key:optKey, value: optValue |
|||
}]; |
|||
optKey = ""; |
|||
optValue = ""; |
|||
} |
|||
} |
|||
|
|||
const removeOption = (opt) => { |
|||
if(opt) { |
|||
delete clonedAction.initialOptions[opt.key] |
|||
initialOptions = chain(initialOptions, [ |
|||
filter(o => o.key !== opt.key) |
|||
]); |
|||
} |
|||
} |
|||
|
|||
const save = () => { |
|||
|
|||
const newActionsList = [ |
|||
...chain(allActions ,[filter(a => a !== action)]), |
|||
clonedAction] |
|||
|
|||
errors = chain(newActionsList ,[ |
|||
validateActions, |
|||
map(e => e.error) |
|||
]); |
|||
|
|||
if(errors.length === 0) |
|||
onFinished(clonedAction); |
|||
} |
|||
|
|||
const cancel = () => { |
|||
onFinished(); |
|||
} |
|||
|
|||
</script> |
|||
|
|||
<div class="root"> |
|||
|
|||
<ErrorsBox {errors} /> |
|||
|
|||
<Textbox label="Name" bind:text={clonedAction.name} /> |
|||
<Textbox label="Behaviour Source" bind:text={clonedAction.behaviourSource} /> |
|||
<Textbox label="Behaviour" bind:text={clonedAction.behaviourName} /> |
|||
|
|||
|
|||
<div class=" uk-form-stacked" style="margin-bottom: 20px"> |
|||
<label class="uk-form-label">Default Options</label> |
|||
<div class="uk-grid-small" uk-grid> |
|||
<input class="uk-input uk-width-1-4" placeholder="key" bind:value={optKey} > |
|||
<input class="uk-input uk-width-1-4" placeholder="value" bind:value={optValue} > |
|||
<Button color="primary-outline uk-width-1-4" on:click={addNewOption}>Add</Button> |
|||
</div> |
|||
<div style="margin-top: 10px"> |
|||
{#each initialOptions as option} |
|||
<span class="option-container">{option.key} : {option.value} <span style="font-size:10pt; cursor: pointer" on:click={() => removeOption(option)}>{@html getIcon("trash-2")}</span></span> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
|
|||
<ButtonGroup> |
|||
<Button color="primary" grouped on:click={save}>Save</Button> |
|||
<Button color="secondary" grouped on:click={cancel}>Cancel</Button> |
|||
</ButtonGroup> |
|||
|
|||
|
|||
</div> |
|||
|
|||
|
|||
<style> |
|||
|
|||
.root { |
|||
padding: 10px; |
|||
} |
|||
|
|||
.option-container { |
|||
border-style: dotted; |
|||
border-width: 1px; |
|||
border-color: var(--primary75); |
|||
padding: 3px; |
|||
margin-right: 5px; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,95 @@ |
|||
<script> |
|||
|
|||
import getIcon from "../common/icon"; |
|||
import {database} from "../builderStore"; |
|||
import Button from "../common/Button.svelte"; |
|||
import ButtonGroup from "../common/ButtonGroup.svelte"; |
|||
import ActionView from "./ActionView.svelte"; |
|||
import Modal from "../common/Modal.svelte"; |
|||
import {chain} from "../common/core"; |
|||
import {keys, map, join} from "lodash/fp"; |
|||
|
|||
export let editingActionIsNew = false; |
|||
export let editingAction = null; |
|||
export let onActionEdit = (action) => {}; |
|||
export let onActionDelete = (action) => {}; |
|||
export let onActionSave = (action) => {}; |
|||
export let onActionCancel = () => {}; |
|||
|
|||
$: isEditing = (editingAction !== null); |
|||
|
|||
let getDefaultOptionsHtml = defaultOptions => |
|||
chain(defaultOptions, [ |
|||
keys, |
|||
map(k => `<span style="color:var(--slate)">${k}: </span>${JSON.stringify(defaultOptions[k])}`), |
|||
join("<br>") |
|||
]); |
|||
|
|||
|
|||
let actionEditingFinished = (action) => { |
|||
|
|||
if(action) { |
|||
onActionSave(action) |
|||
} else { |
|||
onActionCancel(); |
|||
} |
|||
} |
|||
|
|||
</script> |
|||
|
|||
<h3>Actions</h3> |
|||
|
|||
{#if $database.actions} |
|||
<table class="fields-table uk-table uk-table-small"> |
|||
<thead> |
|||
<tr> |
|||
<th>Description</th> |
|||
<th>Behaviour Source</th> |
|||
<th>Behaviour Name</th> |
|||
<th>Default Options</th> |
|||
<th></th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
{#each $database.actions as action} |
|||
<tr> |
|||
<td >{action.name}</td> |
|||
<td >{action.behaviourSource}</td> |
|||
<td >{action.behaviourName}</td> |
|||
<td >{@html getDefaultOptionsHtml(action.initialOptions)}</td> |
|||
<td class="edit-button"> |
|||
<span on:click={() => onActionEdit(action)}>{@html getIcon("edit")}</span> |
|||
<span on:click={() => onActionDelete(action)}>{@html getIcon("trash")}</span> |
|||
</td> |
|||
</tr> |
|||
{/each} |
|||
</tbody> |
|||
</table> |
|||
{:else} |
|||
(no actions added) |
|||
{/if} |
|||
|
|||
|
|||
<Modal bind:isOpen={isEditing}> |
|||
{#if isEditing} |
|||
<ActionView action={editingAction} |
|||
allActions={$database.actions} |
|||
onFinished={actionEditingFinished} |
|||
isNew={editingActionIsNew}/> |
|||
{/if} |
|||
</Modal> |
|||
|
|||
|
|||
<style> |
|||
|
|||
.edit-button { |
|||
cursor:pointer; |
|||
color: var(--white); |
|||
} |
|||
|
|||
tr:hover .edit-button { |
|||
color: var(--secondary75); |
|||
} |
|||
|
|||
|
|||
</style> |
|||
@ -0,0 +1,115 @@ |
|||
<script> |
|||
|
|||
import getIcon from "../common/icon"; |
|||
import {database} from "../builderStore"; |
|||
import Button from "../common/Button.svelte"; |
|||
import ButtonGroup from "../common/ButtonGroup.svelte"; |
|||
import Actions from "./Actions.svelte"; |
|||
import Triggers from "./Triggers.svelte"; |
|||
import {getNewAction, getNewTrigger} from "../common/core"; |
|||
|
|||
let editingAction = null; |
|||
let editingActionIsNew = true; |
|||
let editingTrigger = null; |
|||
let editingTriggerIsNew = true; |
|||
|
|||
let getDefaultOptionsHtml = defaultOptions => |
|||
chain(defaultOptions, [ |
|||
keys, |
|||
map(k => `<span style="color:var(--slate)">${k}: </span>${JSON.parse(typeOptions[k])}`), |
|||
join("<br>") |
|||
]); |
|||
|
|||
let onActionEdit = (action) => { |
|||
editingAction = action; |
|||
editingActionIsNew = false; |
|||
} |
|||
|
|||
let newAction = () => { |
|||
editingAction = getNewAction(); |
|||
editingActionIsNew = true; |
|||
} |
|||
|
|||
let onActionDelete = (action) => { |
|||
database.deleteAction(action); |
|||
} |
|||
|
|||
let deleteTrigger = () => {} |
|||
|
|||
let editTrigger = (trigger) => { |
|||
editingTrigger = trigger; |
|||
editingTriggerIsNew = false; |
|||
} |
|||
|
|||
let newTrigger = () => { |
|||
editingTrigger = getNewTrigger(); |
|||
editingTriggerIsNew = true; |
|||
} |
|||
|
|||
let onActionSave = action => { |
|||
database.saveAction( |
|||
action, |
|||
editingActionIsNew, |
|||
editingAction); |
|||
|
|||
editingAction = null; |
|||
} |
|||
|
|||
let onActionCancel = () => { |
|||
editingAction = null; |
|||
} |
|||
|
|||
let onTriggerSave = trigger => { |
|||
database.saveTrigger( |
|||
trigger, |
|||
editingTriggerIsNew, |
|||
editingTrigger); |
|||
|
|||
editingTrigger = null; |
|||
} |
|||
|
|||
let onTriggerCancel = () => { |
|||
editingTrigger = null; |
|||
} |
|||
|
|||
let onTriggerEdit = (trigger) => { |
|||
editingTrigger = trigger; |
|||
editingTriggerIsNew = false; |
|||
} |
|||
|
|||
|
|||
let onTriggerDelete = (trigger) => { |
|||
database.deleteTrigger(trigger); |
|||
} |
|||
|
|||
</script> |
|||
|
|||
<div class="root"> |
|||
|
|||
<ButtonGroup> |
|||
<Button color="secondary" |
|||
grouped |
|||
on:click={newAction}>Create New Action</Button> |
|||
<Button color="secondary" |
|||
grouped |
|||
on:click={newTrigger}>Create New Trigger</Button> |
|||
</ButtonGroup> |
|||
|
|||
<Actions {editingActionIsNew} {editingAction} |
|||
{onActionEdit} {onActionDelete} {onActionSave} |
|||
{onActionCancel} /> |
|||
|
|||
<Triggers {editingTriggerIsNew} {editingTrigger} |
|||
{onTriggerEdit} {onTriggerDelete} {onTriggerSave} |
|||
{onTriggerCancel} /> |
|||
|
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
.root { |
|||
padding: 10px; |
|||
} |
|||
|
|||
|
|||
</style> |
|||
@ -0,0 +1,64 @@ |
|||
<script> |
|||
|
|||
import Textbox from "../common/Textbox.svelte"; |
|||
import Button from "../common/Button.svelte"; |
|||
import Dropdown from "../common/Dropdown.svelte"; |
|||
import ButtonGroup from "../common/ButtonGroup.svelte"; |
|||
import CodeArea from "../common/CodeArea.svelte"; |
|||
import {cloneDeep, filter, keys, some, |
|||
map, isUndefined} from "lodash/fp"; |
|||
import ErrorsBox from "../common/ErrorsBox.svelte"; |
|||
import {validateTriggers, chain, events} from "../common/core"; |
|||
import getIcon from "../common/icon"; |
|||
|
|||
export let trigger; |
|||
export let onFinished = (action) => {}; |
|||
export let allTriggers; |
|||
export let allActions; |
|||
export let isNew = true; |
|||
|
|||
let clonedTrigger = cloneDeep(trigger); |
|||
let errors = []; |
|||
$: actionNames = map(a => a.name)(allActions); |
|||
|
|||
let cancel = () => onFinished(); |
|||
let save = () => { |
|||
const newTriggersList = [ |
|||
...chain(allTriggers ,[filter(t => t !== trigger)]), |
|||
clonedTrigger] |
|||
|
|||
errors = validateTriggers(newTriggersList, allActions); |
|||
|
|||
const test = map(t =>(!t.actionName || some(a => a.name === t.actionName)(allActions)))(newTriggersList) |
|||
|
|||
if(errors.length === 0) |
|||
onFinished(clonedTrigger); |
|||
} |
|||
|
|||
</script> |
|||
|
|||
<div> |
|||
|
|||
<ErrorsBox {errors} style="margin-bottom:20px"/> |
|||
|
|||
<Dropdown label="Event" |
|||
options={["",...events]} |
|||
bind:selected={clonedTrigger.eventName} /> |
|||
<Dropdown label="Action" |
|||
options={["",...actionNames]} |
|||
bind:selected={clonedTrigger.actionName} /> |
|||
<CodeArea label="Condition (javascript)" |
|||
bind:text={clonedTrigger.condition} /> |
|||
<CodeArea label="Action Options Creator (javascript)" |
|||
bind:text={clonedTrigger.optionsCreator} /> |
|||
|
|||
<ButtonGroup> |
|||
<Button grouped on:click={save}>Save</Button> |
|||
<Button grouped on:click={cancel}>Cancel</Button> |
|||
</ButtonGroup> |
|||
|
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
</style> |
|||
@ -0,0 +1,84 @@ |
|||
<script> |
|||
|
|||
import {database} from "../builderStore"; |
|||
import getIcon from "../common/icon"; |
|||
import Button from "../common/Button.svelte"; |
|||
import Modal from "../common/Modal.svelte"; |
|||
import TriggerView from "./TriggerView.svelte"; |
|||
|
|||
|
|||
export let editingTrigger = null; |
|||
export let editingTriggerIsNew = true; |
|||
export let onTriggerEdit = (trigger) => {}; |
|||
export let onTriggerDelete = (trigger) => {}; |
|||
export let onTriggerSave = (trigger) => {}; |
|||
export let onTriggerCancel = () => {}; |
|||
|
|||
$: isEditing = (editingTrigger !== null); |
|||
|
|||
let triggerEditingFinished = (trigger) => { |
|||
|
|||
if(trigger) { |
|||
onTriggerSave(trigger) |
|||
} else { |
|||
onTriggerCancel(); |
|||
} |
|||
} |
|||
|
|||
</script> |
|||
|
|||
<h3>Triggers</h3> |
|||
|
|||
{#if $database.triggers} |
|||
<table class="fields-table uk-table uk-table-small"> |
|||
<thead> |
|||
<tr> |
|||
<th>Event</th> |
|||
<th>Action</th> |
|||
<th>Condition</th> |
|||
<th>Create Options</th> |
|||
<th></th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
{#each $database.triggers as trigger} |
|||
<tr> |
|||
<td >{trigger.eventName}</td> |
|||
<td >{trigger.actionName}</td> |
|||
<td >{trigger.condition}</td> |
|||
<td >{trigger.optionsCreator}</td> |
|||
<td class="edit-button"> |
|||
<span on:click={() => onTriggerEdit(trigger)}>{@html getIcon("edit")}</span> |
|||
<span on:click={() => onTriggerDelete(trigger)}>{@html getIcon("trash")}</span> |
|||
</td> |
|||
</tr> |
|||
{/each} |
|||
</tbody> |
|||
</table> |
|||
{:else} |
|||
(no triggers added) |
|||
{/if} |
|||
|
|||
|
|||
<Modal bind:isOpen={isEditing}> |
|||
{#if isEditing} |
|||
<TriggerView trigger={editingTrigger} |
|||
allActions={$database.actions} |
|||
allTriggers={$database.triggers} |
|||
onFinished={triggerEditingFinished} |
|||
isNew={editingTriggerIsNew}/> |
|||
{/if} |
|||
</Modal> |
|||
|
|||
<style> |
|||
|
|||
.edit-button { |
|||
cursor:pointer; |
|||
color: var(--white); |
|||
} |
|||
|
|||
tr:hover .edit-button { |
|||
color: var(--secondary75); |
|||
} |
|||
|
|||
</style> |
|||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,663 @@ |
|||
import {createNewHeirarchy} from "../common/core"; |
|||
|
|||
export const createPackage = (packageInfo, database) => { |
|||
packageInfo.createNewPackage(""); |
|||
const root = createNewHeirarchy(); |
|||
database.importAppDefinition({ |
|||
hierarchy:root, |
|||
actions:[], |
|||
triggers:[], |
|||
accessLevels: [], |
|||
accessLevelsVersion: 0 |
|||
}); |
|||
}; |
|||
|
|||
const testroot = { |
|||
"name": "root", |
|||
"type": "root", |
|||
"children": [ |
|||
{ |
|||
"name": "settings", |
|||
"type": "record", |
|||
"fields": [ |
|||
{ |
|||
"name": "appName", |
|||
"type": "string", |
|||
"typeOptions": { |
|||
"maxLength": null, |
|||
"values": null, |
|||
"allowDeclaredValuesOnly": false |
|||
}, |
|||
"label": "appName", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
} |
|||
], |
|||
"children": [], |
|||
"validationRules": [], |
|||
"nodeId": 1, |
|||
"indexes": [], |
|||
"allidsShardFactor": 64, |
|||
"collectionName": "", |
|||
"isSingle": true |
|||
}, |
|||
{ |
|||
"name": "customer", |
|||
"type": "record", |
|||
"fields": [ |
|||
{ |
|||
"name": "surname", |
|||
"type": "string", |
|||
"typeOptions": { |
|||
"maxLength": null, |
|||
"values": null, |
|||
"allowDeclaredValuesOnly": false |
|||
}, |
|||
"label": "surname", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "isalive", |
|||
"type": "bool", |
|||
"typeOptions": { |
|||
"allowNulls": true |
|||
}, |
|||
"label": "isalive", |
|||
"getInitialValue": "true", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "createddate", |
|||
"type": "datetime", |
|||
"typeOptions": { |
|||
"maxValue": "+275760-09-13T00:00:00.000Z", |
|||
"minValue": "-271821-04-20T00:00:00.000Z" |
|||
}, |
|||
"label": "createddate", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "age", |
|||
"type": "number", |
|||
"typeOptions": { |
|||
"maxValue": 9007199254740991, |
|||
"minValue": -9007199254740991, |
|||
"decimalPlaces": 0 |
|||
}, |
|||
"label": "age", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "profilepic", |
|||
"type": "file", |
|||
"typeOptions": {}, |
|||
"label": "profilepic", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "partner", |
|||
"type": "reference", |
|||
"typeOptions": { |
|||
"indexNodeKey": "/partnersReference", |
|||
"displayValue": "name", |
|||
"reverseIndexNodeKeys": [ |
|||
"/partners/4-{id}/partnerCustomers" |
|||
] |
|||
}, |
|||
"label": "partner", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "referredBy", |
|||
"type": "reference", |
|||
"typeOptions": { |
|||
"indexNodeKey": "/customer_index", |
|||
"displayValue": "surname", |
|||
"reverseIndexNodeKeys": [ |
|||
"/customers/2-{id}/referredToCustomers" |
|||
] |
|||
}, |
|||
"label": "referredBy", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
} |
|||
], |
|||
"children": [ |
|||
{ |
|||
"name": "invoice", |
|||
"type": "record", |
|||
"fields": [ |
|||
{ |
|||
"name": "totalIncVat", |
|||
"type": "number", |
|||
"typeOptions": { |
|||
"maxValue": 9007199254740991, |
|||
"minValue": -9007199254740991, |
|||
"decimalPlaces": 2 |
|||
}, |
|||
"label": "totalIncVat", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "createdDate", |
|||
"type": "datetime", |
|||
"typeOptions": { |
|||
"maxValue": "+275760-09-13T00:00:00.000Z", |
|||
"minValue": "-271821-04-20T00:00:00.000Z" |
|||
}, |
|||
"label": "createdDate", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "paidAmount", |
|||
"type": "number", |
|||
"typeOptions": { |
|||
"maxValue": 9007199254740991, |
|||
"minValue": -9007199254740991, |
|||
"decimalPlaces": 0 |
|||
}, |
|||
"label": "paidAmount", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "invoiceType", |
|||
"type": "string", |
|||
"typeOptions": { |
|||
"maxLength": null, |
|||
"values": null, |
|||
"allowDeclaredValuesOnly": false |
|||
}, |
|||
"label": "invoiceType", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "isWrittenOff", |
|||
"type": "bool", |
|||
"typeOptions": { |
|||
"allowNulls": true |
|||
}, |
|||
"label": "isWrittenOff", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "customer", |
|||
"type": "reference", |
|||
"typeOptions": { |
|||
"indexNodeKey": "/customersReference", |
|||
"reverseIndexNodeKeys": [ |
|||
"/customers/2-{id}/invoice_index" |
|||
], |
|||
"displayValue": "name" |
|||
}, |
|||
"label": "customer", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
} |
|||
], |
|||
"children": [ |
|||
{ |
|||
"name": "charge", |
|||
"type": "record", |
|||
"fields": [ |
|||
{ |
|||
"name": "amount", |
|||
"type": "number", |
|||
"typeOptions": { |
|||
"maxValue": 9007199254740991, |
|||
"minValue": -9007199254740991, |
|||
"decimalPlaces": 0 |
|||
}, |
|||
"label": "amount", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "partnerInvoice", |
|||
"type": "reference", |
|||
"typeOptions": { |
|||
"reverseIndexNodeKeys": [ |
|||
"/partners/4-{id}/invoices/6-{id}/partnerCharges" |
|||
], |
|||
"displayValue": "createdDate", |
|||
"indexNodeKey": "/partners/4-{id}/partnerInvoices_index" |
|||
}, |
|||
"label": "partnerInvoice", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
} |
|||
], |
|||
"children": [], |
|||
"validationRules": [], |
|||
"nodeId": 10, |
|||
"indexes": [], |
|||
"allidsShardFactor": 1, |
|||
"collectionName": "charges", |
|||
"isSingle": false |
|||
} |
|||
], |
|||
"validationRules": [], |
|||
"nodeId": 8, |
|||
"indexes": [ |
|||
{ |
|||
"name": "charge_index", |
|||
"type": "index", |
|||
"map": "return {...record};", |
|||
"filter": "", |
|||
"indexType": "ancestor", |
|||
"getShardName": "", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [], |
|||
"allowedRecordNodeIds": [ |
|||
10 |
|||
], |
|||
"nodeId": 11 |
|||
} |
|||
], |
|||
"allidsShardFactor": 1, |
|||
"collectionName": "invoices", |
|||
"isSingle": false |
|||
} |
|||
], |
|||
"validationRules": [], |
|||
"nodeId": 2, |
|||
"indexes": [ |
|||
{ |
|||
"name": "invoice_index", |
|||
"type": "index", |
|||
"map": "return {createdDate: record.createdDate, totalIncVat: record.totalIncVat};", |
|||
"filter": "", |
|||
"indexType": "ancestor", |
|||
"getShardName": "", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [], |
|||
"allowedRecordNodeIds": [ |
|||
8 |
|||
], |
|||
"nodeId": 9 |
|||
}, |
|||
{ |
|||
"name": "referredToCustomers", |
|||
"type": "index", |
|||
"map": "return {...record};", |
|||
"filter": "", |
|||
"indexType": "reference", |
|||
"getShardName": "return !record.surname ? 'null' : record.surname.substring(0,1);", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [], |
|||
"allowedRecordNodeIds": [ |
|||
2 |
|||
], |
|||
"nodeId": 14 |
|||
}, |
|||
{ |
|||
"name": "invoicesByOutstanding", |
|||
"type": "index", |
|||
"map": "return {...record};", |
|||
"filter": "", |
|||
"indexType": "ancestor", |
|||
"getShardName": "return (record.totalIncVat > record.paidAmount ? 'outstanding' : 'paid');", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [ |
|||
{ |
|||
"name": "all_invoices_by_type", |
|||
"type": "aggregateGroup", |
|||
"groupBy": "return record.invoiceType", |
|||
"aggregates": [ |
|||
{ |
|||
"name": "totalIncVat", |
|||
"aggregatedValue": "return record.totalIncVat" |
|||
} |
|||
], |
|||
"condition": "", |
|||
"nodeId": 26 |
|||
} |
|||
], |
|||
"allowedRecordNodeIds": [ |
|||
6, |
|||
8 |
|||
], |
|||
"nodeId": 25 |
|||
} |
|||
], |
|||
"allidsShardFactor": 64, |
|||
"collectionName": "customers", |
|||
"isSingle": false |
|||
}, |
|||
{ |
|||
"name": "partner", |
|||
"type": "record", |
|||
"fields": [ |
|||
{ |
|||
"name": "businessName", |
|||
"type": "string", |
|||
"typeOptions": { |
|||
"maxLength": null, |
|||
"values": null, |
|||
"allowDeclaredValuesOnly": false |
|||
}, |
|||
"label": "businessName", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
} |
|||
], |
|||
"children": [ |
|||
{ |
|||
"name": "invoice", |
|||
"type": "record", |
|||
"fields": [ |
|||
{ |
|||
"name": "totalIncVat", |
|||
"type": "number", |
|||
"typeOptions": { |
|||
"maxValue": 9007199254740991, |
|||
"minValue": -9007199254740991, |
|||
"decimalPlaces": 2 |
|||
}, |
|||
"label": "totalIncVat", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "createdDate", |
|||
"type": "datetime", |
|||
"typeOptions": { |
|||
"maxValue": "+275760-09-13T00:00:00.000Z", |
|||
"minValue": "-271821-04-20T00:00:00.000Z" |
|||
}, |
|||
"label": "createdDate", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "paidAmount", |
|||
"type": "number", |
|||
"typeOptions": { |
|||
"maxValue": 9007199254740991, |
|||
"minValue": -9007199254740991, |
|||
"decimalPlaces": 0 |
|||
}, |
|||
"label": "paidAmount", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
} |
|||
], |
|||
"children": [], |
|||
"validationRules": [], |
|||
"nodeId": 6, |
|||
"indexes": [ |
|||
{ |
|||
"name": "partnerCharges", |
|||
"type": "index", |
|||
"map": "return {...record};", |
|||
"filter": "", |
|||
"indexType": "reference", |
|||
"getShardName": "", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [], |
|||
"allowedRecordNodeIds": [ |
|||
{ |
|||
"name": "charge", |
|||
"type": "record", |
|||
"fields": [ |
|||
{ |
|||
"name": "amount", |
|||
"type": "number", |
|||
"typeOptions": { |
|||
"maxValue": 9007199254740991, |
|||
"minValue": -9007199254740991, |
|||
"decimalPlaces": 0 |
|||
}, |
|||
"label": "amount", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
}, |
|||
{ |
|||
"name": "partnerInvoice", |
|||
"type": "reference", |
|||
"typeOptions": { |
|||
"reverseIndexNodeKeys": [ |
|||
"/partners/4-{id}/invoices/6-{id}/partnerCharges" |
|||
], |
|||
"displayValue": "createdDate", |
|||
"indexNodeKey": "/partners/4-{id}/partnerInvoices_index" |
|||
}, |
|||
"label": "partnerInvoice", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
} |
|||
], |
|||
"children": [], |
|||
"validationRules": [], |
|||
"nodeId": 10, |
|||
"indexes": [], |
|||
"allidsShardFactor": 1, |
|||
"collectionName": "charges", |
|||
"isSingle": false |
|||
} |
|||
], |
|||
"nodeId": 15 |
|||
} |
|||
], |
|||
"allidsShardFactor": 1, |
|||
"collectionName": "invoices", |
|||
"isSingle": false |
|||
} |
|||
], |
|||
"validationRules": [], |
|||
"nodeId": 4, |
|||
"indexes": [ |
|||
{ |
|||
"name": "partnerInvoices_index", |
|||
"type": "index", |
|||
"map": "return {...record};", |
|||
"filter": "", |
|||
"indexType": "ancestor", |
|||
"getShardName": "", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [], |
|||
"allowedRecordNodeIds": [ |
|||
6 |
|||
], |
|||
"nodeId": 7 |
|||
}, |
|||
{ |
|||
"name": "partnerCustomers", |
|||
"type": "index", |
|||
"map": "return {...record};", |
|||
"filter": "record.isalive === true", |
|||
"indexType": "reference", |
|||
"getShardName": "", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [], |
|||
"allowedRecordNodeIds": [ |
|||
2 |
|||
], |
|||
"nodeId": 13 |
|||
} |
|||
], |
|||
"allidsShardFactor": 64, |
|||
"collectionName": "partners", |
|||
"isSingle": false |
|||
} |
|||
], |
|||
"pathMaps": [], |
|||
"indexes": [ |
|||
{ |
|||
"name": "customer_index", |
|||
"type": "index", |
|||
"map": "return record;", |
|||
"filter": "", |
|||
"indexType": "ancestor", |
|||
"getShardName": "", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [ |
|||
{ |
|||
"name": "Customers Summary", |
|||
"type": "aggregateGroup", |
|||
"groupBy": "", |
|||
"aggregates": [ |
|||
{ |
|||
"name": "all customers - age breakdown", |
|||
"aggregatedValue": "return record.age" |
|||
} |
|||
], |
|||
"condition": "", |
|||
"nodeId": 24 |
|||
} |
|||
], |
|||
"allowedRecordNodeIds": [ |
|||
2 |
|||
], |
|||
"nodeId": 3 |
|||
}, |
|||
{ |
|||
"name": "partner_index", |
|||
"type": "index", |
|||
"map": "return {...record};", |
|||
"filter": "", |
|||
"indexType": "ancestor", |
|||
"getShardName": "", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [], |
|||
"allowedRecordNodeIds": [ |
|||
4 |
|||
], |
|||
"nodeId": 5 |
|||
}, |
|||
{ |
|||
"name": "partnersReference", |
|||
"type": "index", |
|||
"map": "return {name:record.businessName};", |
|||
"filter": "", |
|||
"indexType": "ancestor", |
|||
"getShardName": "", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [], |
|||
"allowedRecordNodeIds": [ |
|||
4 |
|||
], |
|||
"nodeId": 12 |
|||
}, |
|||
{ |
|||
"name": "customersReference", |
|||
"type": "index", |
|||
"map": "return {name:record.surname}", |
|||
"filter": "record.isalive === true", |
|||
"indexType": "ancestor", |
|||
"getShardName": "", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [], |
|||
"allowedRecordNodeIds": [ |
|||
2 |
|||
], |
|||
"nodeId": 16 |
|||
}, |
|||
{ |
|||
"name": "deceased", |
|||
"type": "index", |
|||
"map": "return {surname: record.surname, age:record.age};", |
|||
"filter": "record.isalive === false", |
|||
"indexType": "ancestor", |
|||
"getShardName": "", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [], |
|||
"allowedRecordNodeIds": [ |
|||
2 |
|||
], |
|||
"nodeId": 17 |
|||
}, |
|||
{ |
|||
"name": "customer_invoices", |
|||
"type": "index", |
|||
"map": "return record;", |
|||
"filter": "record.type === 'invoice'", |
|||
"indexType": "ancestor", |
|||
"getShardName": "", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [], |
|||
"allowedRecordNodeIds": [ |
|||
8 |
|||
], |
|||
"nodeId": 18 |
|||
}, |
|||
{ |
|||
"name": "Outstanding Invoices", |
|||
"type": "index", |
|||
"map": "return {...record};", |
|||
"filter": "record.type === 'invoice' && record.paidAmount < record.totalIncVat", |
|||
"indexType": "ancestor", |
|||
"getShardName": "", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [ |
|||
{ |
|||
"name": "all_invoices", |
|||
"type": "aggregateGroup", |
|||
"groupBy": "", |
|||
"aggregates": [], |
|||
"condition": "", |
|||
"nodeId": 20 |
|||
}, |
|||
{ |
|||
"name": "all_invoices_by_type", |
|||
"type": "aggregateGroup", |
|||
"groupBy": "return record.invoiceType", |
|||
"aggregates": [ |
|||
{ |
|||
"name": "totalIncVat", |
|||
"aggregatedValue": "return record.totalIncVat" |
|||
}, |
|||
{ |
|||
"name": "paidAmount", |
|||
"aggregatedValue": "return record.paidAmount" |
|||
} |
|||
], |
|||
"condition": "", |
|||
"nodeId": 21 |
|||
}, |
|||
{ |
|||
"name": "written_off", |
|||
"type": "aggregateGroup", |
|||
"groupBy": "return record.invoiceType", |
|||
"aggregates": [ |
|||
{ |
|||
"name": "totalIncVat", |
|||
"aggregatedValue": "return record.totalIncVat" |
|||
} |
|||
], |
|||
"condition": "record.isWrittenOff === true", |
|||
"nodeId": 22 |
|||
} |
|||
], |
|||
"allowedRecordNodeIds": [ |
|||
8, |
|||
6 |
|||
], |
|||
"nodeId": 19 |
|||
}, |
|||
{ |
|||
"name": "customersBySurname", |
|||
"type": "index", |
|||
"map": "return {...record};", |
|||
"filter": "", |
|||
"indexType": "ancestor", |
|||
"getShardName": "return !record.surname ? 'null' : record.surname.substring(0,1);", |
|||
"getSortKey": "record.id", |
|||
"aggregateGroups": [], |
|||
"allowedRecordNodeIds": [ |
|||
2 |
|||
], |
|||
"nodeId": 23 |
|||
} |
|||
], |
|||
"nodeId": 0 |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
import {createPackage} from "./createPackage"; |
|||
import getStore from "./store"; |
|||
import { last } from "lodash/fp"; |
|||
|
|||
const appName = last(window.location.hash.substr(1).split("/")); |
|||
|
|||
export const database = getStore(appName); |
|||
|
|||
export const createNewPackage = () => |
|||
createPackage(packageInfo, database); |
|||
|
|||
export const initialise = async () => { |
|||
try { |
|||
await database.initialise(); |
|||
} catch(err) { |
|||
console.log(err); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,330 @@ |
|||
import {hierarchy as hierarchyFunctions, |
|||
common, getTemplateApi } from "budibase-core"; |
|||
import {filter, cloneDeep, sortBy, map, |
|||
find, isEmpty, groupBy, reduce} from "lodash/fp"; |
|||
import {chain, getNode, validate, |
|||
constructHierarchy, templateApi} from "../common/core"; |
|||
import {writable} from "svelte/store"; |
|||
|
|||
export const getStore = (appname) => { |
|||
|
|||
const initial = { |
|||
hierarchy: {}, |
|||
actions: [], |
|||
triggers: [], |
|||
currentNodeIsNew: false, |
|||
errors: [], |
|||
activeNav: "database", |
|||
hasAppPackage: (!!appname && appname.length > 0), |
|||
accessLevels: [], |
|||
currentNode: null}; |
|||
|
|||
const store = writable(initial); |
|||
|
|||
store.appname = appname; |
|||
store.initialise = initialise(store, initial, appname); |
|||
store.newChildRecord = newRecord(store, false); |
|||
store.newRootRecord = newRecord(store, true); |
|||
store.selectExistingNode = selectExistingNode(store); |
|||
store.newChildIndex = newIndex(store, false); |
|||
store.newRootIndex = newIndex(store, true); |
|||
store.saveCurrentNode = saveCurrentNode(store); |
|||
store.importAppDefinition = importAppDefinition(store); |
|||
store.deleteCurrentNode = deleteCurrentNode(store); |
|||
store.saveField = saveField(store); |
|||
store.deleteField = deleteField(store); |
|||
store.saveAction = saveAction(store); |
|||
store.deleteAction = deleteAction(store); |
|||
store.saveTrigger = saveTrigger(store); |
|||
store.deleteTrigger = deleteTrigger(store); |
|||
store.saveLevel = saveLevel(store); |
|||
store.deleteLevel = deleteLevel(store); |
|||
store.setActiveNav = setActiveNav(store); |
|||
return store; |
|||
} |
|||
|
|||
export default getStore; |
|||
|
|||
const initialise = (store, initial) => async () => { |
|||
|
|||
const pkg = await fetch(`/api/${store.appname}/appPackage`) |
|||
.then(r => r.json()); |
|||
|
|||
initial.hierarchy = pkg.appDefinition.hierarchy; |
|||
initial.accessLevels = pkg.accessLevels; |
|||
initial.actions = reduce((arr, action) => { |
|||
arr.push(action); |
|||
return arr; |
|||
})(pkg.appDefinition.actions, []); |
|||
initial.triggers = pkg.appDefinition.triggers; |
|||
|
|||
if(!!initial.hierarchy && !isEmpty(initial.hierarchy)) { |
|||
initial.hierarchy = constructHierarchy(initial.hierarchy); |
|||
const shadowHierarchy = createShadowHierarchy(initial.hierarchy); |
|||
if(initial.currentNode !== null) |
|||
initial.currentNode = getNode( |
|||
shadowHierarchy, initial.currentNode.nodeId |
|||
); |
|||
} |
|||
store.set(initial); |
|||
return initial; |
|||
} |
|||
|
|||
const newRecord = (store, useRoot) => () => { |
|||
store.update(s => { |
|||
s.currentNodeIsNew = true; |
|||
const shadowHierarchy = createShadowHierarchy(s.hierarchy); |
|||
parent = useRoot ? shadowHierarchy |
|||
: getNode( |
|||
shadowHierarchy, |
|||
s.currentNode.nodeId); |
|||
s.errors = []; |
|||
s.currentNode = templateApi(shadowHierarchy) |
|||
.getNewRecordTemplate(parent, "", true); |
|||
return s; |
|||
}); |
|||
} |
|||
|
|||
|
|||
const selectExistingNode = (store) => (nodeId) => { |
|||
store.update(s => { |
|||
const shadowHierarchy = createShadowHierarchy(s.hierarchy); |
|||
s.currentNode = getNode( |
|||
shadowHierarchy, nodeId |
|||
); |
|||
s.currentNodeIsNew = false; |
|||
s.errors = []; |
|||
return s; |
|||
}) |
|||
} |
|||
|
|||
const newIndex = (store, useRoot) => () => { |
|||
store.update(s => { |
|||
s.currentNodeIsNew = true; |
|||
s.errors = []; |
|||
const shadowHierarchy = createShadowHierarchy(s.hierarchy); |
|||
parent = useRoot ? shadowHierarchy |
|||
: getNode( |
|||
shadowHierarchy, |
|||
s.currentNode.nodeId); |
|||
|
|||
s.currentNode = templateApi(shadowHierarchy) |
|||
.getNewIndexTemplate(parent); |
|||
return s; |
|||
}); |
|||
} |
|||
|
|||
const saveCurrentNode = (store) => () => { |
|||
store.update(s => { |
|||
|
|||
const errors = validate.node(s.currentNode); |
|||
s.errors = errors; |
|||
if(errors.length > 0) { |
|||
return s; |
|||
} |
|||
|
|||
const parentNode = getNode( |
|||
s.hierarchy, s.currentNode.parent().nodeId); |
|||
|
|||
const existingNode = getNode( |
|||
s.hierarchy, s.currentNode.nodeId); |
|||
|
|||
let index = parentNode.children.length; |
|||
if(!!existingNode) { |
|||
// remove existing
|
|||
index = existingNode.parent().children.indexOf(existingNode); |
|||
existingNode.parent().children = chain(existingNode.parent().children, [ |
|||
filter(c => c.nodeId !== existingNode.nodeId) |
|||
]); |
|||
} |
|||
|
|||
// should add node into existing hierarchy
|
|||
const cloned = cloneDeep(s.currentNode); |
|||
templateApi(s.hierarchy).constructNode( |
|||
parentNode, |
|||
cloned |
|||
); |
|||
|
|||
const newIndexOfchild = child => { |
|||
if(child === cloned) return index; |
|||
const currentIndex = parentNode.children.indexOf(child); |
|||
return currentIndex >= index ? currentIndex + 1 : currentIndex; |
|||
} |
|||
|
|||
parentNode.children = chain(parentNode.children, [ |
|||
sortBy(newIndexOfchild) |
|||
]); |
|||
|
|||
s.currentNodeIsNew = false; |
|||
|
|||
savePackage(store, s); |
|||
|
|||
return s; |
|||
}); |
|||
} |
|||
|
|||
const importAppDefinition = store => appDefinition => { |
|||
store.update(s => { |
|||
s.hierarchy = appDefinition.hierarchy; |
|||
s.currentNode = appDefinition.hierarchy.children.length > 0 |
|||
? appDefinition.hierarchy.children[0] |
|||
: null; |
|||
s.actions = appDefinition.actions; |
|||
s.triggers = appDefinition.triggers; |
|||
s.currentNodeIsNew = false; |
|||
return s; |
|||
}); |
|||
} |
|||
|
|||
const deleteCurrentNode = store => () => { |
|||
store.update(s => { |
|||
const nodeToDelete = getNode(s.hierarchy, s.currentNode.nodeId); |
|||
s.currentNode = hierarchyFunctions.isRoot(nodeToDelete.parent()) |
|||
? find(n => n != s.currentNode) |
|||
(s.hierarchy.children) |
|||
: nodeToDelete.parent(); |
|||
if(hierarchyFunctions.isRecord(nodeToDelete)) { |
|||
nodeToDelete.parent().children = filter(c => c.nodeId !== nodeToDelete.nodeId) |
|||
(nodeToDelete.parent().children); |
|||
} else { |
|||
nodeToDelete.parent().indexes = filter(c => c.nodeId !== nodeToDelete.nodeId) |
|||
(nodeToDelete.parent().indexes); |
|||
} |
|||
s.errors = []; |
|||
savePackage(store, s); |
|||
return s; |
|||
}); |
|||
} |
|||
|
|||
const saveField = databaseStore => (field) => { |
|||
databaseStore.update(db => { |
|||
db.currentNode.fields = filter(f => f.name !== field.name) |
|||
(db.currentNode.fields); |
|||
|
|||
templateApi(db.hierarchy).addField(db.currentNode, field); |
|||
return db; |
|||
}); |
|||
} |
|||
|
|||
|
|||
const deleteField = databaseStore => field => { |
|||
databaseStore.update(db => { |
|||
db.currentNode.fields = filter(f => f.name !== field.name) |
|||
(db.currentNode.fields); |
|||
|
|||
return db; |
|||
}); |
|||
} |
|||
|
|||
|
|||
const saveAction = store => (newAction, isNew, oldAction=null) => { |
|||
store.update(s => { |
|||
|
|||
const existingAction = isNew |
|||
? null |
|||
: find(a => a.name === oldAction.name)(s.actions); |
|||
|
|||
if(existingAction) { |
|||
s.actions = chain(s.actions, [ |
|||
map(a => a === existingAction ? newAction : a) |
|||
]); |
|||
} else { |
|||
s.actions.push(newAction); |
|||
} |
|||
savePackage(store, s); |
|||
return s; |
|||
}); |
|||
} |
|||
|
|||
const deleteAction = store => action => { |
|||
store.update(s => { |
|||
s.actions = filter(a => a.name !== action.name)(s.actions); |
|||
savePackage(store, s); |
|||
return s; |
|||
}); |
|||
} |
|||
|
|||
const saveTrigger = store => (newTrigger, isNew, oldTrigger=null) => { |
|||
store.update(s => { |
|||
|
|||
const existingTrigger = isNew |
|||
? null |
|||
: find(a => a.name === oldTrigger.name)(s.triggers); |
|||
|
|||
if(existingTrigger) { |
|||
s.triggers = chain(s.triggers, [ |
|||
map(a => a === existingTrigger ? newTrigger : a) |
|||
]); |
|||
} else { |
|||
s.triggers.push(newTrigger); |
|||
} |
|||
savePackage(store, s); |
|||
return s; |
|||
}); |
|||
} |
|||
|
|||
const deleteTrigger = store => trigger => { |
|||
store.update(s => { |
|||
s.triggers = filter(t => t.name !== trigger.name)(s.triggers); |
|||
return s; |
|||
}); |
|||
} |
|||
|
|||
const saveLevel = store => (newLevel, isNew, oldLevel=null) => { |
|||
store.update(s => { |
|||
|
|||
const existingLevel = isNew |
|||
? null |
|||
: find(a => a.name === oldLevel.name)(s.accessLevels); |
|||
|
|||
if(existingLevel) { |
|||
s.accessLevels = chain(s.accessLevels, [ |
|||
map(a => a === existingLevel ? newLevel : a) |
|||
]); |
|||
} else { |
|||
s.accessLevels.push(newLevel); |
|||
} |
|||
savePackage(store, s); |
|||
return s; |
|||
}); |
|||
} |
|||
|
|||
const deleteLevel = store => level => { |
|||
store.update(s => { |
|||
s.accessLevels = filter(t => t.name !== level.name)(s.accessLevels); |
|||
savePackage(store, s); |
|||
return s; |
|||
}); |
|||
} |
|||
|
|||
const setActiveNav = databaseStore => navName => { |
|||
databaseStore.update(db => { |
|||
db.activeNav = navName; |
|||
return db; |
|||
}); |
|||
} |
|||
|
|||
const createShadowHierarchy = hierarchy => |
|||
constructHierarchy(JSON.parse(JSON.stringify(hierarchy))); |
|||
|
|||
const savePackage = (store, db) => { |
|||
|
|||
const appDefinition = { |
|||
hierarchy:db.hierarchy, |
|||
triggers:db.triggers, |
|||
actions: groupBy("name")(db.actions) |
|||
}; |
|||
|
|||
const data = { |
|||
appDefinition, |
|||
accessLevels:db.accessLevels |
|||
} |
|||
|
|||
fetch(`/api/${store.appname}/appPackage`, { |
|||
method: 'POST', |
|||
headers: { |
|||
'Content-Type': 'application/json', |
|||
}, |
|||
body: JSON.stringify(data), // body data type must match "Content-Type" header
|
|||
}); |
|||
} |
|||
@ -0,0 +1,161 @@ |
|||
<script> |
|||
|
|||
export let color = "primary"; |
|||
export let className = ""; |
|||
export let style = ""; |
|||
export let groupPosition = ""; |
|||
export let grouped = false; |
|||
|
|||
$: borderClass = grouped |
|||
? "" |
|||
: "border-normal"; |
|||
|
|||
</script> |
|||
|
|||
<button class="{color} {className} {borderClass} {grouped ? "grouped" : ""}" |
|||
style="{style}" |
|||
on:click> |
|||
<slot/> |
|||
</button> |
|||
|
|||
<style> |
|||
|
|||
.border-normal { border-radius: var(--borderradiusall); } |
|||
.border-left { border-radius: var(--borderradius) 0 0 var(--borderradius); } |
|||
.border-right { border-radius: 0 var(--borderradius) var(--borderradius) 0; } |
|||
.border-middle { border-radius: 0; } |
|||
|
|||
button { |
|||
border-style: solid; |
|||
padding: 7px 15px; |
|||
cursor:pointer; |
|||
} |
|||
|
|||
/* ---- PRIMARY ----*/ |
|||
.primary { |
|||
background-color: var(--primary100); |
|||
border-color: var(--primary100); |
|||
color: var(--white); |
|||
} |
|||
|
|||
.primary:hover { |
|||
background-color: var(--primary75); |
|||
border-color: var(--primary75); |
|||
} |
|||
|
|||
.primary:active { |
|||
background-color: var(--primarydark); |
|||
border-color: var(--primarydark); |
|||
} |
|||
|
|||
.primary-outline { |
|||
background-color: var(--white); |
|||
border-color: var(--primary100); |
|||
color: var(--primary100); |
|||
} |
|||
|
|||
.primary-outline:hover { |
|||
background-color: var(--primary10); |
|||
} |
|||
|
|||
.primary-outline:pressed { |
|||
background-color: var(--primary25); |
|||
} |
|||
|
|||
/* ---- secondary ----*/ |
|||
|
|||
.secondary { |
|||
background-color: var(--secondary100); |
|||
border-color: var(--secondary100); |
|||
color: var(--white); |
|||
} |
|||
|
|||
.secondary:hover { |
|||
background-color: var(--secondary75); |
|||
border-color: var(--secondary75); |
|||
} |
|||
|
|||
.secondary:pressed { |
|||
background-color: var(--secondarydark); |
|||
border-color: var(--secondarydark); |
|||
} |
|||
|
|||
.secondary-outline { |
|||
background-color: var(--white); |
|||
border-color: var(--secondary100); |
|||
color: var(--secondary100); |
|||
} |
|||
|
|||
.secondary-outline:hover { |
|||
background-color: var(--secondary10); |
|||
} |
|||
|
|||
.secondary-outline:pressed { |
|||
background-color: var(--secondary25); |
|||
} |
|||
|
|||
|
|||
/* ---- success ----*/ |
|||
.success { |
|||
background-color: var(--success100); |
|||
border-color: var(--success100); |
|||
color: var(--white); |
|||
} |
|||
|
|||
.success:hover { |
|||
background-color: var(--success75); |
|||
border-color: var(--success75); |
|||
} |
|||
|
|||
.success:pressed { |
|||
background-color: var(--successdark); |
|||
border-color: var(--successdark); |
|||
} |
|||
|
|||
.success-outline { |
|||
background-color: var(--white); |
|||
border-color: var(--success100); |
|||
color: var(--success100); |
|||
} |
|||
|
|||
.success-outline:hover { |
|||
background-color: var(--success10); |
|||
} |
|||
|
|||
.success-outline:pressed { |
|||
background-color: var(--success25); |
|||
} |
|||
|
|||
/* ---- deletion ----*/ |
|||
.deletion { |
|||
background-color: var(--deletion100); |
|||
border-color: var(--deletion100); |
|||
color: var(--white); |
|||
} |
|||
|
|||
.deletion:hover { |
|||
background-color: var(--deletion75); |
|||
border-color: var(--deletion75); |
|||
} |
|||
|
|||
.deletion:pressed { |
|||
background-color: var(--deletiondark); |
|||
border-color: var(--deletiondark); |
|||
} |
|||
|
|||
.deletion-outline { |
|||
background-color: var(--white); |
|||
border-color: var(--deletion100); |
|||
color: var(--deletion100); |
|||
} |
|||
|
|||
.deletion-outline:hover { |
|||
background-color: var(--deletion10); |
|||
} |
|||
|
|||
.deletion-outline:pressed { |
|||
background-color: var(--deletion25); |
|||
} |
|||
|
|||
|
|||
</style> |
|||
@ -0,0 +1,27 @@ |
|||
<script> |
|||
export let style=""; |
|||
</script> |
|||
|
|||
<div class="root" style={style}> |
|||
<slot /> |
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
.root { |
|||
display: flex; |
|||
} |
|||
|
|||
.root:last-child { |
|||
border-radius: 0 var(--borderradius) var(--borderradius) 0; |
|||
} |
|||
|
|||
.root:first-child { |
|||
border-radius: var(--borderradius) 0 0 var(--borderradius); |
|||
} |
|||
|
|||
.root:not(:first-child):not(:last-child) { |
|||
border-radius: 0; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,16 @@ |
|||
<script> |
|||
|
|||
export let checked=false; |
|||
export let label=""; |
|||
|
|||
</script> |
|||
|
|||
<input class="uk-checkbox" type="checkbox" bind:checked on:change />{label} |
|||
|
|||
<style> |
|||
|
|||
input { |
|||
margin-right:7px; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,21 @@ |
|||
<script> |
|||
// todo: use https://ace.c9.io |
|||
export let text = ""; |
|||
export let label = ""; |
|||
</script> |
|||
|
|||
<div>{label}</div> |
|||
<textarea bind:value={text}></textarea> |
|||
|
|||
<style> |
|||
|
|||
textarea { |
|||
padding:3px; |
|||
background: var(--darkslate); |
|||
color: var(--white); |
|||
font-family: 'Courier New', Courier, monospace; |
|||
width:95%; |
|||
height:100px; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,17 @@ |
|||
<script> |
|||
|
|||
export let name = ""; |
|||
|
|||
</script> |
|||
|
|||
<div> |
|||
<h4>Coming Sometime: {name}</h4> |
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
h4 { |
|||
margin-top: 20px; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,61 @@ |
|||
<script> |
|||
|
|||
import flatpickr from "flatpickr"; |
|||
import "flatpickr/dist/flatpickr.css"; |
|||
import { onMount } from 'svelte'; |
|||
|
|||
export let value; |
|||
export let label; |
|||
let input; |
|||
let fpInstance; |
|||
|
|||
$: if (fpInstance) fpInstance.setDate(value); |
|||
|
|||
onMount(() => { |
|||
fpInstance = flatpickr(input, {}); |
|||
|
|||
fpInstance.config.onChange.push(selectedDates => { |
|||
if(selectedDates.length > 0) |
|||
value = new Date(selectedDates[0]); |
|||
}); |
|||
|
|||
return fpInstance; |
|||
}) |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
</script> |
|||
|
|||
|
|||
<div class="container" > |
|||
<div class="label">{label}</div> |
|||
<input class="control" bind:this={input} /> |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: grid; |
|||
grid-template-columns: [label] 100px [control] auto; |
|||
margin: 20px 0px; |
|||
} |
|||
.label { |
|||
grid-column-start: label; |
|||
align-self: center; |
|||
} |
|||
|
|||
.control { |
|||
grid-column-start: control; |
|||
align-self: center; |
|||
margin: 0; |
|||
} |
|||
|
|||
input { |
|||
width:300px; |
|||
} |
|||
|
|||
|
|||
</style> |
|||
@ -0,0 +1,59 @@ |
|||
<script> |
|||
import {createEventDispatcher} from "svelte"; |
|||
|
|||
export let selected; |
|||
export let label; |
|||
export let options; |
|||
export let valueMember; |
|||
export let textMember; |
|||
export let multiple=false; |
|||
|
|||
const dispatch =createEventDispatcher(); |
|||
|
|||
|
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div class="label">{label}</div> |
|||
|
|||
{#if multiple} |
|||
|
|||
<select class="control uk-select" multiple bind:value={selected} on:change> |
|||
{#each options as option} |
|||
<option value={!valueMember ? option : valueMember(option)}>{!textMember ? option : textMember(option)}</option> |
|||
{/each} |
|||
</select> |
|||
|
|||
{:else} |
|||
|
|||
<select class="control uk-select" bind:value={selected} on:change> |
|||
{#each options as option} |
|||
<option value={!valueMember ? option : valueMember(option)}>{!textMember ? option : textMember(option)}</option> |
|||
{/each} |
|||
</select> |
|||
{/if} |
|||
|
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
.container { |
|||
display: grid; |
|||
grid-template-columns: [label] 100px [control] auto; |
|||
margin: 20px 0px; |
|||
} |
|||
.label { |
|||
grid-column-start: label; |
|||
align-self: center; |
|||
} |
|||
.control { |
|||
grid-column-start: control; |
|||
align-self: center; |
|||
margin: 0; |
|||
} |
|||
|
|||
select { |
|||
width:300px; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,68 @@ |
|||
<script> |
|||
|
|||
import getIcon from "./icon"; |
|||
export let iconName; |
|||
export let actions = []; // [ {label: "Action Name", onclick: () => {...} } ] |
|||
let isDroppedDown = false; |
|||
|
|||
</script> |
|||
|
|||
|
|||
<div class="root" on:click={() => isDroppedDown = !isDroppedDown}> |
|||
{@html getIcon(iconName)} |
|||
|
|||
<div class="dropdown-background" on:click|stopPropagation={() => isDroppedDown = false} style="display: {isDroppedDown ? 'block' : 'none'}"></div> |
|||
|
|||
<div class="dropdown-content" style="display: {isDroppedDown ? 'inline-block' : 'none'}"> |
|||
{#each actions as action} |
|||
<div class="action-row" on:click={action.onclick}> |
|||
{action.label} |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
|
|||
</div> |
|||
|
|||
|
|||
<style> |
|||
|
|||
.dropdown-background { |
|||
position: fixed; |
|||
top:0; |
|||
left:0; |
|||
width:100vw; |
|||
height:100vh; |
|||
} |
|||
|
|||
.root { |
|||
cursor: pointer; |
|||
z-index: 1; |
|||
} |
|||
|
|||
.dropdown-content { |
|||
position: absolute; |
|||
background-color: var(--white); |
|||
min-width: 160px; |
|||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); |
|||
z-index: 1; |
|||
font-weight: normal; |
|||
border-style:solid; |
|||
border-width: 1px; |
|||
border-color: var(--secondary10); |
|||
} |
|||
|
|||
.dropdown-content:not(:focus) { |
|||
display: none; |
|||
} |
|||
|
|||
.action-row { |
|||
padding: 7px 10px; |
|||
cursor:pointer; |
|||
} |
|||
|
|||
.action-row:hover { |
|||
background-color: var(--primary100); |
|||
color:var(--white); |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,29 @@ |
|||
<script> |
|||
export let errors = []; |
|||
|
|||
$:hasErrors = errors.length > 0; |
|||
</script> |
|||
|
|||
{#if hasErrors} |
|||
<div class="error-container"> |
|||
{#each errors as error} |
|||
<div class="error-row">{error.field ? `${error.field}: ` : ""}{error.error}</div> |
|||
{/each} |
|||
</div> |
|||
{/if} |
|||
|
|||
<style> |
|||
|
|||
.error-container { |
|||
padding:10px; |
|||
border-style: solid; |
|||
border-color: var(--deletion100); |
|||
border-radius: var(--borderradiusall); |
|||
background: var(--deletion75); |
|||
} |
|||
|
|||
.error-row { |
|||
padding: 5px 0px; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,36 @@ |
|||
<script> |
|||
import UIkit from "uikit"; |
|||
|
|||
export let isOpen = false; |
|||
export let onClosed = () => {}; |
|||
|
|||
let ukModal; |
|||
let listenerAdded = false; |
|||
|
|||
$: if(ukModal && !listenerAdded) { |
|||
listenerAdded = true; |
|||
ukModal.addEventListener("hide", onClosed); |
|||
} |
|||
|
|||
$: { |
|||
if(ukModal) { |
|||
if(isOpen) { |
|||
UIkit.modal(ukModal).show(); |
|||
} else { |
|||
UIkit.modal(ukModal).hide(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
</script> |
|||
|
|||
<div bind:this={ukModal} uk-modal > |
|||
<div class="uk-modal-dialog uk-modal-body" uk-overflow-auto> |
|||
<slot /> |
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
<style> |
|||
|
|||
</style> |
|||
@ -0,0 +1,46 @@ |
|||
<script> |
|||
|
|||
export let value; |
|||
export let label; |
|||
|
|||
const inputChanged = ev => { |
|||
try { |
|||
value = Number(ev.target.value); |
|||
} catch(_) { |
|||
value = null; |
|||
} |
|||
} |
|||
|
|||
let numberText = value === null || value === undefined |
|||
? "" : value.toString(); |
|||
|
|||
|
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div class="label">{label}</div> |
|||
<input class="control" type="text" value={value} on:change={inputChanged} > |
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
.container { |
|||
display: grid; |
|||
grid-template-columns: [label] 100px [control] auto; |
|||
margin: 20px 0px; |
|||
} |
|||
.label { |
|||
grid-column-start: label; |
|||
align-self: center; |
|||
} |
|||
.control { |
|||
grid-column-start: control; |
|||
align-self: center; |
|||
margin: 0; |
|||
} |
|||
|
|||
input { |
|||
width:300px; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,32 @@ |
|||
<script> |
|||
export let text = ""; |
|||
export let label = ""; |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div class="label">{label}</div> |
|||
<input class="control uk-input" bind:value={text} > |
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
.container { |
|||
display: grid; |
|||
grid-template-columns: [label] 100px [control] auto; |
|||
margin: 20px 0px; |
|||
} |
|||
.label { |
|||
grid-column-start: label; |
|||
align-self: center; |
|||
} |
|||
.control { |
|||
grid-column-start: control; |
|||
align-self: center; |
|||
margin: 0; |
|||
} |
|||
|
|||
input { |
|||
width:300px; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,47 @@ |
|||
<script> |
|||
import {join} from "lodash/fp"; |
|||
|
|||
export let values; |
|||
export let label; |
|||
|
|||
const inputChanged = ev => { |
|||
try { |
|||
values = ev.target.value.split("\n"); |
|||
} catch(_) { |
|||
values = []; |
|||
} |
|||
} |
|||
|
|||
$: valuesText = join("\n")(values); |
|||
|
|||
|
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div class="label">{label}</div> |
|||
<textarea class="control" value={valuesText} on:change={inputChanged} ></textarea> |
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
.container { |
|||
display: grid; |
|||
grid-template-columns: [label] 100px [control] auto; |
|||
margin: 20px 0px; |
|||
} |
|||
.label { |
|||
grid-column-start: label; |
|||
align-self: center; |
|||
} |
|||
.control { |
|||
grid-column-start: control; |
|||
align-self: center; |
|||
margin: 0; |
|||
} |
|||
|
|||
textarea { |
|||
width:300px; |
|||
height:200px; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,75 @@ |
|||
import {hierarchy as hierarchyFunctions, |
|||
common, getTemplateApi, getAuthApi } from "budibase-core"; |
|||
import {find, filter, includes, keyBy, some, |
|||
flatten, map} from "lodash/fp"; |
|||
|
|||
export const chain = common.$; |
|||
|
|||
export const events = common.eventsList; |
|||
|
|||
export const getNode = (hierarchy, nodeId) => |
|||
chain(hierarchy, [ |
|||
hierarchyFunctions.getFlattenedHierarchy, |
|||
find(n => n.nodeId === nodeId || n.nodeKey() === nodeId) |
|||
]); |
|||
|
|||
export const constructHierarchy = node => { |
|||
if(!node) return node; |
|||
return templateApi(node).constructHeirarchy(node); |
|||
} |
|||
|
|||
export const createNewHeirarchy = () => { |
|||
return templateApi().getNewRootLevel(); |
|||
} |
|||
|
|||
export const templateApi = hierarchy => getTemplateApi({heirarchy:hierarchy}) |
|||
export const authApi = (hierarchy, actions) => getAuthApi({ |
|||
heirarchy:hierarchy, actions: keyBy("name")(actions), publish:()=>{}}) |
|||
|
|||
export const allTypes = templateApi({}).allTypes; |
|||
|
|||
export const validate = { |
|||
all: templateApi({}).validateAll, |
|||
node: templateApi({}).validateNode, |
|||
field: templateApi({}).validateField |
|||
}; |
|||
|
|||
export const getPotentialReverseReferenceIndexes = (hierarchy, refIndex) => { |
|||
const res = chain(hierarchy, [ |
|||
hierarchyFunctions.getFlattenedHierarchy, |
|||
filter(n => hierarchyFunctions.isAncestor(refIndex)(n) |
|||
|| hierarchyFunctions.isAncestor(refIndex)(n.parent())), |
|||
map(n => n.indexes), |
|||
flatten, |
|||
filter(hierarchyFunctions.isReferenceIndex) |
|||
]); |
|||
|
|||
return res; |
|||
} |
|||
|
|||
export const getPotentialReferenceIndexes = (hierarchy, record) => |
|||
chain(hierarchy, [ |
|||
hierarchyFunctions.getFlattenedHierarchy, |
|||
filter(hierarchyFunctions.isAncestorIndex), |
|||
filter(i => hierarchyFunctions.isAncestor(record)(i.parent()) |
|||
|| i.parent().nodeId === record.parent().nodeId |
|||
|| hierarchyFunctions.isRoot(i.parent())) |
|||
]); |
|||
|
|||
export const getDefaultTypeOptions = type => |
|||
!type ? {} : allTypes[type].getDefaultOptions(); |
|||
|
|||
export const getNewAction = () => templateApi({}).createAction(); |
|||
export const getNewTrigger = () => templateApi({}).createTrigger(); |
|||
|
|||
export const validateActions = actions => templateApi({}).validateActions(actions); |
|||
export const validateTriggers = (triggers, actions) => templateApi({}).validateTriggers(triggers, actions); |
|||
|
|||
export const generateFullPermissions = (hierarchy, actions) => |
|||
authApi(hierarchy,actions).generateFullPermissions(); |
|||
|
|||
export const getNewAccessLevel = () => |
|||
authApi().getNewAccessLevel(); |
|||
|
|||
export const validateAccessLevels = (hierarchy, actions, accessLevels) => |
|||
authApi(hierarchy, actions).validateAccessLevels(accessLevels); |
|||
@ -0,0 +1,2 @@ |
|||
import feather from "feather-icons"; |
|||
export default icon => feather.toSvg(icon); |
|||
@ -0,0 +1,62 @@ |
|||
<script> |
|||
import Button from "../common/Button.svelte"; |
|||
import ButtonGroup from "../common/ButtonGroup.svelte"; |
|||
import {database} from "../builderStore"; |
|||
import Modal from "../common/Modal.svelte"; |
|||
import ErrorsBox from "../common/ErrorsBox.svelte"; |
|||
|
|||
export let left; |
|||
let confirmDelete = false; |
|||
const openConfirmDelete = () => { |
|||
confirmDelete = true; |
|||
} |
|||
|
|||
const deleteCurrentNode = () => { |
|||
confirmDelete = false; |
|||
database.deleteCurrentNode(); |
|||
} |
|||
|
|||
</script> |
|||
|
|||
<div class="root" style="left: {left}"> |
|||
|
|||
<ButtonGroup> |
|||
<Button color="secondary" grouped on:click={database.saveCurrentNode}> |
|||
{#if $database.currentNodeIsNew} |
|||
Create |
|||
{:else} |
|||
Update |
|||
{/if} |
|||
</Button> |
|||
|
|||
{#if !$database.currentNodeIsNew} |
|||
<Button color="secondary" grouped on:click={openConfirmDelete}> |
|||
Delete |
|||
</Button> |
|||
{/if} |
|||
</ButtonGroup> |
|||
|
|||
{#if !!$database.errors && $database.errors.length > 0} |
|||
<div style="width: 500px"> |
|||
<ErrorsBox errors={$database.errors}/> |
|||
</div> |
|||
{/if} |
|||
|
|||
<Modal bind:isOpen={confirmDelete}> |
|||
<div style="margin: 10px 0px 20px 0px">Are you sure you want to delete {$database.currentNode.name} ?</div> |
|||
<div style="float:right"> |
|||
<Button color="primary" on:click={deleteCurrentNode}>Yes</Button> |
|||
<Button color="secondary" on:click={() => confirmDelete = false}>No</Button> |
|||
</div> |
|||
</Modal> |
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
.root { |
|||
padding:5px; |
|||
top:0; |
|||
width:100%; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,20 @@ |
|||
<script> |
|||
|
|||
import RecordCard from "./RecordCard.svelte"; |
|||
|
|||
export let record = {}; |
|||
|
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<div class="title">{record.name}</div> |
|||
<div class="inner"> |
|||
<div class="node-path">{record.nodeKey()}</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
|
|||
|
|||
</style> |
|||
@ -0,0 +1,129 @@ |
|||
<script> |
|||
|
|||
import HierarchyRow from "./HierarchyRow.svelte"; |
|||
import RecordView from "./RecordView.svelte"; |
|||
import IndexView from "./IndexView.svelte"; |
|||
import ActionsHeader from "./ActionsHeader.svelte"; |
|||
import {database} from "../builderStore"; |
|||
import getIcon from "../common/icon"; |
|||
import DropdownButton from "../common/DropdownButton.svelte"; |
|||
import {hierarchy as hierarchyFunctions} from "budibase-core"; |
|||
|
|||
const hierarchyWidth = "200px"; |
|||
|
|||
const defaultNewIndexActions = [{ |
|||
label:"New Root Index", |
|||
onclick: database.newRootIndex |
|||
}]; |
|||
|
|||
const defaultNewRecordActions = [{ |
|||
label:"New Root Record", |
|||
onclick: database.newRootRecord |
|||
}]; |
|||
|
|||
let newIndexActions = defaultNewIndexActions; |
|||
let newRecordActions = defaultNewRecordActions; |
|||
|
|||
database.subscribe(db => { |
|||
if(!db.currentNode || hierarchyFunctions.isIndex(db.currentNode)) { |
|||
newRecordActions = defaultNewRecordActions; |
|||
newIndexActions = defaultNewIndexActions; |
|||
} else { |
|||
newRecordActions = [ |
|||
...defaultNewRecordActions, |
|||
{label: `New Child Record of ${db.currentNode.name}`, |
|||
onclick: database.newChildRecord} |
|||
]; |
|||
|
|||
newIndexActions = [ |
|||
...defaultNewIndexActions, |
|||
{label: `New Index on ${db.currentNode.name}`, |
|||
onclick: database.newChildIndex} |
|||
]; |
|||
} |
|||
}); |
|||
|
|||
|
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<div class="hierarchy" style="width: {hierarchyWidth}"> |
|||
<div class="hierarchy-title-row"> |
|||
<div class="hierarchy-title">Records</div> |
|||
<DropdownButton iconName="plus" actions={newRecordActions} /> |
|||
</div> |
|||
{#each $database.hierarchy.children as record} |
|||
<HierarchyRow node={record} /> |
|||
{/each} |
|||
|
|||
<div class="hierarchy-title-row" style="margin-top: 20px"> |
|||
<div class="hierarchy-title">Indexes</div> |
|||
<DropdownButton iconName="plus" actions={newIndexActions} /> |
|||
</div> |
|||
{#each $database.hierarchy.indexes as index} |
|||
<HierarchyRow node={index} /> |
|||
{/each} |
|||
</div> |
|||
<div class="node-container"> |
|||
<div class="actions-header"> |
|||
{#if $database.currentNode} |
|||
<ActionsHeader left={hierarchyWidth}/> |
|||
{/if} |
|||
</div> |
|||
<div class="node-view"> |
|||
{#if !$database.currentNode} |
|||
<h1 style="margin-left: 100px">:)</h1> |
|||
{:else if $database.currentNode.type === "record"} |
|||
<RecordView /> |
|||
{:else} |
|||
<IndexView /> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
<style> |
|||
.root { |
|||
display: flex; |
|||
height: 100%; |
|||
position: relative; |
|||
} |
|||
|
|||
.hierarchy { |
|||
flex: 0 1 auto; |
|||
background-color: var(--primary10); |
|||
overflow-y: auto; |
|||
height: 100%; |
|||
} |
|||
|
|||
.node-container { |
|||
flex: 1 1 auto; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.actions-header { |
|||
flex: 0 1 auto; |
|||
} |
|||
|
|||
.node-view { |
|||
overflow-y: auto; |
|||
flex: 1 1 auto; |
|||
} |
|||
|
|||
.hierarchy-title-row { |
|||
padding: 15px 7px; |
|||
/*background-color: var(--secondary75); |
|||
color: var(--white);*/ |
|||
font-size: 11pt; |
|||
display: flex; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.hierarchy-title { |
|||
flex: auto 1 1; |
|||
} |
|||
|
|||
|
|||
</style> |
|||
@ -0,0 +1,117 @@ |
|||
<script> |
|||
import Dropdown from "../common/Dropdown.svelte"; |
|||
import Textbox from "../common/Textbox.svelte"; |
|||
import Button from "../common/Button.svelte"; |
|||
import ButtonGroup from "../common/ButtonGroup.svelte"; |
|||
import NumberBox from "../common/NumberBox.svelte"; |
|||
import ValuesList from "../common/ValuesList.svelte"; |
|||
import ErrorsBox from "../common/ErrorsBox.svelte"; |
|||
import Checkbox from "../common/Checkbox.svelte"; |
|||
import DatePicker from "../common/DatePicker.svelte"; |
|||
import {cloneDeep, assign, keys, |
|||
isNumber, includes, map, isBoolean} from "lodash/fp"; |
|||
import {allTypes, validate, getPotentialReferenceIndexes, |
|||
getDefaultTypeOptions, getNode, |
|||
getPotentialReverseReferenceIndexes} from "../common/core"; |
|||
|
|||
export let field; |
|||
export let allFields; |
|||
export let onFinished = () => {}; |
|||
export let database; |
|||
|
|||
let errors = []; |
|||
let clonedField = cloneDeep(field); |
|||
|
|||
$: isNew = !!field && field.name.length === 0; |
|||
|
|||
$: possibleReferenceIndexes = getPotentialReferenceIndexes( |
|||
database.hierarchy, database.currentNode |
|||
); |
|||
|
|||
$: selectedReverseRefIndex = |
|||
!clonedField.typeOptions.indexNodeKey |
|||
? "" |
|||
: getNode(database.hierarchy, clonedField.typeOptions.indexNodeKey); |
|||
|
|||
$: possibleReverseReferenceIndexes = |
|||
!selectedReverseRefIndex |
|||
? [] |
|||
: getPotentialReverseReferenceIndexes( |
|||
database.hierarchy, selectedReverseRefIndex); |
|||
|
|||
const typeChanged = (ev) => |
|||
clonedField.typeOptions = getDefaultTypeOptions(ev.detail); |
|||
|
|||
|
|||
const save = () => { |
|||
|
|||
errors = validate.field(allFields)(clonedField); |
|||
if(errors.length > 0) return; |
|||
field.typeOptions = cloneDeep(clonedField.typeOptions); |
|||
onFinished( |
|||
assign(field)(clonedField) |
|||
); |
|||
} |
|||
|
|||
</script> |
|||
|
|||
<div class="root"> |
|||
|
|||
<ErrorsBox errors={errors} /> |
|||
|
|||
<Dropdown label="Type" bind:selected={clonedField.type} options={keys(allTypes)} on:change={typeChanged} /> |
|||
|
|||
{#if isNew} |
|||
<Textbox label="Field Name" bind:text={clonedField.name} /> |
|||
{:else} |
|||
<div style="font-weight: bold">{clonedField.name}</div> |
|||
{/if} |
|||
|
|||
<Textbox label="Label" bind:text={clonedField.label} /> |
|||
|
|||
{#if clonedField.type === "string"} |
|||
<NumberBox label="Max Length" bind:value={clonedField.typeOptions.maxLength} /> |
|||
<ValuesList label="Values (options)" bind:values={clonedField.typeOptions.values} /> |
|||
<Checkbox label="Declared Values Only" bind:checked={clonedField.typeOptions.allowDeclaredValuesOnly} /> |
|||
{:else if clonedField.type === "bool"} |
|||
<Checkbox label="Allow Null" bind:checked={clonedField.typeOptions.allowNulls} /> |
|||
{:else if clonedField.type === "datetime"} |
|||
<DatePicker label="Min Value" bind:value={clonedField.typeOptions.minValue} /> |
|||
<DatePicker label="Max Value" bind:value={clonedField.typeOptions.maxValue} /> |
|||
{:else if clonedField.type === "number"} |
|||
<NumberBox label="Min Value" bind:value={clonedField.typeOptions.minValue} /> |
|||
<NumberBox label="Max Value" bind:value={clonedField.typeOptions.maxValue} /> |
|||
<NumberBox label="Decimal Places" bind:value={clonedField.typeOptions.decimalPlaces} /> |
|||
{:else if clonedField.type === "reference"} |
|||
<Dropdown label="Lookup Index" |
|||
options={possibleReferenceIndexes} |
|||
valueMember={n => n.nodeKey()} |
|||
textMember={n => n.name} |
|||
bind:selected={clonedField.typeOptions.indexNodeKey} /> |
|||
|
|||
<Dropdown label="Reverse Reference Index" |
|||
options={possibleReverseReferenceIndexes} |
|||
multiple=true |
|||
valueMember={n => n.nodeKey()} |
|||
textMember={n => n.name} |
|||
bind:selected={clonedField.typeOptions.reverseIndexNodeKeys} /> |
|||
|
|||
<Textbox label="Display Value" bind:text={clonedField.typeOptions.displayValue} /> |
|||
|
|||
{:else if clonedField.type.startsWith("array")} |
|||
<NumberBox label="Min Length" bind:value={clonedField.typeOptions.minLength} /> |
|||
<NumberBox label="Max Length" bind:value={clonedField.typeOptions.maxLength} /> |
|||
{/if} |
|||
|
|||
<ButtonGroup style="float: right;"> |
|||
<Button color="primary" grouped on:click={save}>Save</Button> |
|||
<Button color="secondary" grouped on:click={() => onFinished(false)}>Cancel</Button> |
|||
</ButtonGroup> |
|||
|
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
|
|||
|
|||
</style> |
|||
@ -0,0 +1,44 @@ |
|||
<script> |
|||
|
|||
import {database} from "../builderStore"; |
|||
import {cloneDeep} from "lodash"; |
|||
export let level = 0; |
|||
export let node; |
|||
|
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<div class="title" on:click={() => database.selectExistingNode(node.nodeId)} style="padding-left: {20 + (level * 20)}px"> |
|||
{node.name} |
|||
</div> |
|||
{#if node.children} |
|||
{#each node.children as child} |
|||
<svelte:self node={child} |
|||
level={level+1}/> |
|||
{/each} |
|||
{/if} |
|||
</div> |
|||
|
|||
|
|||
<style> |
|||
|
|||
.root { |
|||
display: block; |
|||
font-size: 13pt; |
|||
width: 100%; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.title { |
|||
font: var(--bodytext); |
|||
padding-top: 10px; |
|||
padding-right: 5px; |
|||
padding-bottom: 10px; |
|||
color: var(--secondary100); |
|||
} |
|||
|
|||
.title:hover { |
|||
background-color: var(--secondary10); |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,76 @@ |
|||
<script> |
|||
|
|||
import Textbox from "../common/Textbox.svelte"; |
|||
import CodeArea from "../common/CodeArea.svelte"; |
|||
import Button from "../common/Button.svelte"; |
|||
import Dropdown from "../common/Dropdown.svelte"; |
|||
import {database} from "../builderStore"; |
|||
import {filter, some, map} from "lodash/fp"; |
|||
import {hierarchy as hierarchyFunctions, common} from "budibase-core"; |
|||
|
|||
const chain = common.$; |
|||
|
|||
let index; |
|||
let indexableRecords = []; |
|||
|
|||
database.subscribe($database => { |
|||
index = $database.currentNode; |
|||
indexableRecords = chain($database.hierarchy,[ |
|||
hierarchyFunctions.getFlattenedHierarchy, |
|||
filter(hierarchyFunctions.isDecendant(index.parent())), |
|||
filter(hierarchyFunctions.isRecord), |
|||
map(n => ({ |
|||
node:n, |
|||
isallowed: some(id => n.nodeId === id)(index.allowedRecordNodeIds) |
|||
})) |
|||
]); |
|||
}); |
|||
|
|||
const toggleAllowedRecord = record => { |
|||
if(record.isallowed) { |
|||
index.allowedRecordNodeIds = filter(id => id !== record.node.nodeId) |
|||
(index.allowedRecordNodeIds); |
|||
} else { |
|||
index.allowedRecordNodeIds.push(record.node.nodeId); |
|||
} |
|||
}; |
|||
|
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<Textbox bind:text={index.name} label="Name"/> |
|||
|
|||
<div class="allowed-records"> |
|||
<div>Records to Index</div> |
|||
{#each indexableRecords as rec} |
|||
<input type="checkbox" checked={rec.isallowed} on:change={() => toggleAllowedRecord(rec)}/> |
|||
<span>{rec.node.name}</span> |
|||
{/each} |
|||
</div> |
|||
|
|||
|
|||
<Dropdown label="Index Type" bind:selected={index.indexType} options={["ancestor", "reference"]} /> |
|||
|
|||
<CodeArea bind:text={index.map} label="Map (javascript)"/> |
|||
<CodeArea bind:text={index.filter} label="Filter (javascript expression)"/> |
|||
<CodeArea bind:text={index.getShardName} label="Shard Name (javascript expression)"/> |
|||
|
|||
|
|||
</div> |
|||
|
|||
<style> |
|||
|
|||
.root { |
|||
height: 100%; |
|||
padding: 15px; |
|||
} |
|||
|
|||
.allowed-records { |
|||
margin: 20px 0px; |
|||
} |
|||
|
|||
.allowed-records > span { |
|||
margin-right:30px; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,258 @@ |
|||
<script> |
|||
|
|||
import Textbox from "../common/Textbox.svelte"; |
|||
import Button from "../common/Button.svelte"; |
|||
import getIcon from "../common/icon"; |
|||
import FieldView from "./FieldView.svelte"; |
|||
import Modal from "../common/Modal.svelte"; |
|||
import {map, join, filter, some, |
|||
find, keys, isDate} from "lodash/fp"; |
|||
import { database } from "../builderStore"; |
|||
import {common, hierarchy as h} from "budibase-core"; |
|||
import {templateApi, chain, validate} from "../common/core"; |
|||
|
|||
let record; |
|||
let getIndexAllowedRecords; |
|||
let editingField = false; |
|||
let fieldToEdit; |
|||
let isNewField = false; |
|||
let newField; |
|||
let editField; |
|||
let deleteField; |
|||
let onFinishedFieldEdit; |
|||
let editIndex; |
|||
|
|||
database.subscribe($database => { |
|||
record = $database.currentNode; |
|||
const flattened = h.getFlattenedHierarchy($database.hierarchy); |
|||
getIndexAllowedRecords = index => |
|||
chain(index.allowedRecordNodeIds, [ |
|||
filter(id => some(n => n.nodeId === id)(flattened)), |
|||
map(id => find(n => n.nodeId === id) |
|||
(flattened).name), |
|||
join(", ") |
|||
]); |
|||
|
|||
newField = () => { |
|||
isNewField = true; |
|||
fieldToEdit = templateApi($database.hierarchy).getNewField("string"); |
|||
editingField = true; |
|||
} |
|||
|
|||
onFinishedFieldEdit = (field) => { |
|||
if(field) { |
|||
database.saveField(field); |
|||
} |
|||
editingField = false; |
|||
} |
|||
|
|||
editField = (field) => { |
|||
isNewField = false; |
|||
fieldToEdit = field; |
|||
editingField = true; |
|||
} |
|||
|
|||
deleteField = (field) => { |
|||
database.deleteField(field); |
|||
} |
|||
|
|||
editIndex = index => { |
|||
database.selectExistingNode(index.nodeId); |
|||
} |
|||
|
|||
}) |
|||
|
|||
let getTypeOptionsValueText = value => { |
|||
if(value === Number.MAX_SAFE_INTEGER |
|||
|| value === Number.MIN_SAFE_INTEGER |
|||
|| new Date(value).getTime() === new Date(8640000000000000).getTime() |
|||
|| new Date(value).getTime() === new Date(-8640000000000000).getTime()) return "(any)"; |
|||
|
|||
if(value === null) return "(not set)"; |
|||
return value; |
|||
} |
|||
|
|||
let getTypeOptions = typeOptions => |
|||
chain(typeOptions, [ |
|||
keys, |
|||
map(k => `<span style="color:var(--slate)">${k}: </span>${getTypeOptionsValueText(typeOptions[k])}`), |
|||
join("<br>") |
|||
]); |
|||
|
|||
|
|||
</script> |
|||
|
|||
<div class="root"> |
|||
|
|||
<Textbox label="Name" bind:text={record.name} /> |
|||
<div>{record.nodeKey()}</div> |
|||
{#if !record.isSingle} |
|||
<Textbox label="Collection Name" bind:text={record.collectionName} /> |
|||
<Textbox label="Shard Factor" bind:text={record.allidsShardFactor} /> |
|||
{/if} |
|||
|
|||
<h4> |
|||
Fields <span class="add-field-button" on:click={newField}>{@html getIcon("plus")}</span> |
|||
</h4> |
|||
|
|||
{#if record.fields.length > 0} |
|||
<table class="fields-table uk-table"> |
|||
<thead> |
|||
<tr> |
|||
<th>Name</th> |
|||
<th>Type</th> |
|||
<th>Options</th> |
|||
<th></th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
{#each record.fields as field} |
|||
<tr> |
|||
<td > |
|||
<div>{field.label}</div> |
|||
<div style="font-size: 0.7em; color: var(--slate)">{field.name}</div> |
|||
</td> |
|||
<td >{field.type}</td> |
|||
<td >{@html getTypeOptions(field.typeOptions)}</td> |
|||
<td> |
|||
<span class="edit-button" on:click={() => editField(field)}>{@html getIcon("edit")}</span> |
|||
<span class="edit-button" on:click={() => deleteField(field)}>{@html getIcon("trash")}</span> |
|||
</td> |
|||
</tr> |
|||
{/each} |
|||
</tbody> |
|||
</table> |
|||
{:else} |
|||
(no fields added) |
|||
{/if} |
|||
|
|||
{#if editingField} |
|||
<Modal bind:isOpen={editingField}> |
|||
<FieldView field={fieldToEdit} |
|||
onFinished={onFinishedFieldEdit} |
|||
allFields={record.fields} |
|||
database={$database}/> |
|||
</Modal> |
|||
{/if} |
|||
|
|||
<h4> |
|||
Indexes |
|||
</h4> |
|||
|
|||
{#each record.indexes as index} |
|||
<div class="index-container"> |
|||
<div class="index-name"> |
|||
{index.name} |
|||
<span style="margin-left: 7px" on:click={() => editIndex(index)}>{@html getIcon("edit")}</span> |
|||
</div> |
|||
<div class="index-field-row"> |
|||
<span class="index-label">records indexed: </span> |
|||
<span>{getIndexAllowedRecords(index)}</span> |
|||
<span class="index-label" style="margin-left: 15px">type:</span> |
|||
<span>{index.indexType}</span> |
|||
</div> |
|||
<div class="index-field-row"> |
|||
<span class="index-label">map:</span> |
|||
<code class="index-mapfilter">{index.map}</code> |
|||
</div> |
|||
{#if index.filter} |
|||
<div class="index-field-row"> |
|||
<span class="index-label">filter:</span> |
|||
<code class="index-mapfilter">{index.filter}</code> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
{:else} |
|||
(no indexes added) |
|||
{/each} |
|||
|
|||
</div> |
|||
|
|||
|
|||
<style> |
|||
|
|||
.root { |
|||
height: 100%; |
|||
padding: 15px; |
|||
} |
|||
|
|||
.fields-table { |
|||
margin:10px; |
|||
border-collapse:collapse; |
|||
} |
|||
|
|||
.add-field-button { |
|||
margin-left:15px; |
|||
cursor:pointer; |
|||
} |
|||
|
|||
.edit-button { |
|||
cursor:pointer; |
|||
color: var(--white); |
|||
} |
|||
|
|||
.edit-button:hover { |
|||
color: var(--secondary75); |
|||
} |
|||
|
|||
th { |
|||
text-align: left; |
|||
} |
|||
|
|||
td { |
|||
padding: 5px 30px 5px 0px; |
|||
margin:0; |
|||
|
|||
} |
|||
|
|||
thead > tr { |
|||
border-width: 0px 0px 1px 0px; |
|||
border-style: solid; |
|||
border-color: var(--secondary75); |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
tbody > tr { |
|||
border-width: 0px 0px 1px 0px; |
|||
border-style: solid; |
|||
border-color: var(--primary10); |
|||
} |
|||
|
|||
tbody > tr:hover { |
|||
background-color: var(--primary10); |
|||
} |
|||
|
|||
tbody > tr:hover .edit-button { |
|||
color: var(--secondary75); |
|||
} |
|||
|
|||
.index-container { |
|||
border-style: solid; |
|||
border-width: 0 0 1px 0; |
|||
border-color: var(--secondary25); |
|||
padding: 10px; |
|||
margin-bottom: 5px; |
|||
} |
|||
|
|||
.index-label { |
|||
color: var(--slate); |
|||
} |
|||
|
|||
.index-name { |
|||
font-weight: bold; |
|||
color: var(--primary100); |
|||
} |
|||
|
|||
.index-container code { |
|||
margin: 0; |
|||
display: inline; |
|||
background-color: var(--primary10); |
|||
color: var(--secondary100); |
|||
padding:3px; |
|||
} |
|||
|
|||
.index-field-row { |
|||
margin-top: 7px; |
|||
} |
|||
|
|||
</style> |
|||
|
After Width: | Height: | Size: 8.8 KiB |
@ -0,0 +1,61 @@ |
|||
/* latin-ext */ |
|||
@font-face { |
|||
font-family: 'Lato'; |
|||
font-weight: 400; |
|||
src: local('Lato Regular'), local('Lato-Regular'), url(./assets/lato-latin-ext.woff2) format('woff2'); |
|||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; |
|||
} |
|||
/* latin */ |
|||
@font-face { |
|||
font-family: 'Lato'; |
|||
font-weight: 400; |
|||
src: local('Lato Regular'), local('Lato-Regular'), url(./assets/lato-latin.woff2) format('woff2'); |
|||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; |
|||
} |
|||
|
|||
|
|||
/* Black latin-ext */ |
|||
@font-face { |
|||
font-family: 'Lato Black'; |
|||
font-weight: 900; |
|||
src: local('Lato Regular'), local('Lato-Regular'), url(./assets/lato-latin-ext.woff2) format('woff2'); |
|||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; |
|||
} |
|||
/* Black latin */ |
|||
@font-face { |
|||
font-family: 'Lato Black'; |
|||
font-weight: 900; |
|||
src: local('Lato Regular'), local('Lato-Regular'), url(./assets/lato-latin.woff2) format('woff2'); |
|||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; |
|||
} |
|||
|
|||
/* Bold latin-ext */ |
|||
@font-face { |
|||
font-family: 'Lato Bold'; |
|||
font-weight: 700; |
|||
src: local('Lato Regular'), local('Lato-Regular'), url(./assets/lato-latin-ext.woff2) format('woff2'); |
|||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; |
|||
} |
|||
/* Bold latin */ |
|||
@font-face { |
|||
font-family: 'Lato Bold'; |
|||
font-weight: 700; |
|||
src: local('Lato Regular'), local('Lato-Regular'), url(./assets/lato-latin.woff2) format('woff2'); |
|||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; |
|||
} |
|||
|
|||
/* Semi Bold latin-ext */ |
|||
@font-face { |
|||
font-family: 'Lato Semi Bold'; |
|||
font-weight: 600; |
|||
src: local('Lato Regular'), local('Lato-Regular'), url(./assets/lato-latin-ext.woff2) format('woff2'); |
|||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; |
|||
} |
|||
/* Semi Bold latin */ |
|||
@font-face { |
|||
font-family: 'Lato Semi Bold'; |
|||
font-weight: 600; |
|||
src: local('Lato Regular'), local('Lato-Regular'), url(./assets/lato-latin.woff2) format('woff2'); |
|||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; |
|||
} |
|||
|
|||
@ -0,0 +1,91 @@ |
|||
:root { |
|||
--primary100: #454CA0FF; |
|||
--primary75: #454CA0BF; |
|||
--primary25: #454CA040; |
|||
--primary10: #454CA01A; |
|||
--primarydark: #3F448A; |
|||
|
|||
--secondary100: #162B4DFF; |
|||
--secondary75: #162B4DBF; |
|||
--secondary25: #162B4D40; |
|||
--secondary10: #162B4D1A; |
|||
--secondarydark: #3F448A; |
|||
|
|||
--tertiary: #F2F5F7; |
|||
|
|||
--success100: #49C39EFF; |
|||
--success75: #49C39EBF; |
|||
--success25: #49C39E40; |
|||
--success10: #49C39E1A; |
|||
--successdark: #44B492; |
|||
|
|||
--deletion100: #F2545BFF; |
|||
--deletion75: #F2545BBF; |
|||
--deletion25: #F2545B40; |
|||
--deletion10: #F2545B1A; |
|||
--deletiondark: #CF4046; |
|||
|
|||
--white: #FFFFFF; |
|||
--darkslate: #5C6B82; |
|||
--slate: #8B95A6; |
|||
|
|||
--borderradius: 2px; |
|||
--borderradiusall: 2px 2px 2px 2px; |
|||
|
|||
--fontblack: "Lato Black"; |
|||
--fontbold: "Lato Bold"; |
|||
--fontsemibold: "Lato Semi Bold"; |
|||
--fontnormal: "Lato"; |
|||
|
|||
--bodytext: var(--fontnormal) "regular" var(--secondary100) 16pt; |
|||
--bigbodytext: var(--fontnormal) "regular" var(--secondary100) 20pt; |
|||
--smallbodytext: var(--fontnormal) "regular" var(--secondary100) 12pt; |
|||
--lightbodytext: var(--fontnormal) "regular" var(--darkslate) 16pt; |
|||
--heavybodytext: var(--fontbold) "regular" var(--secondary100) 16pt; |
|||
--quotation: var(--fontnormal) "italics" var(--darkslate) 16pt; |
|||
--smallheavybodytext: var(--fontbold) "regular" var(--secondary100) 14pt; |
|||
|
|||
} |
|||
|
|||
html, body { |
|||
font-family: var(--fontnormal); |
|||
color: var(--secondary100); |
|||
padding: 0; |
|||
margin: 0; |
|||
} |
|||
|
|||
h1 { |
|||
font-family: var(--fontblack); |
|||
font-size: 36pt; |
|||
color: var(--secondary100); |
|||
} |
|||
|
|||
h2 { |
|||
font-family: var(--fontbold); |
|||
font-size: 30pt; |
|||
color: var(--secondary100); |
|||
} |
|||
|
|||
h3 { |
|||
font-family: var(--fontbold); |
|||
font-size: 24pt; |
|||
color: var(--darkslate); |
|||
} |
|||
|
|||
h4 { |
|||
font-family: var(--fontsemibold); |
|||
font-size: 18pt; |
|||
color: var(--secondary100); |
|||
} |
|||
|
|||
h5 { |
|||
font-family: var(--fontblack); |
|||
font-size: 14pt; |
|||
color: var(--secondary100); |
|||
} |
|||
|
|||
h5 { |
|||
font-family: var(--fontblack); |
|||
font-size: 12pt; |
|||
color: var(--darkslate); |
|||
} |
|||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,18 @@ |
|||
<!doctype html> |
|||
<html> |
|||
<head> |
|||
<meta charset='utf8'> |
|||
<meta name='viewport' content='width=device-width'> |
|||
|
|||
<title>Budibase Builder</title> |
|||
|
|||
<link rel='icon' type='image/png' href='/favicon.png'> |
|||
<link rel='stylesheet' href='/global.css'> |
|||
<link rel='stylesheet' href='/bundle.css'> |
|||
<link rel='stylesheet' href="/uikit.min.css"> |
|||
</head> |
|||
|
|||
<body id="app"> |
|||
<script src='/bundle.js'></script> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,15 @@ |
|||
import App from "./App.svelte"; |
|||
import "./global.css"; |
|||
import "./fonts.css"; |
|||
import "./assets/lato-latin-ext.woff2"; |
|||
import "./assets/lato-latin.woff2"; |
|||
import "./assets/budibase-logo.png"; |
|||
import "./assets/budibase-logo-only.png"; |
|||
import "uikit/dist/css/uikit.min.css"; |
|||
import "uikit/dist/js/uikit.min.js"; |
|||
|
|||
const app = new App({ |
|||
target: document.getElementById("app") |
|||
}); |
|||
|
|||
|
|||
@ -0,0 +1,38 @@ |
|||
<script> |
|||
|
|||
import NavItem from "./NavItem.svelte"; |
|||
|
|||
export let width=50; |
|||
|
|||
</script> |
|||
|
|||
<nav class="nav" |
|||
style="width: {width};"> |
|||
<img src="./assets/budibase-logo-only.png" class="logo" alt="budibase logo"/> |
|||
<NavItem name="database" label="Database" icon="database" /> |
|||
<NavItem name="actions" label="Actions" icon="zap" /> |
|||
<NavItem name="access levels" label="Access Levels" icon="user" /> |
|||
<NavItem name="user interface" label="User Interface" icon="monitor"/> |
|||
<NavItem name="package" label="Package" icon="package"/> |
|||
</nav> |
|||
|
|||
|
|||
|
|||
<style> |
|||
|
|||
.nav { |
|||
height: 100%; |
|||
position: fixed; |
|||
left: 0px; |
|||
background-color: var(--secondary100); |
|||
color: var(--darkslate); |
|||
} |
|||
|
|||
.nav > img { |
|||
width:100%; |
|||
margin-bottom: 30px; |
|||
margin-top: 5px; |
|||
margin-left:0px; |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,75 @@ |
|||
<script> |
|||
|
|||
import {database} from "../builderStore"; |
|||
import getIcon from "../common/icon"; |
|||
|
|||
export let name = ""; |
|||
export let label = ""; |
|||
export let icon = ""; |
|||
|
|||
let navActive = ""; |
|||
|
|||
database.subscribe(db => { |
|||
navActive = (db.activeNav === name ? "active" : "") |
|||
}); |
|||
|
|||
const setActive = () => |
|||
database.setActiveNav(name); |
|||
|
|||
</script> |
|||
|
|||
<div class="{navActive}"> |
|||
<div class="nav-item" |
|||
on:click={setActive}> |
|||
<div class="inner"> |
|||
<div class="icon">{@html getIcon(icon)}</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
<style> |
|||
|
|||
.nav-item { |
|||
padding: 0px 5px; |
|||
display: block; |
|||
padding: 10px; |
|||
color: var(--slate); |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.inner { |
|||
padding: 0px 20px 10px 0px; |
|||
display: inline-block; |
|||
width:100%; |
|||
} |
|||
|
|||
.nav-item:hover { |
|||
background-color: var(--primary25); |
|||
} |
|||
|
|||
.icon { |
|||
font-size:0.9em; |
|||
display: inline-block; |
|||
position: relative; |
|||
top: 5px; |
|||
margin-right:5px; |
|||
width:100%; |
|||
} |
|||
|
|||
.active > div { |
|||
background-color: var(--primary10); |
|||
color: var(--secondary100); |
|||
} |
|||
|
|||
.active>div:hover { |
|||
background-color: var(--slate); |
|||
color: var(--secondary100); |
|||
} |
|||
|
|||
.active { |
|||
background-color: white; |
|||
} |
|||
|
|||
|
|||
</style> |
|||
@ -0,0 +1,11 @@ |
|||
<script> |
|||
|
|||
import ComingSoon from "../common/ComingSoon.svelte"; |
|||
|
|||
</script> |
|||
|
|||
<ComingSoon name="User Interface Designer"/> |
|||
|
|||
<style> |
|||
|
|||
</style> |
|||
@ -1 +1 @@ |
|||
Subproject commit ab14ab9db1698a82b1f98b692ecfa6b71c17752a |
|||
Subproject commit d418f210e2af6f6a37d590ec255730a0da2241c7 |
|||
@ -1,75 +0,0 @@ |
|||
{ |
|||
"levels": [ |
|||
{ |
|||
"name": "owner", |
|||
"permissions": [ |
|||
{ |
|||
"type": "create record", |
|||
"nodeKey": "/customers/1-{id}" |
|||
}, |
|||
{ |
|||
"type": "delete record", |
|||
"nodeKey": "/customers/1-{id}" |
|||
}, |
|||
{ |
|||
"type": "update record", |
|||
"nodeKey": "/customers/1-{id}" |
|||
}, |
|||
{ |
|||
"type": "read record", |
|||
"nodeKey": "/customers/1-{id}" |
|||
}, |
|||
{ |
|||
"type": "create record", |
|||
"nodeKey": "/customers/1-{id}/invoices/2-{id}" |
|||
}, |
|||
{ |
|||
"type": "update record", |
|||
"nodeKey": "/customers/1-{id}/invoices/2-{id}" |
|||
}, |
|||
{ |
|||
"type": "delete record", |
|||
"nodeKey": "/customers/1-{id}/invoices/2-{id}" |
|||
}, |
|||
{ |
|||
"type": "read record", |
|||
"nodeKey": "/customers/1-{id}/invoices/2-{id}" |
|||
}, |
|||
{ |
|||
"type": "write templates" |
|||
}, |
|||
{ |
|||
"type": "create user" |
|||
}, |
|||
{ |
|||
"type": "set password" |
|||
}, |
|||
{ |
|||
"type": "create temporary access" |
|||
}, |
|||
{ |
|||
"type": "enable or disable user" |
|||
}, |
|||
{ |
|||
"type": "write access levels" |
|||
}, |
|||
{ |
|||
"type": "list users" |
|||
}, |
|||
{ |
|||
"type": "list access levels" |
|||
}, |
|||
{ |
|||
"type": "manage index" |
|||
}, |
|||
{ |
|||
"type": "set user access levels" |
|||
}, |
|||
{ |
|||
"type": "manage collection" |
|||
} |
|||
] |
|||
} |
|||
], |
|||
"version": 0 |
|||
} |
|||
@ -1,84 +0,0 @@ |
|||
{ |
|||
"hierarchy": { |
|||
"name": "root", |
|||
"type": "root", |
|||
"children": [ |
|||
{ |
|||
"name": "customer", |
|||
"type": "record", |
|||
"fields": [ |
|||
{ |
|||
"name": "name", |
|||
"type": "string", |
|||
"typeOptions": { |
|||
"maxLength": 1000, |
|||
"values": null, |
|||
"allowDeclaredValuesOnly": false |
|||
}, |
|||
"label": "name", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
} |
|||
], |
|||
"children": [ |
|||
{ |
|||
"name": "invoice", |
|||
"type": "record", |
|||
"fields": [ |
|||
{ |
|||
"name": "amount", |
|||
"type": "number", |
|||
"typeOptions": { |
|||
"minValue": 99999999999, |
|||
"maxValue": 99999999999, |
|||
"decimalPlaces": 2 |
|||
}, |
|||
"label": "amount", |
|||
"getInitialValue": "default", |
|||
"getUndefinedValue": "default" |
|||
} |
|||
], |
|||
"children": [], |
|||
"validationRules": [], |
|||
"nodeId": 2, |
|||
"indexes": [], |
|||
"allidsShardFactor": 1, |
|||
"collectionName": "invoices", |
|||
"isSingle": false |
|||
} |
|||
], |
|||
"validationRules": [], |
|||
"nodeId": 1, |
|||
"indexes": [], |
|||
"allidsShardFactor": 64, |
|||
"collectionName": "customers", |
|||
"isSingle": false |
|||
} |
|||
], |
|||
"pathMaps": [], |
|||
"indexes": [], |
|||
"nodeId": 0 |
|||
}, |
|||
"actions": { |
|||
"output_to_file": { |
|||
"name": "output_to_file", |
|||
"behaviourSource": "main", |
|||
"behaviourName": "outputToFile", |
|||
"initialOptions": {} |
|||
} |
|||
}, |
|||
"triggers": [ |
|||
{ |
|||
"actionName": "output_to_file", |
|||
"eventName": "authApi:createUser:onComplete", |
|||
"optionsCreator": "return { filename:'tempaccess' + context.user.name, content:context.result.tempCode };", |
|||
"condition": "!context.password" |
|||
}, |
|||
{ |
|||
"actionName": "output_to_file", |
|||
"eventName": "authApi:createTemporaryAccess:onComplete", |
|||
"optionsCreator": "return { filename:'tempaccess' + context.userName, content:context.result };", |
|||
"condition": "" |
|||
} |
|||
] |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
const fs = require("fs"); |
|||
|
|||
module.exports = (config) => ({ |
|||
main: { |
|||
outputToFile : ({filename, content}) => { |
|||
fs.writeFile(`./tests/.data/${filename}`, content, {encoding:"utf8"}); |
|||
} |
|||
} |
|||
}) |
|||
@ -0,0 +1,31 @@ |
|||
const { appPackageFolder } = require("./createAppPackage"); |
|||
const { writeFile, readFile } = require("./fsawait"); |
|||
|
|||
module.exports.getPackageForBuilder = async (config, appname) => { |
|||
const appPath = appPackageFolder(config, appname); |
|||
return ({ |
|||
appDefinition: JSON.parse(await readFile( |
|||
`${appPath}/appDefinition.json`, |
|||
"utf8")), |
|||
|
|||
accessLevels: JSON.parse(await readFile( |
|||
`${appPath}/access_levels.json`, |
|||
"utf8")) |
|||
}) |
|||
|
|||
} |
|||
|
|||
|
|||
module.exports.savePackage = async (config, appname, pkg) => { |
|||
const appPath = appPackageFolder(config, appname); |
|||
await writeFile( |
|||
`${appPath}/appDefinition.json`, |
|||
JSON.stringify(pkg.appDefinition), |
|||
"utf8"); |
|||
|
|||
await writeFile( |
|||
`${appPath}/access_levels.json`, |
|||
JSON.stringify(pkg.accessLevels), |
|||
"utf8"); |
|||
|
|||
} |
|||