This commit is contained in:
2025-11-17 18:45:35 +01:00
parent 0f58e3bdff
commit 14d6f9aa73
7607 changed files with 1969407 additions and 0 deletions

1
lowcoder/.gitbook.yaml Normal file
View File

@@ -0,0 +1 @@
root: ./docs/

24
lowcoder/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
.idea/
logs/
stacks/
client/.yarn/cache/*.zip
server/node-service/.yarn/cache/*.zip
.metadata/
.DS_Store
client/node_modules/
client/packages/lowcoder-plugin-demo/.yarn/install-state.gz
client/packages/lowcoder-plugin-demo/yarn.lock
client/packages/lowcoder-plugin-demo/.yarn/cache/@types-node-npm-16.18.68-56f72825c0-094ae9ed80.zip
application-dev.yml
application-lowcoder.yml
application-debug.yaml
application-dev-localhost.yaml
.vscode/settings.json
.vscode/launch.json
server/api-service/lowcoder-server/src/main/resources/application-local-dev.yaml
translations/locales/node_modules/
server/api-service/lowcoder-server/src/main/resources/application-local-dev-ee.yaml
node_modules
# Local Netlify folder
.netlify

661
lowcoder/LICENSE Normal file
View File

@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://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 <http://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
<http://www.gnu.org/licenses/>.

20
lowcoder/MANIFESTO.md Normal file
View File

@@ -0,0 +1,20 @@
# Lowcoder Manifesto
Dear Community,
We are excited to announce a new initiative to support and extend Openblocks, which unfortunately has been abandoned by its original team. Openblocks has been a critical part of many of our workflows, and we believe it's essential to keep it alive and thriving.
We have come together out of a shared passion for the project and a commitment to providing a toolchain that allows you to create apps blazingly fast using a simple, intuitive UI. We are dedicated to continuing the project's legacy under a new name, Lowcoder.
To provide continuous support to all members of the community, we have decided to form two individual teams:
_Lowcoder.org_ will remain fully open source, accessible to all, and free from any restrictions or limitations. As part of our commitment to the community, we will take over all maintenance, bug fixes, and feature development. We plan to prioritize stability and reliability while also introducing new features and enhancements to keep pace with evolving technology. We will maintain open communication channels, seek feedback, and actively engage with users and contributors to ensure that Lowcoder remains relevant and useful to all. We will be transparent about our development roadmap, progress, and any challenges that arise.
_Lowcoder.cloud_ will actively develop and maintain the enterprise version and provide a managed cloud offering using both the latest Lowcoder.org version and the proprietary Enterprise version.
The two teams will collaborate and support each other, focusing on a long-term partnership to ensure a healthy product and community.
We invite all members of the community to join us on this journey. Whether you are a user, contributor, or enterprise customer, your input and support are critical to the success of Lowcoder. Let's work together to keep this project alive and thriving for many years to come.
Sincerely,
The Lowcoder Team

116
lowcoder/README.md Normal file
View File

@@ -0,0 +1,116 @@
> **Note**
> Lowcoder continues from the abandoned Openblocks project. For more information [read our manifesto](MANIFESTO.md).
<div align="center">
<h1 style="border-bottom: none; margin-bottom: 0">Lowcoder</h1>
<h3 style="margin-top: 0">This is the only Platform, which closes the gap between App Development, Website Development, interactive Slides/Presentations and Collaboration Tools!</h3>
<p>
Create software applications (internal and customer-facing!) and Meeting/Collaboration tools for your Company and your Customers with minimal coding experience.
</p>
<h3 style="margin-top: 0">We think, Lowcoder is simply better than Retool, Appsmith Tooljet, Outsystems or Mendix.</h3>
</div>
---
## 🎥 Lowcoder Intro Video
<div align="center">
<a href="https://www.youtube.com/watch?v=AQo0iFWUWiU" target="_blank">
<img src="https://img.youtube.com/vi/AQo0iFWUWiU/maxresdefault.jpg" alt="Lowcoder Intro Video" width="100%">
</a>
<p><i>Click the image above to watch the video on YouTube</i> 📺</p>
</div>
---
## 📢 Use Lowcoder in 3 steps
1. Connect to any data sources or APIs.
2. Build flexible and responsive UI with 120+ components and free layout / design possibilities.
3. Share with colleagues and customers.
## 💡 Why Lowcoder
One platform for everything instead so many different softwares. (like Website Builders, CMS, CRM, POS, ERP, Dashboards & Data Story Visualization, Collaboration Tools).
It's cumbersome to create a single app. You had to design user interfaces, write code in multiple languages and frameworks, and understand how all of that code works together.
NewGen Lowcode Platforms like Retool and others are great for their simplicity and flexibility - like Lowcoder too, but they can also be limited in different ways, especially when it comes to "external" applications for everyone - because their pricing focusses to internal apps and "pay per User".
With Lowcoder we did a step forward. More specifically, Lowcoder is:
- An all-in-one IDE to create internal or customer-facing (external) apps.
- A place to create, build and share building blocks of web applications and whole websites.
- The tool and community to support your business, and lower the cost and time to develop interactive applications.
- The only platform to embed Lowcode Apps natively in Websites (no iFrame!)
- The only platform where you can build your own Meeting Tool - like Teams, Zoom or Google Meets, - just in the Lowcode way.
- The only platform which has extensibility plugin architecture [Check Community Contributions](https://www.npmjs.com/search?q=lowcoder-comp)
## 🪄 Features
- **Visual UI builder** with 120+ built-in components. Save 90% of time to build apps.
- **Modules** for reusable (!) embedable component sets in the UI builder.
- **Embed Lowcoder Apps as native parts of any Website** instead of iFrame (!). [Demo](http://demo-lowcoder.42web.io/ecommerce/), [Docu](https://docs.lowcoder.cloud/lowcoder-documentation/lowcoder-extension/native-embed-sdk)
- **Video Meeting Components** to create your own individual Web-Meeting tool.
- **Query Library** for reusable data queries of your data sources.
- **Custom components** to develop own components and use them in the UI builder.
- **Native Data connections** to PostgreSQL, MongoDB, MySQL, Redis, Elasticsearch, REST API, SMTP, etc.
- **Stream Data connections** to Websockets for realtime data updates & collaboration! [Docu](https://docs.lowcoder.cloud/lowcoder-documentation/connect-your-data/data-sources-in-lowcoder/websocket-datasource)
- **JavaScript supported everywhere** to transform data, control components, etc.
- **Role-based access control (RBAC)** for granular permission management. [Docu](https://docs.lowcoder.cloud/lowcoder-documentation/workspaces-and-teamwork/members-and-groups)
- **Auto-saved and restorable history** for release and version control.
- **App Themes and Theme Editor** to precisely align with your company's brand guidelines.
- **Self Hosting** to use Lowcoder in your internal company network, even behind the firewall. [Docu](https://docs.lowcoder.cloud/lowcoder-documentation/setup-and-run/self-hosting)
- **Free Community Cloud** to start within a minute and build your first Apps. [Start here](https://app.lowcoder.cloud)
## 🏆 Comparisons
### Lowcoder vs Teams, Google Meets, Zoom
- build a Meeting tool with peace in mind. Blue buttons - ok. Red corners or circle Videostream - ok too.
- embedd applications in your Video-Meetings, so attendees can enjoy collaborative "anything". From shopping to working and gaming...
### Lowcoder vs Powerapps
- build a apps way faster than in Power Apps. Save up to 50& of the time and costs at least.
- Use self-hosting to keep all apps and data under your control for example at the own baremetals.
### Lowcoder vs Retool
- Lowcoder is open-source. You don't need to worry about vendor lock-in or being stuck with an outdated version of the software.
- In Lowcoder, developers can build truly responsive apps - not as cumbersome as the "Desktop / Mobile switch" in Retool
- Lowcoder is free and you can contribute!
- With Lowcoder you can design better Apps. More Layout & Design Components as also better support for powerful Data & Admin Dashboards.
- The EE Version of Lowcoder comes with a much better pricing model, so you have no "per-user costs".
### Lowcoder vs Appsmith, Tooljet
- Lowcoder has more components and richer configuration than Appsmith and Tooljet.
- In Lowcoder, you can choose auto-height or fixed-height mode for your components, while Appsmith supports fixed-height mode only.
- In Lowcoder, you can reuse common structures when building apps with modules and query library features.
### Lowcoder vs Mendix, Outsystems, Pega
- Lowcoder is modern. The codebase is fresh and uses modern standards.
- Lowcoder Apps do not need a compile and deployment. Just publish and use. Within seconds!
- Lowcoder Apps can get embedded natively in websites and apps, even in mobile apps.
### Lowcoder vs internal Tool platforms
- Lowcoder supports internal tools like admin panels perfectly, but also customer-facing apps can get developed and published.
- The Lowcoder UI builder is straightforward and better to use than Bubble.
- App release cycles and updates can be done nearly daily without service downtimes for customers and users.
## 👐 Support and Community
If you have any questions, please feel free to contact us or share them with our community. Our team is here ready to help.
And we mean it... Day by day!
📮 Best way is to chat with us on [Discord](https://discord.gg/qMG9uTmAx2)
📑 Search for solutions in our [Documentation](https://docs.lowcoder.cloud/lowcoder-documentation/)
🔎 Submit an issue here on [GitHub](https://github.com/lowcoder-org/lowcoder/issues)
## 💻 Deployment Options
[![Deploy to AWS using Stitch](https://img.shields.io/badge/deploy_with-Stitch-%23E369F7?logo=amazonaws&color=%23E369F7)](https://deploy.stitch.tech/lowcoder/lowcoder)
[![Deploy to Elestio](https://img.shields.io/badge/Deploy_to-Elestio-%23E369F7?color=orange)](https://elest.io/open-source/lowcoder)
You can access Lowcoder from [cloud-hosted version](https://app.lowcoder.cloud/) at any time, or use the following resources for self-host Lowcoder on different platforms:
- [Docker](https://docs.lowcoder.cloud/lowcoder-documentation/setup-and-run/self-hosting)
## 💪 Contributing
- Language support: If you have experience with a language that isn't currently supported by our product, send us a pull request.
- Create and share components or demos: If you've created something that might be useful to others, add the link here.
- [Contributing guide](https://docs.lowcoder.cloud/lowcoder-documentation/lowcoder-extension/opensource-contribution)
Special tanks goes to [@sjhoeksma](https://github.com/sjhoeksma), [@mousheng](https://github.com/mousheng), [@mat02](https://github.com/mat02), [@jomedya](https://github.com/jomedya) and many other contributors!
## 🥇 Sponsors
Accelerate the growth of Lowcoder and unleash its potential with your Sponsorship together, we're shaping the future of Lowcode for everyone!
[Be a Sponsor](https://github.com/sponsors/lowcoder-org)
Like ... [@Darkjamin](https://github.com/Darkjamin), [@spacegoats-io](https://github.com/spacegoats-io), [@Jomedya](https://github.com/Jomedya), [@CHSchuepfer](https://github.com/CHSchuepfer), Thank you very much!!

215
lowcoder/app.json Normal file
View File

@@ -0,0 +1,215 @@
{
"name": "lowcoder",
"description": "A Visual App builder with 120+ built-in components. Create software applications (internal and customer-facing!) and Meeting/Collaboration tools for your Company and your Customers with minimal coding experience.",
"repository": "https://github.com/lowcoder-org/lowcoder",
"logo": "https://raw.githubusercontent.com/lowcoder-org/lowcoder-media-assets/refs/heads/main/images/Lowcoder%20Logo%20512.png",
"keywords": [
"LowCode",
"Low code",
"develop tool",
"Fast Application Development",
"Rapid development",
"Collaboration tool",
"Video conferencing",
"AI User Interface"
],
"stack": "container",
"formation": {
"web": {
"quantity": 1,
"size": "standard-2x"
}
},
"env": {
"LOWCODER_DB_ENCRYPTION_PASSWORD": {
"description": "The encryption password used to encrypt all sensitive credentials in the database. You can use any random string (eg abcd).",
"required": true
},
"LOWCODER_DB_ENCRYPTION_SALT": {
"description": "The encryption salt used to encrypt all sensitive credentials in the database. You can use any random string (eg abcd).",
"required": true
},
"LOWCODER_CORS_DOMAINS": {
"description": "The domains supported for CORS requests. All domains are allowed by default. If there are multiple domains, please separate them with commas.",
"required": false
},
"LOWCODER_MONGODB_URL": {
"description": "Your Mongo Database URL.",
"required": false
},
"LOWCODER_REDIS_URL": {
"description": "Your Redis Database URL.",
"required": false
},
"LOWCODER_MAX_REQUEST_SIZE": {
"description": "Lowcoder max request size",
"value": "20m",
"required": false
},
"LOWCODER_MAX_QUERY_TIMEOUT": {
"description": "Lowcoder max query timeout (in seconds)",
"value": "120",
"required": false
},
"LOWCODER_DEFAULT_QUERY_TIMEOUT": {
"description": "Lowcoder default query timeout (in seconds)",
"value": "10",
"required": false
},
"LOWCODER_API_RATE_LIMIT": {
"description": "Number of max Request per Second",
"value": "100",
"required": false
},
"LOWCODER_API_SERVICE_URL": {
"description": "Lowcoder API service URL (main backend) - for multi-docker image installations.",
"value": "http://localhost:8080",
"required": false
},
"LOWCODER_NODE_SERVICE_URL": {
"description": "Lowcoder Node Service URL (data execution server) - for multi-docker image installations",
"value": "http://localhost:6060",
"required": false
},
"LOWCODER_MAX_ORGS_PER_USER": {
"description": "Default maximum organizations per user",
"value": "100",
"required": false
},
"LOWCODER_MAX_MEMBERS_PER_ORG": {
"description": "Default maximum members per organization",
"value": "1000",
"required": false
},
"LOWCODER_MAX_GROUPS_PER_ORG": {
"description": "Default maximum groups per organization",
"value": "100",
"required": false
},
"LOWCODER_MAX_APPS_PER_ORG": {
"description": "Default maximum applications per organization",
"value": "1000",
"required": false
},
"LOWCODER_MAX_DEVELOPERS": {
"description": "Default maximum developers",
"value": "100",
"required": false
},
"LOWCODER_WORKSPACE_MODE": {
"description": "SAAS (MULTIWORKSPACE) to activate, SINGLEWORKSPACE (ENTERPRISE) to switch off - Workspaces",
"value": "SAAS",
"required": true
},
"LOWCODER_EMAIL_SIGNUP_ENABLED": {
"description": "Control if users create their own Workspace automatic when Sign Up",
"value": "true",
"required": false
},
"LOWCODER_CREATE_WORKSPACE_ON_SIGNUP": {
"description": "IF LOWCODER_WORKSPACE_MODE = SAAS, controls if a own workspace is created for the user after sign up",
"value": "true",
"required": false
},
"LOWCODER_MARKETPLACE_PRIVATE_MODE": {
"description": "Control if not to show Apps on the local Marketplace to anonymous users",
"value": "true",
"required": false
},
"LOWCODER_SUPERUSER_USERNAME": {
"description": "Username of the Super-User of an Lowcoder Installation",
"value": "admin@localhost",
"required": true
},
"LOWCODER_SUPERUSER_PASSWORD": {
"description": "Password of the Super-User, if not present or empty, it will be generated",
"value": "`generated and printed into log file",
"required": true
},
"LOWCODER_API_KEY_SECRET": {
"description": "String to encrypt/sign API Keys that users may create",
"required": true
},
"LOWCODER_ADMIN_SMTP_HOST": {
"description": "SMTP Hostname of your Mail Relay Server",
"required": false
},
"LOWCODER_ADMIN_SMTP_PORT": {
"description": "Port number for the SMTP service",
"value": "587",
"required": false
},
"LOWCODER_ADMIN_SMTP_USERNAME": {
"description": "Username for SMTP authentication",
"required": false
},
"LOWCODER_ADMIN_SMTP_PASSWORD": {
"description": "Password for SMTP authentication",
"required": false
},
"LOWCODER_ADMIN_SMTP_AUTH": {
"description": "Enable SMTP authentication",
"value": "true",
"required": false
},
"LOWCODER_ADMIN_SMTP_SSL_ENABLED": {
"description": "Enable SSL encryption",
"value": "false",
"required": false
},
"LOWCODER_ADMIN_SMTP_STARTTLS_ENABLED": {
"description": "Enable STARTTLS encryption",
"value": "true",
"required": false
},
"LOWCODER_ADMIN_SMTP_STARTTLS_REQUIRED": {
"description": "Require STARTTLS encryption",
"value": "true",
"required": false
},
"LOWCODER_LOST_PASSWORD_EMAIL_SENDER": {
"description": "\"from\" Email address of the password Reset Email Sender",
"value": "service@lowcoder.cloud",
"required": false
},
"LOWCODER_REDIS_ENABLED": {
"description": "If true redis server is started in the single docker image container",
"required": true
},
"LOWCODER_MONGODB_ENABLED": {
"description": "If true mongo database is started in the single docker image container",
"required": true
},
"LOWCODER_MONGODB_EXPOSED": {
"description": "If true mongo database accept connections from outside the docker in the single docker image container",
"required": false
},
"LOWCODER_API_SERVICE_ENABLED": {
"description": "If true lowcoder api-service is started in the container",
"required": false
},
"LOWCODER_NODE_SERVICE_ENABLED": {
"description": "If true lowcoder node-service is started in the container",
"required": false
},
"LOWCODER_FRONTEND_ENABLED": {
"description": "If true lowcoder web frontend is started in the container",
"required": false
},
"LOWCODER_PUID": {
"description": "ID of user running services. It will own all created logs and data.",
"value": "9001",
"required": false
},
"LOWCODER_PGID": {
"description": "ID of group of the user running services.",
"value": "9001",
"required": false
},
"LOWCODER_PUBLIC_URL": {
"description": "The URL of the public User Interface",
"value": "localhost:3000",
"required": false
}
}
}

55
lowcoder/client/.gitignore vendored Normal file
View File

@@ -0,0 +1,55 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/packages/*/node_modules
/packages/*/dist
/packages/*/tsdoc-metadata.json
/.pnp
.pnp.js
# testing
/**/coverage
# production
/build
/packages/lowcoder/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/out
/public/fonts/*
/src/assets/icons/fonts/*
.idea
.storybook-out/
cypress/videos
cypress/screenshots
/cypress.env.json
storybook-static/*
build-storybook.log
TODO
.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
/ossutil_output
package-lock.json
op.mjs

View File

@@ -0,0 +1,69 @@
diff --git a/lib/rules/only-ascii.js b/lib/rules/only-ascii.js
index 7d76f9d37a99be35bab91d7a437073b112e2e985..b6ccac270f309232471d8e26ad8ec1bb782079f7 100644
--- a/lib/rules/only-ascii.js
+++ b/lib/rules/only-ascii.js
@@ -39,7 +39,7 @@ function create (context) {
// Get tokens which include non-ascii characters
var sourceCode = context.getSourceCode();
- var tokens = sourceCode.getTokens(node);
+ var tokens = sourceCode.tokensAndComments;
tokens.forEach(function (token) {
var value = token.value;
diff --git a/package.json b/package.json
index b6a4e0e402ed2c934cca2124e8ab9d2b43a198a2..d781a469e7a7d502a20e0dab27fc9c8adad4ffd8 100644
--- a/package.json
+++ b/package.json
@@ -16,11 +16,14 @@
"requireindex": "~1.1.0"
},
"devDependencies": {
- "eslint": "~3.9.1",
+ "eslint": "^8.0.0",
"mocha": "^3.1.2"
},
"engines": {
"node": ">=0.10.0"
},
- "license": "ISC"
+ "license": "ISC",
+ "peerDependencies": {
+ "eslint": "8"
+ }
}
diff --git a/tests/lib/rules/only-ascii.js b/tests/lib/rules/only-ascii.js
index 21cca9c932ac41804bb36ccd35787cdf121b4f16..a84c5a5dbf7ddb89637b88059432484b215ae098 100644
--- a/tests/lib/rules/only-ascii.js
+++ b/tests/lib/rules/only-ascii.js
@@ -8,15 +8,19 @@ var ruleTester = new RuleTester();
ruleTester.run('no-japanese', rule, {
valid : [
- { code : 'console.log("hello")', options : [] },
+ { code : 'console.log("hello") // ok', options : [] },
{ code : 'console.log("hello☆")', options : [{ allowedChars : '☆' }] },
{ code : 'console.log("☆")', filename : 'foo', options : [{ excludePaths : ['foo'] }] },
],
- invalid : [{
+ invalid : [
+ {
+ code : 'console.info("hello"); // console.log("ハロー")',
+ errors : [{ message : 'Non-ascii character "ハロー" found' }],
+ },{
code : 'console.log("ハロー")',
errors : [{ message : 'Non-ascii character "ハロー" found' }],
- }, {
+ },{
code : 'console.log("ハロー☆")',
options : [{ allowedChars : '☆' }],
errors : [{ message : 'Non-ascii character "ハロー" found' }],
@@ -25,5 +29,6 @@ ruleTester.run('no-japanese', rule, {
filename : 'foo',
options : [{ excludePaths : ['bar'] }],
errors : [{ message : 'Non-ascii character "☆" found' }],
- }],
+ }
+ ],
});

View File

@@ -0,0 +1,11 @@
diff --git a/dist/es/WindowScroller/utils/onScroll.js b/dist/es/WindowScroller/utils/onScroll.js
index d00f0f18c6596e4e57f4f762f91fed4282610c91..c8496e8eabafdf9cf6071986ec446839d7b65556 100644
--- a/dist/es/WindowScroller/utils/onScroll.js
+++ b/dist/es/WindowScroller/utils/onScroll.js
@@ -71,4 +71,3 @@ export function unregisterScrollListener(component, element) {
}
}
}
\ No newline at end of file
-import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";
\ No newline at end of file

File diff suppressed because one or more lines are too long

874
lowcoder/client/.yarn/releases/yarn-3.6.4.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,9 @@
nodeLinker: node-modules
npmRegistryServer: "https://registry.npmjs.org"
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-3.6.4.cjs

188
lowcoder/client/README.md Normal file
View File

@@ -0,0 +1,188 @@
# Lowcoder frontend
## How to contribute
### Start a local backend server
#### Use prebuilt docker image
Simply run the below command to start a backend server.
```bash
docker run -d --name lowcoder -p 3000:3000 -v "$PWD/stacks:/lowcoder-stacks" lowcoderorg/lowcoder-ce
```
For more information, view our [docs](https://docs.lowcoder.cloud/lowcoder-documentation/setup-and-run/self-hosting)
#### Build Docker image from source
1. Check out the source code and change to source dir.
2. Use the command below to build a Docker image :
```bash
docker build -f ./deploy/docker/Dockerfile -t lowcoder-dev .
```
3. Start
```bash
docker run -d --name lowcoder-dev -p 3000:3000 -v "$PWD/stacks:/lowcoder-stacks" lowcoder-dev
```
### Start develop
1. Check out source code.
2. Change to **/client** dir in the source dir.
```bash
cd client
```
3. Run yarn to install dependencies.
```bash
yarn install
```
4. Start dev server:
```bash
LOWCODER_API_SERVICE_URL=http://localhost:3000 yarn start
```
5. After dev server starts successfully, it will be automatically opened in the default browser.
### Before submitting a pull request
In addition, before submitting a pull request, please make sure the following is done:
1. If youve fixed a bug or added code that should be tested and add unit test suite.
2. Run test and ensure all test suites pass.
```bash
yarn test
```
3. If you add new dependency, use the yarn worspace tool to make sure yarn.lock is also updated.
```bash
yarn workspace lowcoder <package name>
```
### Developing and publishung UI components for Lowcoder
1. Initialization
Project initiation
```bash
yarn create Lowcoder-plugin <your plugin name>
```
Go to the project root
```bash
cd my-plugin
```
Start the development environment
```bash
yarn start
```
After executing yarn start, the browser is automatically opened and you enter the component development environment.
Please find more information in our [docs](https://docs.lowcoder.cloud/lowcoder-documentation/lowcoder-extension/develop-ui-components-for-apps)
2. Export components
To export all the components, use src/index.ts, for example:
```bash
import HelloWorldComp from "./HelloWorldComp";
export default {
hello_world: HelloWorldComp,
};
```
import HelloWorldComp from "./HelloWorldComp";
3. Publish plugins
When you finish developing and testing the plugin, you can publish it into the npm registry. Login in to the npm registry locally, and then execute the following command:
```bash
yarn build --publish
```
You can check a code demo here: [Code Demo on Github](https://github.com/lowcoder-org/lowcoder/tree/main/client/packages/lowcoder-plugin-demo)
# Deployment of the Lowcoder Frontend to Netlify (Local Build Flow)
## ⚙️ Prerequisites
* Node.js & Yarn installed
* Netlify CLI installed:
```bash
npm install -g netlify-cli
```
* Netlify CLI authenticated:
```bash
netlify login
```
* The project is linked to the correct Netlify site:
```bash
cd client
netlify link
```
---
## 🛠 Setup `netlify.toml` (only once)
Inside the `client/` folder, create or update `netlify.toml`:
```toml
[build]
base = "client"
command = "yarn workspace lowcoder build"
publish = "client/packages/lowcoder/build"
```
This ensures Netlify uses the correct build and publish paths when building locally.
---
## 🚀 Deployment Steps
1⃣ Navigate into the `client` folder:
```bash
cd client
```
2⃣ Run local build (with Netlify environment variables injected):
```bash
netlify build
```
3⃣ Deploy to production:
```bash
netlify deploy --prod --dir=packages/lowcoder/build
```
---
## 🔧 Notes
* This local build flow fully honors the environment variables configured in Netlify.
* No build happens on Netlify servers — only the deploy step runs on Netlify.
* This approach avoids Netlifys build memory limits.

1
lowcoder/client/VERSION Normal file
View File

@@ -0,0 +1 @@
2.7.5

View File

@@ -0,0 +1,45 @@
import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { buildVars } from "../../scripts/buildVars.js";
export function currentDirName(importMetaUrl) {
return dirname(fileURLToPath(importMetaUrl));
}
const globals = {};
buildVars.forEach(({ name, defaultValue }) => {
globals[name] = process.env[name] || defaultValue;
});
const currentDir = currentDirName(import.meta.url);
export default {
testEnvironment: "jsdom",
moduleNameMapper: {
"react-markdown": path.resolve(currentDir, "./mocks/react-markdown.js"),
"\\.md\\?url$": path.resolve(currentDir, "./mocks/markdown-url-module.js"),
"^@lowcoder-ee(.*)$": path.resolve(
currentDir, "../../packages/lowcoder/src/$1"
),
"lowcoder-sdk": path.resolve(currentDir, "../../packages/lowcoder/src/index.sdk"),
},
globals,
// roots: ["<rootDir>/src"],
modulePaths: [
"<rootDir>/src",
path.resolve(currentDir, "../../packages/lowcoder/src"),
path.resolve(currentDir, "../../packages/lowcoder-comps/src"),
path.resolve(currentDir, "../../packages/lowcoder-design/src"),
],
setupFiles: [path.resolve(currentDir, "./jest.setup.js")],
setupFilesAfterEnv: [path.resolve(currentDir, "./jest.setup-after-env.js"), 'jest-canvas-mock'],
transform: {
"^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": path.resolve(currentDir, "./transform/babelTransform.js"),
"^.+\\.css$": path.resolve(currentDir, "./transform/cssTransform.js"),
"^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": path.resolve(
currentDir,
"./transform/fileTransform.js"
),
},
transformIgnorePatterns: [],
resetMocks: true,
};

View File

@@ -0,0 +1,59 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom";
import { URL } from 'url';
// implementation of window.resizeTo for dispatching event
window.resizeTo = function resizeTo(width, height) {
Object.assign(this, {
innerWidth: width,
innerHeight: height,
outerWidth: width,
outerHeight: height,
}).dispatchEvent(new this.Event("resize"));
};
window.ResizeObserver = function () {
return {
observe: () => {},
unobserve: () => {},
disconnect: () => {},
};
};
Object.defineProperty(window, 'ImageData', { value: 'yourValue' });
Object.defineProperty(window, 'MediaStreamTrack', { value: 'yourValue' });
Object.defineProperty(window, 'URL', {
writable: true,
value: {
createObjectURL: jest.fn(),
}
});
Object.defineProperty(window, "navigator", {
writable: true,
value: {
mediaDevices: {
enumerateDevices: jest.fn(),
},
userAgent: '',
language: '',
browserLanguage: '',
},
});
class Worker {
constructor(stringUrl) {
this.url = stringUrl;
this.onmessage = () => {};
}
postMessage(msg) {
this.onmessage(msg);
}
}
window.Worker = Worker;
global.URL = URL;

View File

@@ -0,0 +1,3 @@
if (typeof window !== "undefined") {
require("whatwg-fetch");
}

View File

@@ -0,0 +1 @@
export default "";

View File

@@ -0,0 +1,5 @@
function ReactMarkdown({ children }) {
return <>{children}</>;
}
export default ReactMarkdown;

View File

@@ -0,0 +1,18 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom";
// import matchMediaPolyfill from "mq-polyfill";
// matchMediaPolyfill(window);
// // implementation of window.resizeTo for dispatching event
// window.resizeTo = function resizeTo(width, height) {
// Object.assign(this, {
// innerWidth: width,
// innerHeight: height,
// outerWidth: width,
// outerHeight: height,
// }).dispatchEvent(new this.Event("resize"));
// };

View File

@@ -0,0 +1,21 @@
import babelJest from "babel-jest";
export default babelJest.createTransformer({
presets: [
[
"babel-preset-react-app",
{
runtime: "automatic",
},
],
[
"babel-preset-vite",
{
"env": true,
"glob": false
}
]
],
babelrc: false,
configFile: false,
});

View File

@@ -0,0 +1,16 @@
// This is a custom Jest transformer turning style imports into empty objects.
// http://facebook.github.io/jest/docs/en/webpack.html
export default {
process() {
return {
code: "module.exports = {};",
};
},
getCacheKey() {
// The output is always the same.
return {
code: "cssTransform",
};
},
};

View File

@@ -0,0 +1,43 @@
import path from "node:path";
import camelcase from "camelcase";
// This is a custom Jest transformer turning file imports into filenames.
// http://facebook.github.io/jest/docs/en/webpack.html
export default {
process(src, filename) {
const assetFilename = JSON.stringify(path.basename(filename));
if (filename.match(/\.svg$/)) {
// Based on how SVGR generates a component name:
// https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
const pascalCaseFilename = camelcase(path.parse(filename).name, {
pascalCase: true,
});
const componentName = `Svg${pascalCaseFilename}`;
return {
code: `
const React = require('react');
module.exports = {
__esModule: true,
default: ${assetFilename},
ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
return {
$$typeof: Symbol.for('react.element'),
type: 'svg',
ref: ref,
key: null,
props: Object.assign({}, props, {
children: ${assetFilename}
})
};
}),
};`,
};
}
return {
code: `module.exports = ${assetFilename};`,
};
},
};

View File

@@ -0,0 +1,4 @@
export default {
projects: ["<rootDir>/packages/lowcoder", "<rootDir>/packages/lowcoder-core"],
};
// we use this for testing.

View File

@@ -0,0 +1,8 @@
[[redirects]]
from = "/*"
to = "/"
status = 200
[build]
base = "client"
command = "yarn workspace lowcoder build"
publish = "client/packages/lowcoder/build"

View File

@@ -0,0 +1,94 @@
{
"name": "lowcoder-frontend",
"version": "2.7.5",
"type": "module",
"private": true,
"workspaces": [
"packages/*"
],
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"scripts": {
"start": "yarn workspace lowcoder start",
"start-win": "LOWCODER_API_SERVICE_URL=http://localhost:3000 yarn start",
"start:ee": "REACT_APP_EDITION=enterprise yarn workspace lowcoder start",
"translate": "node --loader ts-node/esm ./scripts/translate.js",
"build": "yarn node ./scripts/build.js",
"build:ee": "REACT_APP_EDITION=enterprise yarn node ./scripts/build.js",
"test": "jest && yarn workspace lowcoder-comps test",
"prepare": "yarn workspace lowcoder prepare",
"build:core": "yarn workspace lowcoder-core build",
"test:core": "yarn workspace lowcoder-core test",
"lint": "eslint . --fix"
},
"devDependencies": {
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.18.6",
"@rollup/plugin-typescript": "^12.1.0",
"@testing-library/jest-dom": "^5.16.5",
"@types/file-saver": "^2.0.5",
"@types/jest": "^29.2.2",
"@types/mime": "^2.0.3",
"@types/qrcode.react": "^1.0.2",
"@types/react-grid-layout": "^1.3.0",
"@types/react-helmet": "^6.1.5",
"@types/react-resizable": "^3.0.5",
"@types/react-router-dom": "^5.3.2",
"@types/shelljs": "^0.8.11",
"@types/simplebar": "^5.3.3",
"@types/stylis": "^4.0.2",
"@types/tern": "0.23.4",
"@types/ua-parser-js": "^0.7.36",
"@welldone-software/why-did-you-render": "^6.2.3",
"add": "^2.0.6",
"babel-jest": "^29.3.0",
"babel-preset-react-app": "^10.0.1",
"babel-preset-vite": "^1.1.3",
"husky": "^8.0.1",
"jest": "^29.5.0",
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.5.0",
"lint-staged": "^13.0.1",
"lowcoder-cli": "workspace:^",
"mq-polyfill": "^1.1.8",
"prettier": "^3.1.0",
"rimraf": "^3.0.2",
"shelljs": "^0.8.5",
"svgo": "^3.0.0",
"ts-node": "^10.4.0",
"typescript": "^4.8.4",
"whatwg-fetch": "^3.6.2"
},
"lint-staged": {
"**/*.{mjs,ts,tsx,json,md,html}": "prettier --write --ignore-unknown",
"**/*.svg": "svgo"
},
"packageManager": "yarn@3.6.4",
"resolutions": {
"@types/react": "^18",
"moment": "2.29.2",
"canvas": "https://registry.yarnpkg.com/@favware/skip-dependency/-/skip-dependency-1.2.1.tgz",
"react-virtualized@^9.22.3": "patch:react-virtualized@npm%3A9.22.3#./.yarn/patches/react-virtualized-npm-9.22.3-0fff3cbf64.patch",
"eslint-plugin-only-ascii@^0.0.0": "patch:eslint-plugin-only-ascii@npm%3A0.0.0#./.yarn/patches/eslint-plugin-only-ascii-npm-0.0.0-29e3417685.patch"
},
"dependencies": {
"@lottiefiles/react-lottie-player": "^3.5.3",
"@remixicon/react": "^4.1.1",
"@supabase/supabase-js": "^2.45.4",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.1",
"@types/styled-components": "^5.1.34",
"antd-mobile": "^5.34.0",
"chalk": "4",
"flag-icons": "^7.2.1",
"number-precision": "^1.6.0",
"react-countup": "^6.5.3",
"react-github-btn": "^1.4.0",
"react-player": "^2.11.0",
"resize-observer-polyfill": "^1.5.1",
"rollup": "^4.22.5",
"simplebar": "^6.2.5",
"tui-image-editor": "^3.15.3"
}
}

View File

@@ -0,0 +1,4 @@
*.tgz
.DS_Store
node_modules
*.zip

View File

@@ -0,0 +1,37 @@
# lowcoder comp lib
## Start
Start dev server to develop your comp lib.
```bash
yarn start
# or
npm start
```
## Build
Build current comp lib into a .tgz file that you can upload it to the Lowcoder Comp Market.
Before build you should change the version in package.json file.
```bash
yarn build
# or
npm run build
```
## Publish
To publish your plugin on NPM, use following command.
```bash
yarn build_publish
# or
npm run build_publish
```

View File

@@ -0,0 +1,27 @@
# Lowcoder Demonstrator Plugin Sourcecode
## Overview
This repository contains the demonstrator code for a Component Plugin in Lowcoder. It serves as a practical example and a reference for developers looking to build their own Component Plugins within the Lowcoder framework.
This repository is not about to integrate the Plugin like other NPM repositories. However, for the Lowcoder Component Plugin Creator we wanted to publish the Demonstrator Plugin code sources.
### The Lowcoder App Editor
<p align="center">
<img src="https://raw.githubusercontent.com/lowcoder-org/lowcoder-media-assets/main/images/App%20Editor%20%7C%20Main%20Screeen%20clean.png" alt="Lowcoder App Editor">
</p>
### Purpose
The primary goal of this repository is to showcase best practices, demonstrate the structure, and provide a clear example of how to develop Component Plugins for Lowcoder. It is intended to be a learning resource rather than a production-ready solution.
## Getting Started
To explore this demonstrator plugin, install and test it in action you can use the "Lowcoder Component Plugin Builder". https://www.npmjs.com/package/create-lowcoder-plugin
### The Lowcoder Component Plugin Builder and Preivew
<p align="center">
<img src="https://raw.githubusercontent.com/lowcoder-org/lowcoder-media-assets/main/images/Component%20Plugin%20Builder%20%7C%20Preview.png" alt="Lowcoder Plugin Preview">
</p>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(8.000000, 10.000000)">
<rect fill="#D7D9E0" x="0" y="0" width="32" height="28" rx="3"/>
<path d="M8,4 C9.65686841,4 11,5.34313159 11,7 C11,8.65686841 9.65686841,10 8,10 C6.34313159,10 5,8.65686841 5,7 C5,5.34313159 6.34313155,4 8,4 Z" fill="#FFFFFF" fill-rule="nonzero"/>
<path d="M10.8216102,16.7159324 L13.4766616,20.0440299 C13.8210834,20.4757614 14.4502799,20.5465399 14.8820114,20.2021181 C14.9491721,20.1485394 15.0091482,20.0865292 15.0604578,20.0176194 L19.4593737,14.1097995 C19.7892094,13.6668245 20.4156962,13.575107 20.8586713,13.9049427 C20.9441855,13.9686159 21.018967,14.0455546 21.0801833,14.1328445 L26.8960339,22.4258246 C27.2131422,22.877998 27.1036503,23.501624 26.6514769,23.8187323 C26.4832527,23.9367077 26.2827708,24 26.0773016,24 L5.95488979,24 C5.40260504,24 4.95488979,23.5522847 4.95488979,23 C4.95488979,22.7899115 5.0210567,22.5851591 5.14400038,22.4148006 L9.22900176,16.7543638 C9.55219848,16.3065219 10.1772487,16.2054771 10.6250906,16.5286738 C10.6988039,16.5818709 10.7649195,16.6448707 10.8216102,16.7159324 Z" fill="#FFFFFF" fill-rule="nonzero"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1200 1200" style="enable-background:new 0 0 1200 1200;" xml:space="preserve">
<style type="text/css">
.st0{fill:#9B9B9B;}
</style>
<g>
<path d="M443.7,797.6L624,531.3c86.3-127.4,213.3-129.7,300.8-5.2l191.3,271.5L443.7,797.6z"/>
<path d="M652.9,447.4C551,331,415.2,343,320.8,482.7L107.4,797.6h292l192.3-283.8C609.5,487.3,630.2,464.9,652.9,447.4L652.9,447.4
z"/>
</g>
<circle class="st0" cx="334.8" cy="476.1" r="54"/>
<circle class="st0" cx="976.8" cy="600" r="68.1"/>
<circle class="st0" cx="238.9" cy="600" r="54"/>
<circle class="st0" cx="779.9" cy="434.2" r="54"/>
</svg>

After

Width:  |  Height:  |  Size: 855 B

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lowcoder Component Plugin Preview</title>
<style>
#root {
height: 100vh;
}
#root-loader {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<div id="root">
<div id="root-loader">Loading...</div>
</div>
<script src="index.tsx" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,19 @@
import { createRoot } from 'react-dom/client';
import { CompIDE } from "lowcoder-sdk";
import { name, version, lowcoder } from "./package.json";
import compMap from "./src/index";
import "lowcoder-sdk/dist/style.css";
function CompDevApp() {
return (
<CompIDE
compMap={compMap}
packageName={name}
packageVersion={version}
compMeta={lowcoder.comps}
/>
);
}
const container = document.querySelector("#root") as Element | DocumentFragment;
const root = createRoot(container);
root.render(<CompDevApp />);

View File

@@ -0,0 +1,43 @@
{
"name": "lowcoder-cli-template-typescript",
"version": "0.0.22",
"type": "module",
"scripts": {
"start": "NODE_OPTIONS=--max_old_space_size=6144 vite",
"build": "lowcoder-cli build",
"build_publish": "lowcoder-cli build --publish"
},
"lowcoder": {
"description": "A Demo Hillchart Component Plugin",
"comps": {
"hillcharts": {
"name": "Hillcharts Demo",
"icon": "./icons/hills.svg",
"description": "Hillchart Plugin Demo Component",
"layoutInfo": {
"w": 10,
"h": 40
}
}
}
},
"dependencies": {
"@observablehq/inspector": "^5.0.1",
"@observablehq/runtime": "^4.8.2",
"@observablehq/stdlib": "^5.8.8",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"lowcoder-cli": "^0.0.30",
"lowcoder-sdk": "^2.1.10",
"prop-types": "^15.7.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-resize-detector": "^7.0.0",
"typescript": "5.3.3",
"vite": "^4.5.5"
},
"keywords": [
"Lowcoder, Component, Template, Plugin, Demonstrator"
],
"license": "MIT"
}

View File

@@ -0,0 +1,197 @@
import {
UICompBuilder,
NameConfig,
NumberControl,
Section,
withDefault,
withExposingConfigs,
withMethodExposing,
eventHandlerControl,
styleControl,
toJSONObjectArray,
jsonControl,
AutoHeightControl,
EditorContext,
} from "lowcoder-sdk";
import { useResizeDetector } from "react-resize-detector";
import styles from "./styles.module.css";
import { i18nObjs, trans } from "./i18n/comps";
import { Chart } from './vendors'
import { useState } from "react";
export const CompStyles = [
{
name: "margin",
label: trans("style.margin"),
margin: "margin",
},
{
name: "padding",
label: trans("style.padding"),
padding: "padding",
},
{
name: "textSize",
label: trans("style.textSize"),
textSize: "textSize",
},
{
name: "backgroundColor",
label: trans("style.backgroundColor"),
backgroundColor: "backgroundColor",
},
{
name: "border",
label: trans("style.border"),
border: "border",
},
{
name : "radius",
label : trans("style.borderRadius"),
radius : "radius",
},
{
name : "borderWidth",
label : trans("style.borderWidth"),
borderWidth : "borderWidth",
}
] as const;
interface Point {
id: number,
color?: string,
description?: string,
x: number,
size?: number,
}
// const HillchartsCompBase = new UICompBuilder(childrenMap, (props: any) => {
let HillchartsCompBase = (function () {
const childrenMap = {
styles: styleControl(CompStyles),
autoHeight: withDefault(AutoHeightControl, "auto"),
data: jsonControl(toJSONObjectArray, i18nObjs.defaultData),
onEvent: eventHandlerControl([
{
label: "onChange",
value: "change",
description: "Triggers when Chart data changes",
},
] as const),
};
return new UICompBuilder(childrenMap, (props: {
onEvent: any;
styles: { backgroundColor: any; border: any; radius: any; borderWidth: any; margin: any; padding: any; textSize: any; };
data: any[] | null | undefined;
autoHeight: boolean;
}) => {
const handleDataChange = () => {
props.onEvent("change");
};
const [dimensions, setDimensions] = useState({ width: 480, height: 280 });
const { width, height, ref: conRef } = useResizeDetector({onResize: () =>{
const container = conRef.current;
if(!container || !width || !height) return;
if(props.autoHeight) {
setDimensions({
width,
height: dimensions.height,
})
return;
}
setDimensions({
width,
height,
})
}});
return (
<div ref={conRef} className={styles.wrapper} style={{
height: `100%`,
width: `100%`,
backgroundColor: `${props.styles.backgroundColor}`,
borderColor: `${props.styles.border}`,
borderRadius: `${props.styles.radius}`,
borderWidth: `${props.styles.borderWidth}`,
margin: `${props.styles.margin}`,
padding: `${props.styles.padding}`,
fontSize: `${props.styles.textSize}`,
}}>
<Chart
data={props.data}
height={dimensions.height}
width={dimensions.width}
onDataChange={handleDataChange}
/>
</div>
);
})
.setPropertyViewFn((children: any) => {
return (
<>
<Section name="Basic">
{children.data.propertyView({ label: "Data" })}
</Section>
<Section name="Interaction">
{children.onEvent.propertyView()}
</Section>
<Section name="Styles">
{children.autoHeight.getPropertyView()}
{children.styles.getPropertyView()}
</Section>
</>
);
})
.build();
})();
HillchartsCompBase = class extends HillchartsCompBase {
autoHeight(): boolean {
return this.children.autoHeight.getView();
}
};
HillchartsCompBase = withMethodExposing(HillchartsCompBase, [
{
method: {
name: "setPoint",
description: trans("methods.setPoint"),
params: [{
name: "data",
type: "JSON",
description: "JSON value"
}],
},
execute: (comp: any, values: any[]) => {
const point = values[0] as Point;
if(typeof point !== 'object') {
return Promise.reject(trans("methods.invalidInput"))
}
if(!point.id) {
return Promise.reject(trans("methods.requiredField", { field: 'ID' }));
}
if(!point.x) {
return Promise.reject(trans("methods.requiredField", { field: 'X position' }));
}
const data = comp.children.data.getView();
const newData = [
...data,
point,
];
comp.children.data.dispatchChangeValueAction(JSON.stringify(newData, null, 2));
}
},
]);
export default withExposingConfigs(HillchartsCompBase, [
new NameConfig("data", trans("component.data")),
]);

View File

@@ -0,0 +1,35 @@
# Using Lowcoder Component Plugin
## Prerequisites
Before you start, ensure you have a running Lowcoder installation. Alternatively, you can use it online at [https://app.lowcoder.cloud](https://app.lowcoder.cloud).
## Steps to Use the Plugin
1. **Open the App Editor**: Navigate to the App Editor within your Lowcoder application.
<p align="center">
<img src="https://raw.githubusercontent.com/lowcoder-org/lowcoder-media-assets/main/images/App%20Editor%20%7C%20Main%20Screeen%20clean.png" alt="Lowcoder App Editor">
</p>
1. **Access Components Panel**: In the App Editor, locate the right panel where components are listed.
2. **Switch to Extensions**: Find and switch on the "Extensions" toggle. This option allows you to add additional components to your project.
<p align="center">
<img src="https://raw.githubusercontent.com/lowcoder-org/lowcoder-media-assets/main/images/App%20Editor%20%7C%20Import%20Component%20Plugin%201.png" alt="Lowcoder App Editor">
</p>
3. **Load the Plugin**: Here you have the option to load a Lowcoder Component Plugin from NPM. For example, to load the "hill charts" plugin, type `lowcoder-comp-hillcharts` in the provided field.
<p align="center">
<img src="https://raw.githubusercontent.com/lowcoder-org/lowcoder-media-assets/main/images/App%20Editor%20%7C%20Import%20Component%20Plugin%202.png" alt="Lowcoder App Editor">
</p>
4. **Start Using the Plugin**: After loading the plugin, it will be available for use within your Lowcoder project. You can now integrate and customize the component as per your application's needs.
<p align="center">
<img src="https://raw.githubusercontent.com/lowcoder-org/lowcoder-media-assets/main/images/App%20Editor%20%7C%20Import%20Component%20Plugin%203.png" alt="Lowcoder App Editor">
</p>
<p align="center">
<img src="https://raw.githubusercontent.com/lowcoder-org/lowcoder-media-assets/main/images/App%20Editor%20%7C%20Import%20Component%20Plugin%204.png" alt="Lowcoder App Editor">
</p>

View File

@@ -0,0 +1,3 @@
/// <reference types="lowcoder-cli/client" />
declare module "lowcoder-sdk";

View File

@@ -0,0 +1,34 @@
import { getI18nObjects, getValueByLocale, Translator } from "lowcoder-sdk";
import * as localeData from "./locales";
import { I18nObjects } from "./locales/types";
export const { trans, language } = new Translator<typeof localeData.en>(
localeData,
REACT_APP_LANGUAGES
);
export const i18nObjs = getI18nObjects<I18nObjects>(localeData, REACT_APP_LANGUAGES);
export function getEchartsLocale() {
return getValueByLocale("EN", (locale) => {
switch (locale.language) {
case "en":
return "EN";
case "pt":
return "PT";
case "zh":
return "ZH";
}
});
}
export function getCalendarLocale() {
switch (language) {
case "zh":
return "zh-cn";
case "pt":
return "pt-br";
default:
return "en-gb";
}
}

View File

@@ -0,0 +1,35 @@
export const en = {
"style": {
"textColor": "Text Color",
"contrastText": "Contrast Text Color",
"accent": "Accent",
"border": "Border Color",
"borderRadius": "Border Radius",
"borderWidth": "Border Width",
"backgroundColor": "Background Color",
"headerBackground": "Header Background",
"footerBackground": "Footer Background",
"checkedBackground": "Checked Background",
"uncheckedBackground": "Unchecked Background",
"uncheckedBorder": "Unchecked Border",
"indicatorBackground": "Indicator Background",
"toolbarBackground": "Toolbar Background",
"margin": "Margin",
"padding": "Padding",
"marginLeft": "Margin Left",
"marginRight": "Margin Right",
"marginTop": "Margin Top",
"marginBottom": "Margin Bottom",
"minWidth": "Minimum Width",
"aspectRatio": "Aspect Ratio",
"textSize": "Text Size"
},
"component": {
"data": "Hillchart Data",
},
"methods": {
"setPoint": "Set Point",
"invalidInput": "Invalid Input",
"requiredField": "{field} is required",
}
};

View File

@@ -0,0 +1,40 @@
import { I18nObjects } from "./types";
export const enObj: I18nObjects = {
defaultData: [
{
id : 1,
color: 'gray',
description: 'Validation: Salesforce Integration',
x: 25,
size: 15
},
{
id : 2,
color: 'maroon',
description: 'Renewals',
x: 80,
size: 10 },
{
id : 3,
color: 'maroon',
description: 'Refactor: Fancy Pants',
x: 35,
size: 10
},
{
id : 4,
color: 'cyan',
description: 'Refactor: Lighthouse Orbs',
x: 45,
size: 10
},
{
id : 5,
color: 'yellow',
description: 'Migration & Legacy Data Updates',
x: 50,
size: 20
}
],
};

View File

@@ -0,0 +1,7 @@
// file examples: en, enGB, zh, zhHK
// fallback example: current locale is zh-HK, fallback order is zhHK => zh => en
export * from "./en";
export * from "./zh";
export * from "./enObj";
export * from "./zhObj";

View File

@@ -0,0 +1,37 @@
import {en} from "./en"
export const pt: typeof en = {
...en,
"style": {
"textColor": "Cor do Texto",
"contrastText": "Cor de Contraste do Texto",
"accent": "Acento",
"border": "Cor da Borda",
"borderRadius": "Raio da Borda",
"borderWidth": "Grossura da Borda",
"backgroundColor": "Cor de Fundo",
"headerBackground": "Cor do Header",
"footerBackground": "Cor do Footer",
"checkedBackground": "Cor com Seleção",
"uncheckedBackground": "Cor sem Seleção",
"uncheckedBorder": "Borda sem Seleção",
"indicatorBackground": "Cor de Indicação",
"toolbarBackground": "Cor de Fundo da Barra de Informações",
"margin": "Margem",
"padding": "Preenchimento",
"marginLeft": "Margem Esquerda",
"marginRight": "Margem Direita",
"marginTop": "Margem Superior",
"marginBottom": "Margem Inferior",
"minWidth": "Largura Mínima",
"aspectRatio": "Proporção de Tela",
"textSize": "Tamanho do Texto",
},
"component": {
"data": "Dados Hillchart",
},
"methods": {
"setPoint": "Definir Ponto",
"invalidInput": "Entrada Inválida",
"requiredField": "{field} é obrigatório",
}
};

View File

@@ -0,0 +1,40 @@
import { I18nObjects } from "./types";
export const enObj: I18nObjects = {
defaultData: [
{
id : 1,
color: 'gray',
description: 'Validação: Integração do Salesforce',
x: 25,
size: 15
},
{
id : 2,
color: 'maroon',
description: 'Renovações',
x: 80,
size: 10 },
{
id : 3,
color: 'maroon',
description: 'Rafatoramento: Fancy Pants',
x: 35,
size: 10
},
{
id : 4,
color: 'cyan',
description: 'Refatoramento: Lighthouse Orbs',
x: 45,
size: 10
},
{
id : 5,
color: 'yellow',
description: 'Migração e Atualização de Dados',
x: 50,
size: 20
}
],
};

View File

@@ -0,0 +1,13 @@
import { JSONObject } from "lowcoder-sdk";
export type JSONValue = string | number | boolean | JSONObject | JSONArray | null;
export interface JSONObject {
[x: string]: JSONValue | undefined;
}
export type I18nObjects = {
defaultData: JSONObject[];
};
export type JSONArray = Array<JSONValue>;

View File

@@ -0,0 +1,3 @@
export const zh = {
};

View File

@@ -0,0 +1,4 @@
import { I18nObjects } from "./types";
export const zhObj: I18nObjects | undefined = undefined;

View File

@@ -0,0 +1,5 @@
import HillchartsComp from "./HillchartsComp";
export default {
hillcharts: HillchartsComp
};

View File

@@ -0,0 +1,9 @@
.wrapper {
padding: 5px;
display: flex;
justify-content: center;
align-items: center;
/* height: 100%; */
border: 1px solid #dddddd;
background-color: white;
}

View File

@@ -0,0 +1,218 @@
import React from 'react';
import PropTypes from 'prop-types'
import { Runtime } from '@observablehq/runtime';
import { Inspector } from "@observablehq/inspector";
import { Library } from "@observablehq/stdlib";
const library = new Library();
function Chart(props) {
const [chartRef, setChartRef] = React.useState();
function define(runtime, observer) {
const main = runtime.module();
// Define your variables but don't attach observers to all of them
main.variable().define('data', () => props.data);
main.variable().define('width', () => props.width);
main.variable().define('height', () => props.height);
main.variable().define('onDataChange', () => props.onDataChange);
main.variable().define('translateXtoY', function() {
return x => 50 * Math.sin((Math.PI / 50) * x - (1 / 2) * Math.PI) + 50;
});
main.variable().define('d3', [], function() {
return Library.require('https://d3js.org/d3.v5.min.js');
});
// Define the HillChart class
main.variable().define('HillChart', ['d3', 'translateXtoY'], function(d3, translateXtoY) {
return class HillChart {
constructor(chart_height, chart_width, items) {
this.chart_height = chart_height;
this.chart_width = chart_width;
this.items = items;
this.svg = d3.select(library.DOM.svg(this.chart_width, this.chart_height)).attr('viewBox', `-20 -20 ${this.chart_width + 80} ${this.chart_height + 20}`);
}
render() {
const xScale = d3
.scaleLinear()
.domain([0, 100])
.range([0, this.chart_width - 10]);
const yScale = d3
.scaleLinear()
.domain([0, 100])
.range([this.chart_height - 40, 10]);
// HILL LINE
const hillData = d3.range(0, 100, 0.1).map(i => ({
x: i,
y: translateXtoY(i)
}));
const hillLine = d3
.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y));
// MIDDLE LINE
this.svg
.append('line')
.attr('class', 'middle')
.attr('x1', xScale(50))
.attr('y1', yScale(0))
.attr('x2', xScale(50))
.attr('y2', yScale(100))
.attr('stroke', '#dddddd')
.attr('stroke-width', 1)
.attr('stroke-dasharray', 10);
// BOTTOM AXIS
this.svg
.append('line')
.attr('class', 'middle')
.attr('x1', xScale(0))
.attr('y1', yScale(-5))
.attr('x2', xScale(100))
.attr('y2', yScale(-5))
.attr('stroke', '#dddddd')
.attr('stroke-width', 1);
this.svg
.append('path')
.attr('class', 'line')
.datum(hillData)
.attr('fill', 'none')
.attr('stroke', '#cccccc')
.attr('stroke-width', 2)
.attr('d', hillLine);
// PLOT POINTS
const dragFn = d3.drag()
.on('drag', function(d) {
let xPoint = d.x + xScale.invert(d3.event.dx);
if (xPoint < 0) {
xPoint = 0;
} else if (xPoint > 100) {
xPoint = 100;
}
d.x = xPoint;
d3.select(this).attr(
'transform',
`translate(${xScale(xPoint)}, ${yScale(translateXtoY(xPoint))})`
);
}).on('end', function(event, d) {
props.onDataChange();
});
const group = this.svg
.selectAll('.group')
.data(this.items)
.enter()
.append('g')
.attr('class', 'group')
.attr('transform', d => {
return `translate(${xScale(d.x)}, ${yScale(
translateXtoY(d.x)
)})`;
})
.call(dragFn);
group
.append('circle')
.attr('fill', d => d.color)
.attr('stroke', '#ffffff')
.attr('stroke-width', 2)
.attr('style', 'cursor: move')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', d => d.size);
group
.append('line')
.attr('stroke', d => d.color)
.attr('stroke-width', 1)
.attr('x1', 10)
.attr('y1', 0)
.attr('x2', 20)
.attr('y2', 0);
group
.append('text')
.attr('style', 'font-family: Tahoma; font-size: 14px;')
.text(d => d.description)
.attr('x', 25)
.attr('y', 5);
// AXIS LABELS
this.svg
.append('text')
.attr('class', 'text')
.attr('style', 'font-family: Tahoma; font-size: 14px;')
.attr('fill', '#999999')
.text('FIGURING THINGS OUT')
.attr('x', xScale(0))
.attr('y', this.chart_height - 5);
this.svg
.append('text')
.attr('class', 'text')
.attr('style', 'font-family: Tahoma; font-size: 14px;')
.attr('fill', '#999999')
.text('MAKING IT HAPPEN')
.attr('x', xScale(70))
.attr('y', this.chart_height - 5);
return this.svg.node();
}
};
});
// Attach an observer only to the chart rendering part
main.variable(observer('chart')).define(['HillChart', 'height', 'width', 'data'], function(HillChart, height, width, data) {
return new HillChart(height, width, data).render();
});
return main;
}
const useChartRef = React.useCallback(ref => {
setChartRef(ref);
}, []);
const [runtime] = React.useState(() => new Runtime());
React.useEffect(() => {
if (chartRef) {
// Clear the chart
chartRef.innerHTML = '';
// Render an updated chart
runtime.module(define, Inspector.into(chartRef), 'chart');
}
}, [chartRef, props.data, props.width, props.height]);
return (
<div
ref={useChartRef}
style={{ height: "100%" }}
/>
);
}
Chart.propTypes = {
width: PropTypes.number,
height: PropTypes.number,
data: PropTypes.array,
onDataChange: PropTypes.func
}
export default Chart;

View File

@@ -0,0 +1 @@
export { default as Chart } from './Chart'

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "es5",
"experimentalDecorators": true,
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "src"
},
"include": ["src", "index.tsx"]
}

View File

@@ -0,0 +1,8 @@
import config from "lowcoder-cli/config/vite.config";
export default {
...config,
server: {
open: true,
port: 9000,
},
};

View File

@@ -0,0 +1,2 @@
.out
node_modules

View File

@@ -0,0 +1,3 @@
# lowcoder-cli
CLI tool used to start build and publish lowcoder component library.

View File

@@ -0,0 +1,133 @@
import { execSync } from "child_process";
import fsExtra from "fs-extra";
import { build } from "vite";
import { writeFileSync, existsSync, readFileSync, readdirSync } from "fs";
import { resolve } from "path";
import { pathToFileURL } from "url";
import paths from "../config/paths.js";
import "../util/log.js";
import chalk from "chalk";
const { copySync } = fsExtra;
const packageJSON = JSON.parse(readFileSync(paths.appPackageJson).toString());
function validPackageJSON() {
if (!packageJSON.name) {
return "- package name is required";
}
if (!packageJSON.version) {
return "- package version is required";
}
if (!packageJSON.lowcoder) {
return "- lowcoder field is required in package.json";
}
const lowcoder = packageJSON.lowcoder;
if (!lowcoder.comps || Object.keys(lowcoder.comps).length === 0) {
return "- not found any comps to build";
}
const compErrors = [];
Object.keys(lowcoder.comps).forEach((name) => {
const compManifest = packageJSON.lowcoder.comps[name];
if (!compManifest.icon) {
// compErrors.push(`- comp ${name} must specify an icon`);
return;
}
if (
!compManifest.icon.startsWith("data:") &&
!existsSync(paths.resolveApp(compManifest.icon))
) {
compErrors.push(`- comp ${name}'s icon file ${chalk.cyan(compManifest.icon)} not found`);
return;
}
});
if (compErrors.length > 0) {
return compErrors.join("\n");
}
}
function findReadmeFileName(directory) {
const files = readdirSync(directory);
const readmeFile = files.find(file => file.toLowerCase() === 'readme.md');
return readmeFile ? `${directory}/${readmeFile}` : null;
}
/**
* 1. webpack production build
* 2. generate package.json
* 3. copy locales
* 3. pack tar ball
*/
export default async function buildAction(options) {
const beginTime = performance.now();
process.env.NODE_ENV = "production";
const { outDir } = options;
const err = validPackageJSON();
if (err) {
console.red("Invalid package.json:\n");
console.red(err);
console.log("");
return;
}
const compNames = Object.keys(packageJSON.lowcoder.comps);
console.cyan(`Name : ${packageJSON.name}`);
console.cyan(`Version : ${packageJSON.version}`);
console.cyan(`Comps : ${compNames.length}\n`);
compNames.forEach((i) => {
console.log(` ${i}`);
});
console.log("");
console.cyan("Building...");
const viteConfigURL = pathToFileURL(paths.appViteConfigJs);
const viteConfig = await import(viteConfigURL).default;
console.log(paths.appViteConfigJs);
await build(viteConfig);
// write package.json
packageJSON.lowcoder.entry = "index.js";
writeFileSync(paths.appOutPackageJson, JSON.stringify(packageJSON, null, 2));
// copy locales
if (existsSync(paths.appLocales)) {
copySync(paths.appLocales, resolve(paths.appOutPath, "locales"));
}
// copy icon files
compNames.forEach((name) => {
const compManifest = packageJSON.lowcoder.comps[name];
if (compManifest.icon) {
copySync(paths.resolveApp(compManifest.icon), resolve(paths.appOutPath, compManifest.icon));
}
});
// copy readme file
const readmePath = findReadmeFileName(paths.appPath + '/src');
if (readmePath) {
const destinationPath = resolve(paths.appOutPath, 'readme.md');
copySync(readmePath, destinationPath);
console.log(`Copied README file to: ${destinationPath}`);
} else {
console.warn('README.md file not found.');
}
if (options.publish) {
// publish
execSync("npm publish", {
stdio: "inherit",
cwd: paths.appOutPath,
});
} else {
// pack
const tarOutPath = paths.resolveApp(outDir);
execSync(`npm pack --pack-destination ${tarOutPath}`, {
stdio: "ignore",
cwd: paths.appOutPath,
});
console.green(`Package generated in: ${tarOutPath}`);
}
console.green(`Done in ${Math.round(performance.now() - beginTime)}ms!`);
}

View File

@@ -0,0 +1,109 @@
import path from "path";
import fs from "fs-extra";
import { spawn } from "cross-spawn";
import paths from "../config/paths.js";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const isUsingYarn = (process.env.npm_config_user_agent || "").indexOf("yarn") === 0;
function install(dependencies, registry) {
return new Promise((resolve, reject) => {
let cmd = "npm";
let args = ["install", "--no-audit", "--save", "--save-exact", "--loglevel", "error"];
if (isUsingYarn) {
cmd = "yarn";
args = ["add"];
}
if (registry) {
args.push("--registry", registry);
}
args.push(...dependencies);
const child = spawn(cmd, args, { stdio: "inherit" });
child.on("close", (code) => {
if (code !== 0) {
reject({
command: `${cmd} ${args.join(" ")}`,
});
return;
}
resolve();
});
});
}
async function uninstall(dependencies) {
return new Promise((resolve, reject) => {
let cmd = "npm";
let args = ["uninstall"];
if (isUsingYarn) {
cmd = "yarn";
args = ["remove"];
}
args.push(...dependencies);
const child = spawn(cmd, args, { stdio: "inherit" });
child.on("close", (code) => {
if (code !== 0) {
reject({
command: `${cmd} ${args.join(" ")}`,
});
return;
}
resolve();
});
});
}
/**
* init dir with specified template name
* 1. install template package
* 2. update package.json
* 3. copy template files
* 4. install other dependencies
* 5. uninstall template package
*/
export default async function initAction(options) {
const { template, registry } = options;
const templatePackageName = `lowcoder-cli-template-${template}`;
await install([templatePackageName], registry);
console.log("template package installed");
const templatePackageJsonFile = require.resolve(`${templatePackageName}/package.json`);
const templateDir = path.dirname(templatePackageJsonFile);
const templatePackageJson = fs.readJsonSync(templatePackageJsonFile);
const appPackageJson = fs.readJsonSync(paths.appPackageJson);
appPackageJson.lowcoder = templatePackageJson.lowcoder || {};
appPackageJson.scripts = {
start: "vite",
build: "lowcoder-cli build",
build_publish: "lowcoder-cli build --publish",
};
fs.writeFileSync(paths.appPackageJson, JSON.stringify(appPackageJson, null, 2));
console.log("package.json updated");
const notCopiedFiles = ["package.json", "README.md", "README-template.md", "node_modules"];
fs.copySync(templateDir, "./", {
filter: (src) => notCopiedFiles.every((i) => !src.startsWith(path.join(templateDir, i))),
});
fs.copyFile(path.join(templateDir, "README-template.md"), "./README.md");
console.log("template files copied");
const dependencies = [];
if (template === "typescript") {
dependencies.push("typescript");
}
if (dependencies.length > 0) {
await install(dependencies, registry);
console.log("dependencies installed");
}
await uninstall([templatePackageName]);
console.log("template package uninstalled");
console.log();
console.log("Done! Now, you can run below command to start:");
console.log(` ${isUsingYarn ? "yarn" : "npm"} start`);
console.log();
}

View File

@@ -0,0 +1,41 @@
/// <reference path="./global.d.ts" />
/// <reference types="vite/client" />
declare module "*.svg" {
import * as React from "react";
export const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement> & { title?: string }
>;
// const src: string;
// export default src;
}
declare module "*.md" {
const value: string;
export default value;
}
declare module "eslint4b-prebuilt-2";
declare module "mq-polyfill";
declare module "@rjsf/antd";
declare module "really-relaxed-json";
declare module "tui-image-editor";
declare var numbro: any;
declare var uuid: any;
declare var PUBLIC_URL: string;
declare var REACT_APP_EDITION: string;
declare var REACT_APP_LANGUAGES: string;
declare var REACT_APP_COMMIT_ID: string;
declare var REACT_APP_API_SERVICE_URL: string;
declare var REACT_APP_NODE_SERVICE_URL: string;
declare var REACT_APP_ENV: string;
declare var REACT_APP_BUILD_ID: string;
declare var REACT_APP_LOG_LEVEL: string;
declare var REACT_APP_IMPORT_MAP: string;
declare var REACT_APP_SERVER_IPS: string;
declare var REACT_APP_BUNDLE_TYPE: "sdk" | "app";
declare var REACT_APP_DISABLE_JS_SANDBOX: string;
declare var REACT_APP_BUNDLE_BUILTIN_PLUGIN: string;

View File

@@ -0,0 +1,119 @@
import fs from "fs";
import path from "path";
import paths from "./paths.js";
import resolve from "resolve";
function getAdditionalEntries() {}
/**
* Get additional module paths based on the baseUrl of a compilerOptions object.
*
* @param {Object} options
*/
function getAdditionalModulePaths(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return "";
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
// We don't need to do anything if `baseUrl` is set to `node_modules`. This is
// the default behavior.
if (path.relative(paths.appNodeModules, baseUrlResolved) === "") {
return null;
}
// Allow the user set the `baseUrl` to `appSrc`.
if (path.relative(paths.appSrc, baseUrlResolved) === "") {
return [paths.appSrc];
}
// If the path is equal to the root directory we ignore it here.
// We don't want to allow importing from the root directly as source files are
// not transpiled outside of `src`. We do allow importing them with the
// absolute path (e.g. `src/Components/Button.js`) but we set that up with
// an alias.
if (path.relative(paths.appPath, baseUrlResolved) === "") {
return null;
}
// Otherwise, throw an error.
throw new Error("Your project's `baseUrl` can only be set to `src` or `node_modules`.");
}
/**
* Get webpack aliases based on the baseUrl of a compilerOptions object.
*
* @param {*} options
*/
function getWebpackAliases(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return {};
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
if (path.relative(paths.appPath, baseUrlResolved) === "") {
return {
src: paths.appSrc,
};
}
}
/**
* Get jest aliases based on the baseUrl of a compilerOptions object.
*
* @param {*} options
*/
function getJestAliases(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return {};
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
if (path.relative(paths.appPath, baseUrlResolved) === "") {
return {
"^src/(.*)$": "<rootDir>/src/$1",
};
}
}
function getModules() {
// Check if TypeScript is setup
const hasTsConfig = fs.existsSync(paths.appTsConfig);
let config;
// If there's a tsconfig.json we assume it's a
// TypeScript project and set up the config
// based on tsconfig.json
if (hasTsConfig) {
const ts = require(resolve.sync("typescript", {
basedir: paths.appNodeModules,
}));
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
// Otherwise we'll check if there is jsconfig.json
// for non TS projects.
}
config = config || {};
const options = config.compilerOptions || {};
const additionalModulePaths = getAdditionalModulePaths(options);
return {
additionalModulePaths: additionalModulePaths,
webpackAliases: getWebpackAliases(options),
jestAliases: getJestAliases(options),
hasTsConfig,
};
}
export default getModules();

View File

@@ -0,0 +1,57 @@
import path from "node:path";
import fs from "node:fs";
import { currentDirName } from "../dev-utils/util.js";
const currentDir = currentDirName(import.meta.url);
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);
const moduleFileExtensions = [
"web.mjs",
"mjs",
"web.js",
"js",
"web.ts",
"ts",
"web.tsx",
"tsx",
"json",
"web.jsx",
"jsx",
];
const resolveModule = (resolveFn, filePath) => {
const extension = moduleFileExtensions.find((extension) =>
fs.existsSync(resolveFn(`${filePath}.${extension}`))
);
if (extension) {
return resolveFn(`${filePath}.${extension}`);
}
return resolveFn(`${filePath}.js`);
};
const resolveOwn = (relativePath) => path.resolve(currentDir, "..", relativePath);
const paths = {
resolveApp,
appOutPath: resolveOwn(".out"),
appOutPackageJson: resolveOwn(".out/package.json"),
appPath: resolveApp("."),
appHtml: resolveOwn("ide/index.html"),
appRoot: resolveOwn("ide"),
appBaseTsConfig: resolveOwn("ide/tsconfig.json"),
appPackageJson: resolveApp("package.json"),
appSrc: resolveApp("src"),
appLocales: resolveApp("locales"),
compsIndexJs: resolveModule(resolveApp, "src/index"),
appViteConfigJs: resolveModule(resolveApp, "vite.config"),
appTsConfig: resolveApp("tsconfig.json"),
yarnLockFile: resolveApp("yarn.lock"),
appNodeModules: resolveApp("node_modules"),
appWebpackCache: resolveApp("node_modules/.cache"),
appTsBuildInfoFile: resolveApp("node_modules/.cache/tsconfig.tsbuildinfo"),
};
export default paths;

View File

@@ -0,0 +1,67 @@
import react from "@vitejs/plugin-react";
import svgrPlugin from "vite-plugin-svgr";
import global from "rollup-plugin-external-globals";
import { buildVars } from "../dev-utils/buildVars.js";
import injectCss from "vite-plugin-css-injected-by-js";
import { getLibNames, getAllLibGlobalVarNames } from "../dev-utils/external.js";
import paths from "./paths.js";
import { defineConfig } from "vite";
import { readJson } from "../dev-utils/util.js";
const isProduction = process.env.NODE_ENV === "production";
const packageJson = readJson(paths.appPackageJson);
const define = {};
buildVars.forEach(({ name, defaultValue }) => {
define[name] = JSON.stringify(process.env[name] || defaultValue);
});
export default defineConfig({
define: {
...define,
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
__LOWCODER_ORG__: JSON.stringify({}),
},
assetsInclude: ["**/*.md"],
resolve: {
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json"],
},
build: {
target: "es2020",
cssTarget: "chrome87",
outDir: paths.appOutPath,
emptyOutDir: true,
lib: {
formats: ["es"],
entry: paths.compsIndexJs,
fileName: "index",
},
rollupOptions: {
external: getLibNames(),
output: {
chunkFileNames: "[hash].js",
},
},
},
plugins: [
react({
babel: {
compact: false,
parserOpts: {
plugins: ["decorators-legacy"],
},
},
}),
svgrPlugin({
svgrOptions: {
exportType: "named",
prettier: false,
svgo: false,
titleProp: true,
ref: true,
},
}),
isProduction && global(getAllLibGlobalVarNames(), { exclude: [/\.css$/] }),
isProduction && injectCss({ styleId: `${packageJson.name}-${packageJson.version}` }),
].filter(Boolean),
});

View File

@@ -0,0 +1,58 @@
export const buildVars = [
{
name: "PUBLIC_URL",
defaultValue: "/",
},
{
name: "REACT_APP_EDITION",
defaultValue: "community",
},
{
name: "REACT_APP_LANGUAGES",
defaultValue: "",
},
{
name: "REACT_APP_COMMIT_ID",
defaultValue: "00000",
},
{
name: "REACT_APP_API_SERVICE_URL",
defaultValue: "",
},
{
name: "REACT_APP_NODE_SERVICE_URL",
defaultValue: "",
},
{
name: "REACT_APP_ENV",
defaultValue: "production",
},
{
name: "REACT_APP_BUILD_ID",
defaultValue: "",
},
{
name: "REACT_APP_LOG_LEVEL",
defaultValue: "error",
},
{
name: "REACT_APP_IMPORT_MAP",
defaultValue: "{}",
},
{
name: "REACT_APP_SERVER_IPS",
defaultValue: "",
},
{
name: "REACT_APP_BUNDLE_BUILTIN_PLUGIN",
defaultValue: "",
},
{
name: "REACT_APP_BUNDLE_TYPE",
defaultValue: "app",
},
{
name: "REACT_APP_DISABLE_JS_SANDBOX",
defaultValue: "",
},
];

View File

@@ -0,0 +1,102 @@
/**
* libs to import as global var
* name: module name
* mergeDefaultAndNameExports: whether to merge default and named exports
*/
export const libs = [
"axios",
"redux",
"react-router",
"react-router-dom",
"react-redux",
"react",
"react-dom",
"lodash",
"history",
"antd",
"@dnd-kit/core",
"@dnd-kit/modifiers",
"@dnd-kit/sortable",
"@dnd-kit/utilities",
{
name: "moment",
extractDefault: true,
},
{
name: "dayjs",
extractDefault: true,
},
{
name: "lowcoder-sdk",
from: "./src/index.sdk.ts",
},
{
name: "styled-components",
mergeDefaultAndNameExports: true,
},
];
/**
* get global var name from module name
* @param {string} name
* @returns
*/
export const getLibGlobalVarName = (name) => {
return "$" + name.replace(/@/g, "$").replace(/[\/\-]/g, "_");
};
export const getLibNames = () => {
return libs.map((i) => {
if (typeof i === "object") {
return i.name;
}
return i;
});
};
export const getAllLibGlobalVarNames = () => {
const ret = {};
libs.forEach((lib) => {
let name = lib;
if (typeof lib === "object") {
name = lib.name;
}
ret[name] = getLibGlobalVarName(name);
});
return ret;
};
export const libsImportCode = (exclude = []) => {
const importLines = [];
const assignLines = [];
libs.forEach((i) => {
let name = i;
let merge = false;
let from = name;
let extractDefault = false;
if (typeof i === "object") {
name = i.name;
merge = i.mergeDefaultAndNameExports ?? false;
from = i.from ?? name;
extractDefault = i.extractDefault ?? false;
}
if (exclude.includes(name)) {
return;
}
const varName = getLibGlobalVarName(name);
if (merge) {
importLines.push(`import * as ${varName}_named_exports from '${from}';`);
importLines.push(`import ${varName} from '${from}';`);
assignLines.push(`Object.assign(${varName}, ${varName}_named_exports);`);
} else if (extractDefault) {
importLines.push(`import ${varName} from '${from}';`);
} else {
importLines.push(`import * as ${varName} from '${from}';`);
}
assignLines.push(`window.${varName} = ${varName};`);
});
return importLines.concat(assignLines).join("\n");
};

View File

@@ -0,0 +1,18 @@
import { libsImportCode } from "./external.js";
export function globalDepPlugin(exclude = []) {
const virtualModuleId = "virtual:globals";
return {
name: "lowcoder-global-plugin",
resolveId(id) {
if (id === virtualModuleId) {
return id;
}
},
load(id) {
if (id === virtualModuleId) {
return libsImportCode(exclude);
}
},
};
}

View File

@@ -0,0 +1,28 @@
import fs from "node:fs";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
export function stripLastSlash(str) {
if (str.endsWith("/")) {
return str.slice(0, str.length - 1);
}
return str;
}
export function ensureLastSlash(str) {
if (!str) {
return "/";
}
if (!str.endsWith("/")) {
return `${str}/`;
}
return str;
}
export function readJson(file) {
return JSON.parse(fs.readFileSync(file).toString());
}
export function currentDirName(importMetaUrl) {
return dirname(fileURLToPath(importMetaUrl));
}

View File

@@ -0,0 +1,8 @@
declare global {
interface Window {
printPerf: () => void;
__LOWCODER_ORG__?: {};
}
}
export {};

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env node
import("./util/log.js");
import fs from "node:fs";
import { Command } from "commander";
import initAction from "./actions/init.js";
import buildAction from "./actions/build.js";
const program = new Command();
const pkg = JSON.parse(fs.readFileSync("./package.json").toString());
program.name(pkg.name).description(pkg.description);
program
.command("init")
.description("init project")
.option("-t, --template", "template name", "typescript")
.option("--registry [addr]", "npm registry")
.action(initAction);
program
.command("build")
.description("build component lib")
.option("--outDir", "where to place tar ball", "./")
.option("--publish", "publish to npm", false)
.action(buildAction);
program.parse();

View File

@@ -0,0 +1,49 @@
{
"name": "lowcoder-cli",
"description": "CLI tool used to start build publish lowcoder components",
"version": "0.0.30",
"license": "MIT",
"bin": "./index.js",
"type": "module",
"exports": {
".": {
"import": "./index.js",
"require": "./index.js"
},
"./config/vite.config": {
"import": "./config/vite.config.js",
"require": "./config/vite.config.js"
},
"./client": {
"types": "./client.d.ts"
},
"./actions/init.js": {
"import": "./actions/init.js",
"require": "./actions/init.js"
}
},
"dependencies": {
"@vitejs/plugin-react": "^2.2.0",
"axios": "^1.7.4",
"chalk": "4",
"commander": "^9.4.1",
"cross-spawn": "^7.0.3",
"fs-extra": "^10.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-json-view": "^1.21.3",
"rollup-plugin-external-globals": "^0.7.1",
"vite": "^4.5.5",
"vite-plugin-css-injected-by-js": "^2.1.1",
"vite-plugin-svgr": "^2.2.2"
},
"devDependencies": {
"typescript": "^4.8.4"
},
"peerDependencies": {
"lowcoder-sdk": "*"
},
"keywords": [
"lowcoder"
]
}

View File

@@ -0,0 +1,30 @@
import chalk from "chalk";
const colors = [
"black",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white",
"gray",
"redBright",
"greenBright",
"yellowBright",
"blueBright",
"magentaBright",
"cyanBright",
"whiteBright",
];
colors.forEach((color) => {
console[color] = (text) => {
console.log(chalk[color](text));
};
});
export const logBug = (text) => {
console.red(`${text}\n This maybe is a bug, you can issue a bug for us.`);
};

View File

@@ -0,0 +1,4 @@
/lib
/node_modules
/package
*.tgz

View File

@@ -0,0 +1,120 @@
# Lowcoder Extra Components
This is the workspace for Lowcoder Extra Components like Calendar, Image Editor, Mermaid Charts and eCharts.
## Local Development preparation
Navigate your terminal or bash to your /root folder (lowcoder repository) to install Lowcoder Extra Components dependencies and the Lowcoder SDK
To develop with the Lowcoder Extra Components after you clone the Lowcoder Repository, first make sure the Lowcoder SDK is local built.
```bash
cd client/packages/lowcoder-sdk
yarn build
```
## Start
Now you can start the local dev server for Lowcoder Extra Components to develop and add your Component Plugin
```bash
cd client/packages/lowcoder-comps
yarn start
```
The local dev server will build for roughly a minute and open then a Browser Window on http://localhost:9000/ with the Lowcoder Component Builder.
## Local development
After the local dev server is started, the Lowcoder Component Builder is prepared. A new browser window should open at http://localhost:9000 This is the Components Preview, which allows you to see your new component in action, as it would work in the Lowcoder Editor.
Data, methods and properties are visible and interactive, so you can test your Component during development. The view will get automatically refreshed.
The Lowcoder Component Builder makes the development & publishing of multiple individual components as bundle possible. Find the /src/comps folder in /lowcoder-comps. Here are existing components to find. It is suggested for new components to create a new folder. In the left navigation of the Components Preview you can switch between your components.
to see your component and include it in the processing on the development server, you have to do the folloiwing steps:
### modify /lowcoder-comps/package.json
```JSON
"yournewcomponent": {
"name": "Your new Component name",
"icon": "./icons/your-icon.svg",
"description": "A Component Plugin to ...",
"category": "itemHandling",
"layoutInfo": {
"w": 6,
"h": 30
}
}
```
Please choose one category out of:
- dashboards
- layout
- forms
- collaboration
- projectmanagement
- scheduling
- documents
- itemHandling
- multimedia
- integration
layoutInfo helps you to define the size (in grid-cells) of your Component in the grid for the very first moment, when a user drags your Component out of the components display on the right side in the Lowcoder Editor.
### modify /lowcoder-comps/src/index.ts
```JavaScript
Add your Component for the exported members of Lowcoder Extra Components
import { ChartCompWithDefault } from "./comps/chartComp/chartComp";
import { ImageEditorComp } from "./comps/imageEditorComp/index";
import { CalendarComp } from "./comps/calendarComp/calendarComp";
import { MermaidComp } from "comps/mermaidComp";
import { YourComponent } from "comps/yourComponentFolder/yourComponent";
export default {
chart: ChartCompWithDefault,
imageEditor: ImageEditorComp,
calendar: CalendarComp,
mermaid: MermaidComp,
yourcomponent: YourComponent,
};
```
Now your Plugin should be visibe and displayed in the Lowcoder Component Builder at http://localhost:9000/
## Build
When you finish development and all tests, you can build the Components to use it in runtime.
This will build the current Component Plugins into a .tgz file that you can upload.
**Before build you should change the version in package.json file.**
```bash
yarn build
# or
npm run build
```
## How to publish a Component Plugin
With the following command you can publish the script to the NPM repository:
```bash
yarn build --publish
```
This command will publis the whole Lowcoder Extra Components bundle to [NPMjs](https://www.npmjs.com/)
Make sure, you updated the Version of Lowcoder Comps before in /lowcoder-comps/package.json
## Contribute your Plugin
If you wish to contribute your plugin and persist it as general Lowcoder Extra Component, please raise a PR to our /dev branch in the Lowcoder Community-Edition Repository https://github.com/lowcoder-org/lowcoder

View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48px" height="48px" viewBox="0 0 48 48" version="1.1">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(6.000000, 11.500000)" stroke="#D7D9E0" stroke-linecap="round" stroke-width="2">
<line x1="-2.00715996e-17" y1="0.5" x2="36" y2="0.5"/>
<line x1="-2.00715996e-17" y1="8.5" x2="36" y2="8.5"/>
<line x1="2.70913881e-14" y1="16.5" x2="36" y2="16.5"/>
<line x1="2.70913881e-14" y1="24.5" x2="36" y2="24.5"/>
</g>
<g transform="translate(11.000000, 10.000000)" fill="#3377FF">
<path d="M11,0 L15,0 C15.5522847,-1.01453063e-16 16,0.44771525 16,1 L16,25 L16,25 L10,25 L10,1 C10,0.44771525 10.4477153,7.67586877e-16 11,0 Z"/>
<path d="M1,13 L5,13 C5.55228475,13 6,13.4477153 6,14 L6,25 L6,25 L0,25 L0,14 C-6.76353751e-17,13.4477153 0.44771525,13 1,13 Z"/>
<path d="M21,6 L25,6 C25.5522847,6 26,6.44771525 26,7 L26,25 L26,25 L20,25 L20,7 C20,6.44771525 20.4477153,6 21,6 Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(6.000000, 5.000000)">
<rect stroke="#D7D9E0" stroke-width="2" fill="#FFFFFF" x="1" y="4" width="34" height="30" rx="3"></rect>
<g transform="translate(5.000000, 14.000000)">
<rect fill="#3377FF" x="14" y="0" width="5" height="5" rx="1.5"></rect>
<rect fill="#8B8FA3" x="7" y="0" width="5" height="5" rx="1.5"></rect>
<rect fill="#3377FF" x="14" y="6" width="5" height="5" rx="1.5"></rect>
<rect fill="#8B8FA3" x="7" y="6" width="5" height="5" rx="1.5"></rect>
<rect fill="#8B8FA3" x="0" y="6" width="5" height="5" rx="1.5"></rect>
<rect fill="#8B8FA3" x="7" y="12" width="5" height="5" rx="1.5"></rect>
<rect fill="#8B8FA3" x="0" y="12" width="5" height="5" rx="1.5"></rect>
<rect fill="#8B8FA3" x="21" y="0" width="5" height="5" rx="1.5"></rect>
<rect fill="#8B8FA3" x="21" y="6" width="5" height="5" rx="1.5"></rect>
</g>
<path d="M3,3 L33,3 C34.6568542,3 36,4.34314575 36,6 L36,12 L36,12 L0,12 L0,6 C-2.02906125e-16,4.34314575 1.34314575,3 3,3 Z" fill="#D7D9E0"></path>
<rect fill="#D7D9E0" x="7" y="0" width="4" height="8" rx="1"></rect>
<rect fill="#D7D9E0" x="25" y="0" width="4" height="8" rx="1"></rect>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.9.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 490.2 490.2" style="enable-background:new 0 0 490.2 490.2;" xml:space="preserve">
<style type="text/css">
.st0{fill:#E6E7E8;}
.st1{fill:#3377FF;}
</style>
<path class="st0" d="M84.6,0h320.9c46.7,0,84.6,37.9,84.6,84.6v320.9c0,46.7-37.9,84.6-84.6,84.6H84.6C37.9,490.2,0,452.3,0,405.5
V84.6C0,37.9,37.9,0,84.6,0z"/>
<path class="st1" d="M407.5,111.2c-72.1-3.1-137.8,41-162.4,108.8c-24.6-67.8-90.3-111.9-162.4-108.8
c-2.4,57.1,24.8,111.4,72.1,143.6c24.2,16.6,38.6,44.1,38.5,73.5v50.9h103.6v-50.9c-0.1-29.3,14.3-56.8,38.5-73.4
C382.6,222.6,409.9,168.3,407.5,111.2L407.5,111.2z"/>
<path class="st1" d="M160.6,328.3c0.1-18.6-9-36-24.3-46.5c-10.2-7-19.8-14.9-28.5-23.7c-9.5-9.5-17.9-19.9-25.3-31.1v152.1h78.1
V328.3z"/>
<path class="st1" d="M329.5,328.3c-0.1-18.6,9-36,24.3-46.5c10.2-7,19.8-14.9,28.5-23.7c9.5-9.5,17.9-19.9,25.3-31.1v152.1h-78.1
V328.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lowcoder Comp Playground</title>
<style>
#root {
height: 100vh;
}
#root-loader {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<div id="root">
<div id="root-loader">Loading...</div>
</div>
<script src="index.tsx" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,21 @@
import { createRoot } from "react-dom/client";
import { CompIDE } from "lowcoder-sdk";
import { name, version, lowcoder } from "./package.json";
import compMap from "./src/index";
import "lowcoder-sdk/dist/style.css";
function CompDevApp() {
return (
<CompIDE
compMap={compMap}
packageName={name}
packageVersion={version}
compMeta={lowcoder.comps}
/>
);
}
const container = document.querySelector("#root");
const root = createRoot(container!);
root.render(<CompDevApp />);

View File

@@ -0,0 +1,6 @@
import config from "../../config/test/jest.config.js";
export default {
...config,
setupFiles: [...config.setupFiles],
};

View File

@@ -0,0 +1,268 @@
{
"name": "lowcoder-comps",
"version": "2.7.5",
"type": "module",
"license": "MIT",
"dependencies": {
"@fullcalendar/adaptive": "^6.1.11",
"@fullcalendar/core": "^6.1.6",
"@fullcalendar/daygrid": "^6.1.6",
"@fullcalendar/interaction": "^6.1.6",
"@fullcalendar/list": "^6.1.9",
"@fullcalendar/moment": "^6.1.6",
"@fullcalendar/multimonth": "^6.1.6",
"@fullcalendar/react": "^6.1.6",
"@fullcalendar/resource": "^6.1.11",
"@fullcalendar/resource-timegrid": "^6.1.11",
"@fullcalendar/resource-timeline": "^6.1.11",
"@fullcalendar/timegrid": "^6.1.6",
"@fullcalendar/timeline": "^6.1.6",
"agora-rtc-sdk-ng": "^4.20.2",
"agora-rtm-sdk": "^1.5.1",
"big.js": "^6.2.1",
"echarts-extension-gmap": "^1.6.0",
"echarts-gl": "^2.0.9",
"echarts-wordcloud": "^2.1.0",
"lowcoder-cli": "workspace:^",
"lowcoder-sdk": "workspace:^",
"mermaid": "^10.6.1",
"react": "18.3.0",
"react-dom": "18.3.0",
"typescript": "4.8.4"
},
"lowcoder": {
"description": "",
"comps": {
"calendar": {
"name": "Calendar",
"icon": "./icons/icon-comp-calendar.svg",
"layoutInfo": {
"w": 19,
"h": 60
}
},
"chart": {
"name": "Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"basicChart": {
"name": "Basic Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"barChart": {
"name": "Bar Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"lineChart": {
"name": "Line Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"pieChart": {
"name": "Pie Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"scatterChart": {
"name": "Scatter Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"boxplotChart": {
"name": "Boxplot Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"parallelChart": {
"name": "Parallel Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"line3dChart": {
"name": "Line3D Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"imageEditor": {
"name": "Image Editor",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"mermaid": {
"name": "Mermaid",
"icon": "./icons/mermaidchart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"funnelChart": {
"name": "Funnel Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"gaugeChart": {
"name": "Gauge Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"sankeyChart": {
"name": "Sankey Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"candleStickChart": {
"name": "CandleStick Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"radarChart": {
"name": "Radar Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"heatmapChart": {
"name": "Heatmap Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"graphChart": {
"name": "Graph Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"treeChart": {
"name": "Tree Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"treemapChart": {
"name": "Treemap Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"sunburstChart": {
"name": "Sunburst Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"themeriverChart": {
"name": "Themeriver Chart",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 12,
"h": 40
}
},
"chartsGeoMap": {
"name": "chartsGeoMap",
"icon": "./icons/icon-chart.svg",
"layoutInfo": {
"w": 19,
"h": 60
}
},
"meetingController": {
"name": "Agora Meeting Controller",
"icon": "./icons/icon-comp-calendar.svg",
"layoutInfo": {
"w": 1,
"h": 1
}
},
"meetingSharing": {
"name": "Agora Meeting Sharing",
"icon": "./icons/icon-comp-calendar.svg",
"layoutInfo": {
"w": 6,
"h": 40
}
},
"meetingStream": {
"name": "Video Stream",
"icon": "./icons/icon-comp-calendar.svg",
"layoutInfo": {
"w": 6,
"h": 40
}
}
}
},
"scripts": {
"start": "NODE_OPTIONS=--max_old_space_size=6442 vite",
"build": "NODE_OPTIONS=--max_old_space_size=6442 yarn test && lowcoder-cli build",
"build_only": "NODE_OPTIONS=--max_old_space_size=6442 lowcoder-cli build",
"build_publish": "NODE_OPTIONS=--max_old_space_size=6442 lowcoder-cli build --publish",
"test": "jest"
},
"devDependencies": {
"@types/react": "18",
"@types/react-dom": "18",
"jest": "29.3.0",
"vite": "^4.5.5",
"vite-tsconfig-paths": "^3.6.0"
}
}

View File

@@ -0,0 +1,61 @@
import _ from "lodash";
import { CompConstructor, CustomAction } from "lowcoder-core";
import { evalAndReduce, isExposingMethodComp } from "lowcoder-sdk";
import { ChartCompWithDefault } from "comps/chartComp/chartComp";
import log from "loglevel";
const COMPS_MAP = {
chart: ChartCompWithDefault,
} as Record<string, CompConstructor>;
const compContextActionMap = {} as Record<string, CustomAction>;
test("test comp don't change if no value change", () => {
Object.keys(COMPS_MAP).forEach((name) => {
const Comp = COMPS_MAP[name];
let comp = new Comp({});
if (compContextActionMap[name]) {
// set context
comp = comp.reduce(compContextActionMap[name]);
}
comp.getView(); // getView() is allowed when unevaluated
comp = evalAndReduce(comp);
expect(comp.node() === comp.node()).toBe(true);
expect(comp.node()?.evaluate() === comp.node()?.evaluate()).toBe(true);
const comp2 = evalAndReduce(comp);
if (comp2 !== comp) {
const comp3 = evalAndReduce(comp2);
if (comp3 === comp2) {
// log.debug(`${name} need eval twice to converge`);
} else {
const newComp = evalAndReduce(comp);
Object.keys((comp as any).children).forEach((key) => {
log.log(
`${key}, isEqual ${(comp as any).children[key] === (newComp as any).children[key]}`
);
});
throw new Error("bad " + name);
}
}
});
});
test("comp exposing method duplicate name", () => {
// one comp can't expose two methods with the same name
Object.values(COMPS_MAP).forEach((Comp) => {
const comp = new Comp({});
const methods = [];
if (isExposingMethodComp(comp)) {
methods.push(comp.getMethodConfig());
}
const childrenMap = (comp as any).children;
childrenMap &&
Object.values(childrenMap).forEach((child) => {
if (isExposingMethodComp(child as any)) {
methods.push((child as any).getMethodConfig());
}
});
const allMethodName = methods.flatMap((m) => m.map((methodConfig) => methodConfig.name));
expect(allMethodName.length).toEqual(_.uniq(allMethodName).length);
});
});

View File

@@ -0,0 +1,3 @@
/// <reference types="lowcoder-cli/client" />
declare module "lowcoder-sdk";

View File

@@ -0,0 +1,37 @@
import { CheckBox, controlItem, Switch, SwitchWrapper } from "lowcoder-design";
import { ReactNode } from "react";
import { ControlParams } from "comps/controls/controlParams";
import { SimpleComp } from "lowcoder-core";
export class BoolShareVideoControl extends SimpleComp<boolean> {
readonly IGNORABLE_DEFAULT_VALUE = false;
protected getDefaultValue(): boolean {
return false;
}
getPropertyView(): ReactNode {
return (
<Switch
value={this.value}
onChange={(x) => this.dispatchChangeValueAction(x)}
/>
);
}
propertyView(params: ControlParams & { type?: "switch" | "checkbox" }) {
return controlItem(
{ filterText: params.label },
<SwitchWrapper {...params}>
{params.type === "checkbox" ? (
<CheckBox
style={{ marginRight: "8px" }}
checked={this.value}
onChange={(x) => this.dispatchChangeValueAction(x.target.checked)}
/>
) : (
this.getPropertyView()
)}
</SwitchWrapper>
);
}
}

View File

@@ -0,0 +1,902 @@
import {
NameConfig,
BoolControl,
withDefault,
withExposingConfigs,
StringControl,
Section,
sectionNames,
styleControl,
BooleanStateControl,
AutoHeightControl,
stringStateControl,
InnerGrid,
useUserViewMode,
getData,
gridItemCompToGridItems,
Layers,
isNumeric,
withMethodExposing,
eventHandlerControl,
DrawerStyle,
PositionControl,
jsonObjectExposingStateControl,
stateComp,
Drawer,
changeChildAction,
HintPlaceHolder,
// styledm,
// DrawerWrapper,
BackgroundColorContext,
ContainerCompBuilder,
closeEvent,
MeetingEventHandlerControl,
} from "lowcoder-sdk";
import { default as CloseOutlined } from "@ant-design/icons/CloseOutlined";
import type { JSONValue } from "../../../../lowcoder/src/util/jsonTypes";
// import { default as Button } from "antd/es/button";
const EventOptions = [closeEvent] as const;
import { trans } from "../../i18n/comps";
// const DrawerWrapper = styledm.div`
// // Shield the mouse events of the lower layer, the mask can be closed in the edit mode to prevent the lower layer from sliding
// pointer-events: auto;
// `;
import AgoraRTC, {
type ICameraVideoTrack,
type IMicrophoneAudioTrack,
type IAgoraRTCClient,
type IAgoraRTCRemoteUser,
type UID,
type ILocalVideoTrack,
} from "agora-rtc-sdk-ng";
import type { RtmChannel, RtmClient } from "agora-rtm-sdk";
import { useCallback, useEffect, useState } from "react";
import { ResizeHandle } from "react-resizable";
import { v4 as uuidv4 } from "uuid";
const DEFAULT_SIZE = 378;
const DEFAULT_PADDING = 16;
function transToPxSize(size: string | number) {
return isNumeric(size) ? size + "px" : (size as string);
}
export const client: IAgoraRTCClient = AgoraRTC.createClient({
mode: "rtc",
codec: "vp8",
});
AgoraRTC.setLogLevel(4);
/*
0: DEBUG. Output all API logs.
1: INFO. Output logs of the INFO, WARNING and ERROR level.
2: WARNING. Output logs of the WARNING and ERROR level.
3: ERROR. Output logs of the ERROR level.
4: NONE. Do not output any log.
*/
let audioTrack: IMicrophoneAudioTrack;
let videoTrack: ICameraVideoTrack;
let screenShareStream: ILocalVideoTrack;
let userId: UID | null | undefined;
let rtmChannelResponse: RtmChannel;
let rtmClient: RtmClient;
// const ButtonStyle = styledm(Button)`
// position: absolute;
// left: 0;
// top: 0;
// z-index: 10;
// font-weight: 700;
// box-shadow: none;
// color: rgba(0, 0, 0, 0.45);
// height: 54px;
// width: 54px;
// svg {
// width: 16px;
// height: 16px;
// }
// &,
// :hover,
// :focus {
// background-color: transparent;
// border: none;
// }
// :hover,
// :focus {
// color: rgba(0, 0, 0, 0.75);
// }
// `;
const turnOnCamera = async (flag?: boolean) => {
if (videoTrack) {
return videoTrack.setEnabled(flag!);
}
videoTrack = await AgoraRTC.createCameraVideoTrack();
videoTrack.play(userId + "");
};
const turnOnMicrophone = async (flag?: boolean) => {
if (audioTrack) {
return audioTrack.setEnabled(flag!);
}
audioTrack = await AgoraRTC.createMicrophoneAudioTrack();
if (!flag) {
await client.unpublish(audioTrack);
} else {
await client.publish(audioTrack);
}
};
const shareScreen = async (sharing: boolean) => {
try {
if (sharing === false) {
await client.unpublish(screenShareStream);
screenShareStream.close();
await client.publish(videoTrack);
videoTrack.play(userId + "");
} else {
screenShareStream = await AgoraRTC.createScreenVideoTrack(
{
screenSourceType: "screen",
},
"disable"
);
await client.unpublish(videoTrack);
screenShareStream.play("share-screen");
await client.publish(screenShareStream);
}
} catch (error) {
console.error("Failed to create screen share stream:", error);
}
};
const leaveChannel = async () => {
//stops local sharing video
if (screenShareStream) {
screenShareStream.close();
}
//stops local video streaming and puts off the camera
if (videoTrack) {
await client.unpublish(videoTrack);
await turnOnCamera(false);
}
//mutes and stops locla audio stream
if (audioTrack) {
await turnOnMicrophone(false);
}
await client.leave();
await rtmChannelResponse.leave();
};
const publishVideo = async (
appId: string,
channel: string,
rtmToken: string,
rtcToken: string
) => {
await turnOnCamera(true);
await client.join(appId, channel, rtcToken, userId);
await client.publish(videoTrack);
await rtmInit(appId, userId, rtmToken, channel);
};
const sendMessageRtm = (message: any) => {
rtmChannelResponse.sendMessage({ text: JSON.stringify(message) });
};
const sendPeerMessageRtm = (message: any, toId: string) => {
rtmClient.sendMessageToPeer({ text: JSON.stringify(message) }, toId);
};
const rtmInit = async (appId: any, uid: any, token: any, channel: any) => {
const AgoraRTM = (await import("agora-rtm-sdk")).default;
rtmClient = AgoraRTM.createInstance(appId);
let options = {
uid: String(uid),
token: token ? token : null,
};
await rtmClient.login(options);
rtmChannelResponse = rtmClient.createChannel(channel);
await rtmChannelResponse.join();
};
const CanvasContainerID = "__canvas_container__";
const meetingControllerChildren = {
visible: withDefault(BooleanStateControl, "false"),
onEvent: eventHandlerControl(EventOptions),
onMeetingEvent: MeetingEventHandlerControl,
width: StringControl,
height: StringControl,
autoHeight: AutoHeightControl,
style: styleControl(DrawerStyle),
placement: PositionControl,
maskClosable: withDefault(BoolControl, true),
showMask: withDefault(BoolControl, true),
meetingActive: withDefault(BooleanStateControl, "false"),
audioControl: withDefault(BooleanStateControl, "false"),
videoControl: withDefault(BooleanStateControl, "true"),
endCall: withDefault(BooleanStateControl, "false"),
sharing: withDefault(BooleanStateControl, "false"),
appId: withDefault(StringControl, trans("meeting.appid")),
participants: stateComp<JSONValue>([]),
usersScreenShared: stateComp<JSONValue>([]),
localUser: jsonObjectExposingStateControl(""),
localUserID: withDefault(
stringStateControl(trans("meeting.localUserID")),
uuidv4() + ""
),
meetingName: withDefault(
stringStateControl(trans("meeting.meetingName")),
uuidv4() + ""
),
rtmToken: stringStateControl(trans("meeting.rtmToken")),
rtcToken: stringStateControl(trans("meeting.rtcToken")),
messages: stateComp<JSONValue>([]),
};
let MeetingControllerComp = () => (
<div>
Meeting Component is not available. It needs Lowcoder from Version v2.4
</div>
);
if (typeof ContainerCompBuilder === "function") {
let MTComp = (function () {
return new ContainerCompBuilder(
meetingControllerChildren,
(props: any, dispatch: any) => {
const isTopBom = ["top", "bottom"].includes(props.placement);
const { items, ...otherContainerProps } = props.container;
const userViewMode = useUserViewMode();
const resizable = !userViewMode && (!isTopBom || !props.autoHeight);
const onResizeStop = useCallback(
(
e: React.SyntheticEvent,
node: HTMLElement,
size: { width: number; height: number },
handle: ResizeHandle
) => {
isTopBom
? dispatch(changeChildAction("height", size.height, true))
: dispatch(changeChildAction("width", size.width, true));
},
[dispatch, isTopBom]
);
const [userIds, setUserIds] = useState<any>([]);
const [updateVolume, setUpdateVolume] = useState<any>({
update: false,
userid: null,
});
const [rtmMessages, setRtmMessages] = useState<any>([]);
const [localUserSpeaking, setLocalUserSpeaking] = useState<any>(false);
const [localUserVideo, setLocalUserVideo] =
useState<IAgoraRTCRemoteUser>();
const [userJoined, setUserJoined] = useState<IAgoraRTCRemoteUser>();
const [userLeft, setUserLeft] = useState<IAgoraRTCRemoteUser>();
useEffect(() => {
if (userJoined) {
// console.log("userJoined ", userJoined);
let prevUsers: any[] = props.participants as [];
// console.log("prevUsers ", prevUsers);
let userData = {
user: userJoined.uid,
audiostatus: userJoined.hasAudio,
streamingVideo: true,
};
// console.log("userData ", userData);
setUserIds((userIds: any) => [...userIds, userData]);
// console.log("userIds ", userIds);
/* console.log(
"removeDuplicates ",
removeDuplicates(getData([...prevUsers, userData]).data, "user")
); */
dispatch(
changeChildAction(
"participants",
removeDuplicates(
getData([...prevUsers, userData]).data,
"user"
),
false
)
);
}
}, [userJoined]);
function removeDuplicates(arr: any, prop: any) {
const uniqueObjects = [];
const seenValues = new Set();
for (const obj of arr) {
const objValue = obj[prop];
if (!seenValues.has(objValue)) {
seenValues.add(objValue);
uniqueObjects.push(obj);
}
}
return uniqueObjects;
}
useEffect(() => {
if (userLeft) {
let newUsers = userIds.filter(
(item: any) => item.user !== userLeft.uid
);
let hostExists = newUsers.filter((f: any) => f.host === true);
if (hostExists.length == 0 && newUsers.length > 0) {
newUsers[0].host = true;
}
setUserIds(newUsers);
dispatch(
changeChildAction(
"participants",
removeDuplicates(getData(newUsers).data, "user"),
false
)
);
}
}, [userLeft]);
// console.log("sharing", props.sharing);
useEffect(() => {
if (updateVolume.userid) {
let prevUsers: [] = props.participants as [];
const updatedItems = prevUsers.map((userInfo: any) => {
if (
userInfo.user === updateVolume.userid &&
userInfo.speaking != updateVolume.update
) {
return { ...userInfo, speaking: updateVolume.update };
}
return userInfo;
});
dispatch(
changeChildAction(
"participants",
getData(updatedItems).data,
false
)
);
}
}, [updateVolume]);
useEffect(() => {
let prevUsers: [] = props.participants as [];
const updatedItems = prevUsers.map((userInfo: any) => {
if (userInfo.user === localUserVideo?.uid) {
return { ...userInfo, streamingSharing: props.sharing.value };
}
return userInfo;
});
dispatch(
changeChildAction("participants", getData(updatedItems).data, false)
);
let localObject = {
user: userId + "",
audiostatus: props.audioControl.value,
streamingVideo: props.videoControl.value,
streamingSharing: props.sharing.value,
speaking: localUserSpeaking,
};
props.localUser.onChange(localObject);
}, [props.sharing.value]);
// console.log("participants ", props.participants);
useEffect(() => {
let prevUsers: [] = props.participants as [];
const updatedItems = prevUsers.map((userInfo: any) => {
if (userInfo.user === localUserVideo?.uid) {
return { ...userInfo, streamingVideo: localUserVideo?.hasVideo };
}
return userInfo;
});
dispatch(
changeChildAction("participants", getData(updatedItems).data, false)
);
}, [localUserVideo?.hasVideo]);
useEffect(() => {
if (rtmMessages) {
dispatch(
changeChildAction("messages", getData(rtmMessages).data, false)
);
}
}, [rtmMessages]);
useEffect(() => {
if (localUserSpeaking === true || localUserVideo) {
let localObject = {
user: userId + "",
audiostatus: props.audioControl.value,
streamingVideo: props.videoControl.value,
speaking: localUserSpeaking,
};
props.localUser.onChange(localObject);
}
}, [localUserSpeaking]);
useEffect(() => {
if (rtmChannelResponse) {
rtmClient.on("MessageFromPeer", function (message, peerId) {
setRtmMessages((prevMessages: any[]) => {
// Check if the messages array exceeds the maximum limit
if (prevMessages.length >= 500) {
prevMessages.pop(); // Remove the oldest message
}
return [
...prevMessages,
{ peermessage: JSON.parse(message.text + ""), from: peerId },
];
});
});
rtmChannelResponse.on(
"ChannelMessage",
function (message, memberId) {
setRtmMessages((prevMessages: any[]) => {
// Check if the messages array exceeds the maximum limit
if (prevMessages.length >= 500) {
prevMessages.pop(); // Remove the oldest message
}
return [
...prevMessages,
{
channelmessage: JSON.parse(message.text + ""),
from: memberId,
},
];
});
dispatch(
changeChildAction(
"messages",
getData(rtmMessages).data,
false
)
);
}
);
}
}, [rtmChannelResponse]);
useEffect(() => {
if (client) {
//Enable Agora to send audio bytes
client.enableAudioVolumeIndicator();
//user activity listeners
client.on("user-joined", (user: IAgoraRTCRemoteUser) => {
setUserJoined(user);
});
client.on("user-left", (user: IAgoraRTCRemoteUser, reason: any) => {
setUserLeft(user);
});
//listen to user speaking,
client.on("volume-indicator", (volumeInfos: any) => {
if (volumeInfos.length === 0) return;
volumeInfos.map((volumeInfo: any) => {
//when the volume is above 30, user is probably speaking
const speaking = volumeInfo.level >= 30;
if (
volumeInfo.uid === userId &&
props.localUser.value.speaking != speaking
) {
setLocalUserSpeaking(speaking);
} else {
setUpdateVolume({ update: speaking, userid: volumeInfo.uid });
}
});
});
client.on(
"user-published",
async (
user: IAgoraRTCRemoteUser,
mediaType: "video" | "audio"
) => {
setLocalUserVideo(user);
}
);
client.on(
"user-unpublished",
(user: IAgoraRTCRemoteUser, mediaType: "video" | "audio") => {
setLocalUserVideo(user);
}
);
}
}, [client]);
return (
<BackgroundColorContext.Provider value={props.style.background}>
{/* <DrawerWrapper> */}
<Drawer
resizable={resizable}
onResizeStop={onResizeStop}
rootStyle={
props.visible.value
? { overflow: "auto", pointerEvents: "auto" }
: {}
}
styles={{
wrapper: {
maxHeight: "100%",
maxWidth: "100%",
},
body: {
padding: 0,
backgroundColor: props.style.background,
},
}}
closable={false}
placement={props.placement}
open={props.visible.value}
getContainer={() =>
document.querySelector(`#${CanvasContainerID}`) || document.body
}
footer={null}
width={transToPxSize(props.width || DEFAULT_SIZE)}
height={
!props.autoHeight
? transToPxSize(props.height || DEFAULT_SIZE)
: ""
}
onClose={(e: any) => {
props.visible.onChange(false);
}}
afterOpenChange={(visible: any) => {
if (!visible) {
props.onEvent("close");
}
}}
zIndex={Layers.drawer}
maskClosable={props.maskClosable}
mask={props.showMask}
>
{/* <ButtonStyle
onClick={() => {
props.visible.onChange(false);
}}
>
<CloseOutlined />
</ButtonStyle> */}
<InnerGrid
{...otherContainerProps}
items={gridItemCompToGridItems(items)}
autoHeight={props.autoHeight}
minHeight={isTopBom ? DEFAULT_SIZE + "px" : "100%"}
style={{ height: "100%" }}
containerPadding={[DEFAULT_PADDING, DEFAULT_PADDING]}
hintPlaceholder={HintPlaceHolder}
bgColor={props.style.background}
/>
</Drawer>
{/* </DrawerWrapper> */}
</BackgroundColorContext.Provider>
);
}
)
.setPropertyViewFn((children: any) => (
<>
{/* {(EditorContext.editorModeStatus === "logic" ||
EditorContext.editorModeStatus === "both") && (
<> */}
<Section name={sectionNames.meetings}>
{children.appId.propertyView({
label: trans("meeting.appid"),
})}
{children.meetingName.propertyView({
label: trans("meeting.meetingName"),
})}
{children.localUserID.propertyView({
label: trans("meeting.localUserID"),
})}
{children.rtmToken.propertyView({
label: trans("meeting.rtmToken"),
})}
{children.rtcToken.propertyView({
label: trans("meeting.rtcToken"),
})}
</Section>
<Section name={sectionNames.interaction}>
{children.onEvent.getPropertyView()}
{children.onMeetingEvent.getPropertyView()}
</Section>
{/* </>
)} */}
{/* {(EditorContext.editorModeStatus === "layout" ||
EditorContext.editorModeStatus === "both") && (
<> */}
{/* <Section name={sectionNames.layout}>
{children.placement.propertyView({
label: trans("meeting.placement"),
radioButton: true,
})}
{["top", "bottom"].includes(children.placement.getView())
? children.autoHeight.getPropertyView()
: children.width.propertyView({
label: trans("meeting.width"),
tooltip: trans("meeting.widthTooltip"),
placeholder: DEFAULT_SIZE + "",
})}
{!children.autoHeight.getView() &&
["top", "bottom"].includes(children.placement.getView()) &&
children.height.propertyView({
label: trans("meeting.height"),
tooltip: trans("meeting.heightTooltip"),
placeholder: DEFAULT_SIZE + "",
})}
{children.maskClosable.propertyView({
label: trans("meeting.maskClosable"),
})}
{children.showMask.propertyView({
label: trans("meeting.showMask"),
})}
</Section>
<Section name={sectionNames.style}>
{children.style.getPropertyView()}
</Section> */}
{/* </> */}
{/* )} */}
</>
))
.build();
})();
MTComp = class extends MTComp {
autoHeight(): boolean {
return false;
}
};
MTComp = withMethodExposing(MTComp, [
{
method: {
name: "openDrawer",
params: [],
},
execute: (comp: any, values: any) => {
comp.children.visible.getView().onChange(true);
},
},
{
method: {
name: "startSharing",
params: [],
},
execute: async (comp: any, values: any) => {
if (!comp.children.meetingActive.getView().value) return;
let sharing = !comp.children.sharing.getView().value;
await shareScreen(sharing);
comp.children.sharing.change(sharing);
},
},
{
method: {
name: "audioControl",
description: trans("meeting.actionBtnDesc"),
params: [],
},
execute: async (comp: any, values: any) => {
if (!comp.children.meetingActive.getView().value) return;
let value = !comp.children.audioControl.getView().value;
comp.children.localUser.change({
user: userId + "",
audiostatus: value,
streamingVideo: comp.children.videoControl.getView().value,
speaking: false,
});
await turnOnMicrophone(value);
comp.children.audioControl.change(value);
},
},
{
method: {
name: "videoControl",
description: trans("meeting.actionBtnDesc"),
params: [],
},
execute: async (comp: any, values: any) => {
//check if meeting is active
if (!comp.children.meetingActive.getView().value) return;
//toggle videoControl
let value = !comp.children.videoControl.getView().value;
if (videoTrack) {
videoTrack.setEnabled(value);
} else {
await turnOnCamera(value);
}
//change my local user data
let localData = {
user: userId + "",
streamingVideo: value,
audiostatus: comp.children.audioControl.getView().value,
speaking: comp.children.localUser.getView().value.speaking,
};
comp.children.localUser.change(localData);
comp.children.videoControl.change(value);
},
},
{
method: {
name: "startMeeting",
description: trans("meeting.actionBtnDesc"),
params: [],
},
execute: async (comp: any, values: any) => {
/* console.log("startMeeting ", {
// user: userId + "",
audiostatus: false,
speaking: false,
streamingVideo: true,
}); */
if (comp.children.meetingActive.getView().value) return;
userId =
comp.children.localUserID.getView().value === ""
? uuidv4()
: comp.children.localUserID.getView().value;
comp.children.localUser.change({
user: userId + "",
audiostatus: false,
speaking: false,
streamingVideo: true,
});
/* console.log("startMeeting localUser ", {
user: userId + "",
audiostatus: false,
speaking: false,
streamingVideo: true,
}); */
comp.children.localUser.children.value.dispatch(
changeChildAction(
"localUser",
{
user: userId + "",
audiostatus: false,
speaking: false,
streamingVideo: true,
},
false
)
);
comp.children.videoControl.change(true);
await publishVideo(
comp.children.appId.getView(),
comp.children.meetingName.getView().value === ""
? uuidv4()
: comp.children.meetingName.getView().value,
comp.children.rtmToken.getView().value,
comp.children.rtcToken.getView().value
);
comp.children.meetingActive.change(true);
},
},
{
method: {
name: "broadCast",
description: trans("meeting.broadCast"),
params: [],
},
execute: async (comp: any, values: any) => {
if (!comp.children.meetingActive.getView().value) return;
let messagedata =
values !== undefined && values[0] !== undefined ? values[0] : "";
let toUsers: any =
values !== undefined && values[1] !== undefined ? values[1] : "";
let message: any = {
time: Date.now(),
message: messagedata,
};
if (toUsers.length > 0 && toUsers[0] !== undefined) {
toUsers.forEach((peer: any) => {
message.to = peer;
sendPeerMessageRtm(message, String(peer));
});
} else {
sendMessageRtm(message);
}
},
},
{
method: {
name: "setMeetingName",
description: trans("meeting.meetingName"),
params: [],
},
execute: async (comp: any, values: any) => {
let meetingName: any = values[0];
comp.children.meetingName.change(meetingName);
},
},
{
method: {
name: "setUserName",
description: trans("meeting.userName"),
params: [],
},
execute: async (comp: any, values: any) => {
let userName: any = values[0];
let userLocal = comp.children.localUser.getView().value;
comp.children.localUser.change({ ...userLocal, userName: userName });
},
},
{
method: {
name: "setRTCToken",
description: trans("meeting.rtcToken"),
params: [],
},
execute: async (comp: any, values: any) => {
let rtcToken: any = values[0];
comp.children.rtcToken.change(rtcToken);
},
},
{
method: {
name: "setRTMToken",
description: trans("meeting.rtmToken"),
params: [],
},
execute: async (comp: any, values: any) => {
let rtmToken: any = values[0];
comp.children.rtmToken.change(rtmToken);
},
},
{
method: {
name: "endMeeting",
description: trans("meeting.actionBtnDesc"),
params: [],
},
execute: async (comp: any, values: any) => {
if (!comp.children.meetingActive.getView().value) return;
let value = !comp.children.endCall.getView().value;
comp.children.endCall.change(value);
comp.children.meetingActive.change(false);
await leaveChannel();
comp.children.localUser.change({
user: userId + "",
streamingVideo: false,
});
},
},
]);
MeetingControllerComp = withExposingConfigs(MTComp, [
new NameConfig("appId", trans("meeting.appid")),
new NameConfig("localUser", trans("meeting.host")),
new NameConfig("participants", trans("meeting.participants")),
new NameConfig("meetingActive", trans("meeting.meetingActive")),
new NameConfig("meetingName", trans("meeting.meetingName")),
new NameConfig("localUserID", trans("meeting.localUserID")),
new NameConfig("messages", trans("meeting.messages")),
new NameConfig("rtmToken", trans("meeting.rtmToken")),
new NameConfig("rtcToken", trans("meeting.rtcToken")),
]);
} else {
console.error(
"ContainerCompBuilder for Meeting Comp is not available. Please ensure that Lowcoder SDK version v2.4 or higher is installed."
);
}
export { MeetingControllerComp };

View File

@@ -0,0 +1,252 @@
import {
NameConfig,
withDefault,
withExposingConfigs,
StringControl,
Section,
sectionNames,
AutoHeightControl,
EditorContext,
styled,
MeetingEventHandlerControl,
BoolCodeControl,
RefControl,
stringExposingStateControl,
StringStateControl,
UICompBuilder,
CommonNameConfig,
} from "lowcoder-sdk";
import { ButtonStyleControl } from "./videobuttonCompConstants";
import { trans } from "../../i18n/comps";
import { client } from "./meetingControllerComp";
import type { IAgoraRTCRemoteUser } from "agora-rtc-sdk-ng";
import { useEffect, useRef, useState } from "react";
import { useResizeDetector } from "react-resize-detector";
const VideoContainer = styled.video`
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: space-around;
`;
const meetingStreamChildren = {
autoHeight: withDefault(AutoHeightControl, "auto"),
profilePadding: withDefault(StringControl, "0px"),
profileBorderRadius: withDefault(StringControl, "0px"),
videoAspectRatio: withDefault(StringControl, "1 / 1"),
onEvent: MeetingEventHandlerControl,
disabled: BoolCodeControl,
loading: BoolCodeControl,
style: ButtonStyleControl,
viewRef: RefControl,
userId: withDefault(stringExposingStateControl(""), "{{meeting1.localUser}}"),
profileImageUrl: withDefault(
StringStateControl,
"https://api.dicebear.com/7.x/fun-emoji/svg?seed=Peanut&radius=50&backgroundColor=transparent&randomizeIds=true&eyes=wink,sleepClose"
),
noVideoText: stringExposingStateControl(trans("meeting.noVideo")),
};
let VideoCompBuilder = (function () {
return new UICompBuilder(meetingStreamChildren, (props: any) => {
const videoRef = useRef<HTMLVideoElement>(null);
const conRef = useRef<HTMLDivElement>(null);
const [userId, setUserId] = useState();
const [userName, setUsername] = useState("");
const [showVideo, setVideo] = useState(true);
useEffect(() => {
if (props.userId.value !== "") {
let userData = JSON.parse(props.userId?.value);
client.on(
"user-published",
async (user: IAgoraRTCRemoteUser, mediaType: "video" | "audio") => {
if (mediaType === "video") {
const remoteTrack = await client.subscribe(user, mediaType);
let userId = user.uid + "";
if (
user.hasVideo &&
user.uid + "" !== userData.user &&
userData.user !== ""
) {
props.onEvent("videoOn");
}
const element = document.getElementById(userId);
if (element) {
remoteTrack.play(userId);
}
}
if (mediaType === "audio") {
const remoteTrack = await client.subscribe(user, mediaType);
if (
user.hasAudio &&
user.uid + "" !== userData.user &&
userData.user !== ""
) {
userData.audiostatus = user.hasVideo;
props.onEvent("audioUnmuted");
}
remoteTrack.play();
}
}
);
client.on(
"user-unpublished",
(user: IAgoraRTCRemoteUser, mediaType: "video" | "audio") => {
// console.log("user-unpublished");
if (mediaType === "audio") {
if (
!user.hasAudio &&
user.uid + "" !== userData.user &&
userData.user !== ""
) {
userData.audiostatus = user.hasVideo;
props.onEvent("audioMuted");
}
}
if (mediaType === "video") {
if (videoRef.current && videoRef.current?.id === user.uid + "") {
videoRef.current.srcObject = null;
}
if (
!user.hasVideo &&
user.uid + "" !== userData.user &&
userData.user !== ""
) {
props.onEvent("videoOff");
}
}
}
);
setUserId(userData.user);
setUsername(userData.userName);
setVideo(userData.streamingVideo);
}
}, [props.userId.value]);
// console.log("userId", userId);
useResizeDetector({
targetRef: conRef,
});
return (
<EditorContext.Consumer>
{(editorState: any) => (
<div
ref={conRef}
style={{
display: "flex",
alignItems: "center",
height: "100%",
overflow: "hidden",
borderRadius: props.style.radius,
aspectRatio: props.videoAspectRatio,
backgroundColor: props.style.background,
padding: props.style.padding,
margin: props.style.margin,
}}
>
{userId ? (
<VideoContainer
onClick={() => props.onEvent("videoClicked")}
ref={videoRef}
style={{
display: `${showVideo ? "flex" : "none"}`,
aspectRatio: props.videoAspectRatio,
borderRadius: props.style.radius,
width: "auto",
}}
id={userId}
></VideoContainer>
) : (
<></>
)}
<div
style={{
flexDirection: "column",
alignItems: "center",
display: `${!showVideo || userId ? "flex" : "none"}`,
margin: "0 auto",
padding: props.profilePadding,
}}
>
<img
alt=""
style={{
borderRadius: props.profileBorderRadius,
width: "100%",
overflow: "hidden",
}}
src={props.profileImageUrl.value}
/>
<p style={{ margin: "0" }}>{userName ?? ""}</p>
</div>
</div>
)}
</EditorContext.Consumer>
);
})
.setPropertyViewFn((children: any) => (
<>
<Section name={sectionNames.basic}>
{children.userId.propertyView({ label: trans("meeting.videoId") })}
{children.profileImageUrl.propertyView({
label: trans("meeting.profileImageUrl"),
placeholder:
"https://api.dicebear.com/7.x/fun-emoji/svg?seed=Peanut&radius=50&backgroundColor=transparent&randomizeIds=true&eyes=wink,sleepClose",
})}
</Section>
{/* {(useContext(EditorContext).editorModeStatus === "logic" ||
useContext(EditorContext).editorModeStatus === "both") && (
<Section name={sectionNames.interaction}>
{children.onEvent.getPropertyView()}
{hiddenPropertyView(children)}
</Section>
)}
{(useContext(EditorContext).editorModeStatus === "layout" ||
useContext(EditorContext).editorModeStatus === "both") && (
<> */}
<Section name={sectionNames.layout}>
{children.autoHeight.getPropertyView()}
</Section>
<Section name={sectionNames.style}>
{children.profilePadding.propertyView({
label: "Profile Image Padding",
})}
{children.profileBorderRadius.propertyView({
label: "Profile Image Border Radius",
})}
{children.videoAspectRatio.propertyView({
label: "Video Aspect Ratio",
})}
{children.style.getPropertyView()}
</Section>
{/* </> */}
{/* )} */}
</>
))
.build();
})();
VideoCompBuilder = class extends VideoCompBuilder {
autoHeight(): boolean {
return this.children.autoHeight.getView();
}
};
export const VideoMeetingStreamComp = withExposingConfigs(VideoCompBuilder, [
new NameConfig("loading", trans("meeting.loadingDesc")),
new NameConfig("profileImageUrl", trans("meeting.profileImageUrl")),
...CommonNameConfig,
]);

View File

@@ -0,0 +1,235 @@
import {
NameConfig,
withDefault,
withExposingConfigs,
StringControl,
Section,
sectionNames,
AutoHeightControl,
EditorContext,
styled,
MeetingEventHandlerControl,
BoolCodeControl,
RefControl,
stringExposingStateControl,
UICompBuilder,
CommonNameConfig,
} from "lowcoder-sdk";
import { useEffect, useRef, useState } from "react";
import { client } from "./meetingControllerComp";
import type { IAgoraRTCRemoteUser } from "agora-rtc-sdk-ng";
import { trans } from "../../i18n/comps";
import { useResizeDetector } from "react-resize-detector";
import { ButtonStyleControl } from "./videobuttonCompConstants";
const VideoContainer = styled.video`
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: space-around;
`;
const sharingStreamChildren = {
autoHeight: withDefault(AutoHeightControl, "fixed"),
profilePadding: withDefault(StringControl, "0px"),
profileBorderRadius: withDefault(StringControl, "0px"),
videoAspectRatio: withDefault(StringControl, ""),
onEvent: MeetingEventHandlerControl,
disabled: BoolCodeControl,
loading: BoolCodeControl,
style: ButtonStyleControl,
viewRef: RefControl,
userId: withDefault(stringExposingStateControl(""), "{{meeting1.localUser}}"),
noVideoText: stringExposingStateControl(trans("meeting.noVideo")),
};
let SharingCompBuilder = (function () {
return new UICompBuilder(sharingStreamChildren, (props: any) => {
const videoRef = useRef<HTMLVideoElement>(null);
const conRef = useRef<HTMLDivElement>(null);
const [userId, setUserId] = useState();
const [userName, setUsername] = useState("");
const [showVideoSharing, setVideoSharing] = useState(true);
useEffect(() => {
if (props.userId.value !== "") {
let userData = JSON.parse(props.userId?.value);
client.on(
"user-published",
async (user: IAgoraRTCRemoteUser, mediaType: "video" | "audio") => {
if (mediaType === "video") {
const remoteTrack = await client.subscribe(user, mediaType);
let userId = user.uid + "";
if (
user.hasVideo &&
user.uid + "" !== userData.user &&
userData.user !== ""
) {
props.onEvent("videoOn");
}
const element = document.getElementById(userId);
if (element) {
remoteTrack.play(userId);
}
}
if (mediaType === "audio") {
const remoteTrack = await client.subscribe(user, mediaType);
if (
user.hasAudio &&
user.uid + "" !== userData.user &&
userData.user !== ""
) {
userData.audiostatus = user.hasVideo;
props.onEvent("audioUnmuted");
}
remoteTrack.play();
}
}
);
client.on(
"user-unpublished",
(user: IAgoraRTCRemoteUser, mediaType: "video" | "audio") => {
if (mediaType === "audio") {
if (
!user.hasAudio &&
user.uid + "" !== userData.user &&
userData.user !== ""
) {
userData.audiostatus = user.hasVideo;
props.onEvent("audioMuted");
}
}
if (mediaType === "video") {
if (videoRef.current && videoRef.current?.id === user.uid + "") {
videoRef.current.srcObject = null;
}
if (
!user.hasVideo &&
user.uid + "" !== userData.user &&
userData.user !== ""
) {
props.onEvent("videoOff");
}
}
}
);
setUserId(userData.user);
setUsername(userData.userName);
setVideoSharing(userData.streamingSharing);
}
}, [props.userId.value]);
useResizeDetector({
targetRef: conRef,
});
return (
<EditorContext.Consumer>
{(editorState: any) => (
<div
ref={conRef}
style={{
display: "flex",
alignItems: "center",
height: "100%",
overflow: "hidden",
borderRadius: props?.style?.radius,
aspectRatio: props?.videoAspectRatio,
backgroundColor: props.style?.background,
padding: props.style?.padding,
margin: props.style?.margin,
}}
>
{userId ? (
<VideoContainer
onClick={() => props.onEvent("videoClicked")}
ref={videoRef}
style={{
display: `${showVideoSharing ? "flex" : "none"}`,
aspectRatio: props.videoAspectRatio,
borderRadius: props.style.radius,
width: "auto",
}}
id="share-screen"
></VideoContainer>
) : (
<></>
)}
<div
style={{
flexDirection: "column",
alignItems: "center",
display: `${!showVideoSharing || userId ? "flex" : "none"}`,
margin: "0 auto",
padding: props.profilePadding,
}}
>
<img
alt=""
style={{
borderRadius: props.profileBorderRadius,
width: "100%",
overflow: "hidden",
}}
src={props.profileImageUrl?.value}
/>
<p style={{ margin: "0" }}>{userName ?? ""}</p>
</div>
</div>
)}
</EditorContext.Consumer>
);
})
.setPropertyViewFn((children: any) => (
<>
<Section name={sectionNames.basic}>
{children.userId.propertyView({ label: trans("meeting.videoId") })}
</Section>
{/* {(useContext(EditorContext).editorModeStatus === "logic" ||
useContext(EditorContext).editorModeStatus === "both") && (
<Section name={sectionNames.interaction}>
{children.onEvent.getPropertyView()}
{hiddenPropertyView(children)}
</Section>
)} */}
{/* {(useContext(EditorContext).editorModeStatus === "layout" ||
useContext(EditorContext).editorModeStatus === "both") && (
<> */}
<Section name={sectionNames.layout}>
{children.autoHeight.getPropertyView()}
</Section>
<Section name={sectionNames.style}>
{children.profilePadding.propertyView({
label: "Profile Image Padding",
})}
{children.profileBorderRadius.propertyView({
label: "Profile Image Border Radius",
})}
{children.videoAspectRatio.propertyView({
label: "Video Aspect Ratio",
})}
{children.style?.getPropertyView()}
</Section>
{/* </> */}
{/* )} */}
</>
))
.build();
})();
SharingCompBuilder = class extends SharingCompBuilder {
autoHeight(): boolean {
return this.children.autoHeight.getView();
}
};
export const VideoSharingStreamComp = withExposingConfigs(SharingCompBuilder, [
new NameConfig("loading", trans("meeting.loadingDesc")),
...CommonNameConfig,
]);

View File

@@ -0,0 +1,111 @@
import { default as Button } from "antd/es/button";
import {
styleControl,
ButtonStyleType,
ButtonStyle,
migrateOldData,
refMethods,
blurMethod,
clickMethod,
focusWithOptions,
genActiveColor,
genHoverColor,
} from "lowcoder-sdk";
import styled, { css } from "styled-components";
// import { genActiveColor, genHoverColor } from "lowcoder-design";
export function getButtonStyle(buttonStyle: any) {
const hoverColor = genHoverColor(buttonStyle.background);
const activeColor = genActiveColor(buttonStyle.background);
return css`
&&& {
border-radius: ${buttonStyle.radius};
margin: ${buttonStyle.margin};
padding: ${buttonStyle.padding};
&:not(:disabled) {
// click animation color
--antd-wave-shadow-color: ${buttonStyle.border};
border-color: ${buttonStyle.border};
color: ${buttonStyle.text};
background-color: ${buttonStyle.background};
border-radius: ${buttonStyle.radius};
margin: ${buttonStyle.margin};
padding: ${buttonStyle.padding};
&:hover,
&:focus {
color: ${buttonStyle.text};
background-color: ${hoverColor};
border-color: ${buttonStyle.border === buttonStyle.background
? hoverColor
: buttonStyle.border};
}
&:active {
color: ${buttonStyle.text};
background-color: ${activeColor};
border-color: ${buttonStyle.border === buttonStyle.background
? activeColor
: buttonStyle.border};
}
}
}
`;
}
export const Button100 = styled(Button)<{ $buttonStyle?: any }>`
${(props) => props.$buttonStyle && getButtonStyle(props.$buttonStyle)}
width: 100%;
height: auto;
display: inline-flex;
justify-content: center;
align-items: center;
overflow: hidden;
span {
overflow: hidden;
text-overflow: ellipsis;
}
gap: 6px;
`;
export const ButtonCompWrapper = styled.div<{ disabled: boolean }>`
// The button component is disabled but can respond to drag & select events
${(props) =>
props.disabled &&
`
cursor: not-allowed;
button:disabled {
pointer-events: none;
}
`};
`;
/**
* Compatible with old data 2022-08-05
*/
function fixOldData(oldData: any) {
if (
oldData &&
(oldData.hasOwnProperty("backgroundColor") ||
oldData.hasOwnProperty("borderColor") ||
oldData.hasOwnProperty("color"))
) {
return {
background: oldData.backgroundColor,
border: oldData.borderColor,
text: oldData.color,
};
}
return oldData;
}
const ButtonTmpStyleControl = styleControl(ButtonStyle);
export const ButtonStyleControl = migrateOldData(
ButtonTmpStyleControl,
fixOldData
);
export const buttonRefMethods = refMethods([
focusWithOptions,
blurMethod,
clickMethod,
]);

View File

@@ -0,0 +1,338 @@
import {
changeChildAction,
changeValueAction,
CompAction,
CompActionTypes,
wrapChildAction,
} from "lowcoder-core";
import { AxisFormatterComp, EchartsAxisType } from "../basicChartComp/chartConfigs/cartesianAxisConfig";
import { barChartChildrenMap, ChartSize, getDataKeys } from "./barChartConstants";
import { barChartPropertyView } from "./barChartPropertyView";
import _ from "lodash";
import { useContext, useEffect, useMemo, useRef, useState } from "react";
import { useResizeDetector } from "react-resize-detector";
import ReactECharts from "../basicChartComp/reactEcharts";
import {
childrenToProps,
depsConfig,
genRandomKey,
NameConfig,
UICompBuilder,
withDefault,
withExposingConfigs,
withViewFn,
ThemeContext,
chartColorPalette,
getPromiseAfterDispatch,
dropdownControl,
JSONObject,
} from "lowcoder-sdk";
import { getEchartsLocale, trans } from "i18n/comps";
import { ItemColorComp } from "comps/basicChartComp/chartConfigs/lineChartConfig";
import {
echartsConfigOmitChildren,
getEchartsConfig,
getSelectedPoints,
} from "./barChartUtils";
import 'echarts-extension-gmap';
import log from "loglevel";
let clickEventCallback = () => {};
const chartModeOptions = [
{
label: "ECharts JSON",
value: "json",
}
] as const;
let BarChartTmpComp = (function () {
return new UICompBuilder({mode:dropdownControl(chartModeOptions,'ui'),...barChartChildrenMap}, () => null)
.setPropertyViewFn(barChartPropertyView)
.build();
})();
BarChartTmpComp = withViewFn(BarChartTmpComp, (comp) => {
const mode = comp.children.mode.getView();
const onUIEvent = comp.children.onUIEvent.getView();
const onEvent = comp.children.onEvent.getView();
const echartsCompRef = useRef<ReactECharts | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [chartSize, setChartSize] = useState<ChartSize>();
const firstResize = useRef(true);
const theme = useContext(ThemeContext);
const [chartKey, setChartKey] = useState(0);
const prevRaceMode = useRef<boolean>();
const defaultChartTheme = {
color: chartColorPalette,
backgroundColor: "#fff",
};
let themeConfig = defaultChartTheme;
try {
themeConfig = theme?.theme.chart ? JSON.parse(theme?.theme.chart) : defaultChartTheme;
} catch (error) {
log.error('theme chart error: ', error);
}
// Detect race mode changes and force chart recreation
const currentRaceMode = comp.children.chartConfig?.children?.comp?.children?.race?.getView();
useEffect(() => {
if (prevRaceMode.current !== undefined && prevRaceMode.current !== currentRaceMode) {
// Force chart recreation when race mode changes
setChartKey(prev => prev + 1);
}
prevRaceMode.current = currentRaceMode;
}, [currentRaceMode]);
const triggerClickEvent = async (dispatch: any, action: CompAction<JSONValue>) => {
await getPromiseAfterDispatch(
dispatch,
action,
{ autoHandleAfterReduce: true }
);
onEvent('click');
}
useEffect(() => {
const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance();
if (!echartsCompInstance) {
return _.noop;
}
echartsCompInstance?.on("click", (param: any) => {
document.dispatchEvent(new CustomEvent("clickEvent", {
bubbles: true,
detail: {
action: 'click',
data: param.data,
}
}));
triggerClickEvent(
comp.dispatch,
changeChildAction("lastInteractionData", param.data, false)
);
});
return () => {
echartsCompInstance?.off("click");
document.removeEventListener('clickEvent', clickEventCallback)
};
}, []);
useEffect(() => {
// bind events
const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance();
if (!echartsCompInstance) {
return _.noop;
}
echartsCompInstance?.on("selectchanged", (param: any) => {
const option: any = echartsCompInstance?.getOption();
document.dispatchEvent(new CustomEvent("clickEvent", {
bubbles: true,
detail: {
action: param.fromAction,
data: getSelectedPoints(param, option)
}
}));
if (param.fromAction === "select") {
comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false));
onUIEvent("select");
} else if (param.fromAction === "unselect") {
comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false));
onUIEvent("unselect");
}
triggerClickEvent(
comp.dispatch,
changeChildAction("lastInteractionData", getSelectedPoints(param, option), false)
);
});
// unbind
return () => {
echartsCompInstance?.off("selectchanged");
document.removeEventListener('clickEvent', clickEventCallback)
};
}, [onUIEvent]);
const echartsConfigChildren = _.omit(comp.children, echartsConfigOmitChildren);
const childrenProps = childrenToProps(echartsConfigChildren);
const option = useMemo(() => {
return getEchartsConfig(
childrenProps as ToViewReturn<typeof echartsConfigChildren>,
chartSize,
themeConfig
);
}, [theme, childrenProps, chartSize, ...Object.values(echartsConfigChildren)]);
useEffect(() => {
comp.children.mapInstance.dispatch(changeValueAction(null, false))
if(comp.children.mapInstance.value) return;
}, [option])
useResizeDetector({
targetRef: containerRef,
onResize: ({width, height}) => {
console.log('barChart - resize');
if (width && height) {
setChartSize({ w: width, h: height });
}
if (!firstResize.current) {
// ignore the first resize, which will impact the loading animation
echartsCompRef.current?.getEchartsInstance().resize();
} else {
firstResize.current = false;
}
}
})
return (
<div ref={containerRef} style={{height: '100%'}}>
<ReactECharts
key={chartKey}
ref={(e) => (echartsCompRef.current = e)}
style={{ height: "100%" }}
notMerge={!currentRaceMode}
lazyUpdate={!currentRaceMode}
opts={{ locale: getEchartsLocale() }}
option={option}
mode={mode}
/>
</div>
);
});
function getYAxisFormatContextValue(
data: Array<JSONObject>,
yAxisType: EchartsAxisType,
yAxisName?: string
) {
const dataSample = yAxisName && data.length > 0 && data[0][yAxisName];
let contextValue = dataSample;
if (yAxisType === "time") {
// to timestamp
const time =
typeof dataSample === "number" || typeof dataSample === "string"
? new Date(dataSample).getTime()
: null;
if (time) contextValue = time;
}
return contextValue;
}
BarChartTmpComp = class extends BarChartTmpComp {
private lastYAxisFormatContextVal?: JSONValue;
private lastColorContext?: JSONObject;
updateContext(comp: this) {
// the context value of axis format
let resultComp = comp;
const data = comp.children.data.getView();
const sampleSeries = comp.children.series.getView().find((s) => !s.getView().hide);
const yAxisContextValue = getYAxisFormatContextValue(
data,
comp.children.yConfig.children.yAxisType.getView(),
sampleSeries?.children.columnName.getView()
);
if (yAxisContextValue !== comp.lastYAxisFormatContextVal) {
comp.lastYAxisFormatContextVal = yAxisContextValue;
resultComp = comp.setChild(
"yConfig",
comp.children.yConfig.reduce(
wrapChildAction(
"formatter",
AxisFormatterComp.changeContextDataAction({ value: yAxisContextValue })
)
)
);
}
// item color context
const colorContextVal = {
seriesName: sampleSeries?.children.seriesName.getView(),
value: yAxisContextValue,
};
if (
comp.children.chartConfig.children.comp.children.hasOwnProperty("itemColor") &&
!_.isEqual(colorContextVal, comp.lastColorContext)
) {
comp.lastColorContext = colorContextVal;
resultComp = resultComp.setChild(
"chartConfig",
comp.children.chartConfig.reduce(
wrapChildAction(
"comp",
wrapChildAction("itemColor", ItemColorComp.changeContextDataAction(colorContextVal))
)
)
);
}
return resultComp;
}
override reduce(action: CompAction): this {
const comp = super.reduce(action);
if (action.type === CompActionTypes.UPDATE_NODES_V2) {
const newData = comp.children.data.getView();
// data changes
if (comp.children.data !== this.children.data) {
setTimeout(() => {
// update x-axis value
const keys = getDataKeys(newData);
if (keys.length > 0 && !keys.includes(comp.children.xAxisKey.getView())) {
comp.children.xAxisKey.dispatch(changeValueAction(keys[0] || ""));
}
// pass to child series comp
comp.children.series.dispatchDataChanged(newData);
}, 0);
}
return this.updateContext(comp);
}
return comp;
}
override autoHeight(): boolean {
return false;
}
};
let BarChartComp = withExposingConfigs(BarChartTmpComp, [
depsConfig({
name: "selectedPoints",
desc: trans("chart.selectedPointsDesc"),
depKeys: ["selectedPoints"],
func: (input) => {
return input.selectedPoints;
},
}),
depsConfig({
name: "lastInteractionData",
desc: trans("chart.lastInteractionDataDesc"),
depKeys: ["lastInteractionData"],
func: (input) => {
return input.lastInteractionData;
},
}),
depsConfig({
name: "data",
desc: trans("chart.dataDesc"),
depKeys: ["data", "mode"],
func: (input) =>[] ,
}),
new NameConfig("title", trans("chart.titleDesc")),
]);
export const BarChartCompWithDefault = withDefault(BarChartComp, {
xAxisKey: "month",
series: [
{
dataIndex: genRandomKey(),
seriesName: "Sales",
columnName: "sales",
},
{
dataIndex: genRandomKey(),
seriesName: "Target",
columnName: "target",
},
],
});

View File

@@ -0,0 +1,357 @@
import {
jsonControl,
JSONObject,
stateComp,
toJSONObjectArray,
toObject,
BoolControl,
withDefault,
StringControl,
NumberControl,
FunctionControl,
dropdownControl,
eventHandlerControl,
valueComp,
withType,
uiChildren,
clickEvent,
styleControl,
EchartDefaultTextStyle,
EchartDefaultChartStyle,
toArray
} from "lowcoder-sdk";
import { RecordConstructorToComp, RecordConstructorToView } from "lowcoder-core";
import { BarChartConfig } from "../basicChartComp/chartConfigs/barChartConfig";
import { XAxisConfig, YAxisConfig } from "../basicChartComp/chartConfigs/cartesianAxisConfig";
import { LegendConfig } from "../basicChartComp/chartConfigs/legendConfig";
import { EchartsLegendConfig } from "../basicChartComp/chartConfigs/echartsLegendConfig";
import { EchartsLabelConfig } from "../basicChartComp/chartConfigs/echartsLabelConfig";
import { LineChartConfig } from "../basicChartComp/chartConfigs/lineChartConfig";
import { PieChartConfig } from "../basicChartComp/chartConfigs/pieChartConfig";
import { ScatterChartConfig } from "../basicChartComp/chartConfigs/scatterChartConfig";
import { SeriesListComp } from "./seriesComp";
import { EChartsOption } from "echarts";
import { i18nObjs, trans } from "i18n/comps";
import { GaugeChartConfig } from "../basicChartComp/chartConfigs/gaugeChartConfig";
import { FunnelChartConfig } from "../basicChartComp/chartConfigs/funnelChartConfig";
import {EchartsTitleVerticalConfig} from "../chartComp/chartConfigs/echartsTitleVerticalConfig";
import {EchartsTitleConfig} from "../basicChartComp/chartConfigs/echartsTitleConfig";
// Enhanced default data for bar charts
export const barChartDefaultData = [
{
month: "Jan",
sales: 1200,
target: 1000
},
{
month: "Feb",
sales: 1500,
target: 1200
},
{
month: "Mar",
sales: 1300,
target: 1400
},
{
month: "Apr",
sales: 1800,
target: 1500
},
{
month: "May",
sales: 1600,
target: 1700
},
{
month: "Jun",
sales: 2100,
target: 1900
}
];
export const ChartTypeOptions = [
{
label: trans("chart.bar"),
value: "bar",
},
{
label: trans("chart.line"),
value: "line",
},
{
label: trans("chart.scatter"),
value: "scatter",
},
{
label: trans("chart.pie"),
value: "pie",
},
] as const;
export const UIEventOptions = [
{
label: trans("chart.select"),
value: "select",
description: trans("chart.selectDesc"),
},
{
label: trans("chart.unSelect"),
value: "unselect",
description: trans("chart.unselectDesc"),
},
] as const;
export const MapEventOptions = [
{
label: trans("chart.mapReady"),
value: "mapReady",
description: trans("chart.mapReadyDesc"),
},
{
label: trans("chart.zoomLevelChange"),
value: "zoomLevelChange",
description: trans("chart.zoomLevelChangeDesc"),
},
{
label: trans("chart.centerPositionChange"),
value: "centerPositionChange",
description: trans("chart.centerPositionChangeDesc"),
},
] as const;
export const XAxisDirectionOptions = [
{
label: trans("chart.horizontal"),
value: "horizontal",
},
{
label: trans("chart.vertical"),
value: "vertical",
},
] as const;
export type XAxisDirectionType = ValueFromOption<typeof XAxisDirectionOptions>;
export const noDataAxisConfig = {
animation: false,
xAxis: {
type: "category",
name: trans("chart.noData"),
nameLocation: "middle",
data: [],
axisLine: {
lineStyle: {
color: "#8B8FA3",
},
},
},
yAxis: {
type: "value",
axisLabel: {
color: "#8B8FA3",
},
splitLine: {
lineStyle: {
color: "#F0F0F0",
},
},
},
tooltip: {
show: false,
},
series: [
{
data: [700],
type: "line",
itemStyle: {
opacity: 0,
},
},
],
} as EChartsOption;
export const noDataPieChartConfig = {
animation: false,
tooltip: {
show: false,
},
legend: {
formatter: trans("chart.unknown"),
top: "bottom",
selectedMode: false,
},
color: ["#B8BBCC", "#CED0D9", "#DCDEE6", "#E6E6EB"],
series: [
{
type: "pie",
radius: "35%",
center: ["25%", "50%"],
silent: true,
label: {
show: false,
},
data: [
{
name: "1",
value: 70,
},
{
name: "2",
value: 68,
},
{
name: "3",
value: 48,
},
{
name: "4",
value: 40,
},
],
},
{
type: "pie",
radius: "35%",
center: ["75%", "50%"],
silent: true,
label: {
show: false,
},
data: [
{
name: "1",
value: 70,
},
{
name: "2",
value: 68,
},
{
name: "3",
value: 48,
},
{
name: "4",
value: 40,
},
],
},
],
} as EChartsOption;
export type ChartSize = { w: number; h: number };
export const getDataKeys = (data: Array<JSONObject>) => {
if (!data) {
return [];
}
const dataKeys: Array<string> = [];
data.slice(0, 50).forEach((d) => {
Object.keys(d).forEach((key) => {
if (!dataKeys.includes(key)) {
dataKeys.push(key);
}
});
});
return dataKeys;
};
const ChartOptionMap = {
bar: BarChartConfig,
line: LineChartConfig,
pie: PieChartConfig,
scatter: ScatterChartConfig,
};
const EchartsOptionMap = {
funnel: FunnelChartConfig,
gauge: GaugeChartConfig,
};
const ChartOptionComp = withType(ChartOptionMap, "bar");
const EchartsOptionComp = withType(EchartsOptionMap, "funnel");
export type CharOptionCompType = keyof typeof ChartOptionMap;
export const chartUiModeChildren = {
title: withDefault(StringControl, trans("barChart.defaultTitle")),
data: jsonControl(toJSONObjectArray, barChartDefaultData),
xAxisKey: valueComp<string>("month"), // x-axis, key from data
xAxisDirection: dropdownControl(XAxisDirectionOptions, "horizontal"),
xAxisData: jsonControl(toArray, []),
series: SeriesListComp,
xConfig: XAxisConfig,
yConfig: YAxisConfig,
legendConfig: LegendConfig,
chartConfig: ChartOptionComp,
onUIEvent: eventHandlerControl(UIEventOptions),
};
let chartJsonModeChildren: any = {
echartsOption: jsonControl(toObject, i18nObjs.defaultEchartsJsonOption),
echartsTitle: withDefault(StringControl, trans("echarts.defaultTitle")),
echartsLegendConfig: EchartsLegendConfig,
echartsLabelConfig: EchartsLabelConfig,
echartsConfig: EchartsOptionComp,
echartsTitleVerticalConfig: EchartsTitleVerticalConfig,
echartsTitleConfig:EchartsTitleConfig,
left:withDefault(NumberControl,trans('chart.defaultLeft')),
right:withDefault(NumberControl,trans('chart.defaultRight')),
top:withDefault(NumberControl,trans('chart.defaultTop')),
bottom:withDefault(NumberControl,trans('chart.defaultBottom')),
tooltip: withDefault(BoolControl, true),
legendVisibility: withDefault(BoolControl, true),
}
if (EchartDefaultChartStyle && EchartDefaultTextStyle) {
chartJsonModeChildren = {
...chartJsonModeChildren,
chartStyle: styleControl(EchartDefaultChartStyle, 'chartStyle'),
titleStyle: styleControl(EchartDefaultTextStyle, 'titleStyle'),
xAxisStyle: styleControl(EchartDefaultTextStyle, 'xAxis'),
yAxisStyle: styleControl(EchartDefaultTextStyle, 'yAxisStyle'),
legendStyle: styleControl(EchartDefaultTextStyle, 'legendStyle'),
}
}
const chartMapModeChildren = {
mapInstance: stateComp(),
getMapInstance: FunctionControl,
mapApiKey: withDefault(StringControl, ''),
mapZoomLevel: withDefault(NumberControl, 3),
mapCenterLng: withDefault(NumberControl, 15.932644),
mapCenterLat: withDefault(NumberControl, 50.942063),
mapOptions: jsonControl(toObject, i18nObjs.defaultMapJsonOption),
onMapEvent: eventHandlerControl(MapEventOptions),
showCharts: withDefault(BoolControl, true),
}
export type UIChartDataType = {
seriesName: string;
// coordinate chart
x?: any;
y?: any;
// pie or funnel
itemName?: any;
value?: any;
};
export type NonUIChartDataType = {
name: string;
value: any;
}
export const barChartChildrenMap = {
selectedPoints: stateComp<Array<UIChartDataType>>([]),
lastInteractionData: stateComp<Array<UIChartDataType> | NonUIChartDataType>({}),
onEvent: eventHandlerControl([clickEvent] as const),
...chartUiModeChildren,
...chartJsonModeChildren,
...chartMapModeChildren,
};
const chartUiChildrenMap = uiChildren(barChartChildrenMap);
export type ChartCompPropsType = RecordConstructorToView<typeof chartUiChildrenMap>;
export type ChartCompChildrenType = RecordConstructorToComp<typeof chartUiChildrenMap>;

View File

@@ -0,0 +1,150 @@
import { changeChildAction, CompAction } from "lowcoder-core";
import { ChartCompChildrenType, ChartTypeOptions,getDataKeys } from "./barChartConstants";
import { newSeries } from "./seriesComp";
import {
CustomModal,
Dropdown,
hiddenPropertyView,
Option,
RedButton,
Section,
sectionNames,
controlItem,
} from "lowcoder-sdk";
import { trans } from "i18n/comps";
export function barChartPropertyView(
children: ChartCompChildrenType,
dispatch: (action: CompAction) => void
) {
const series = children.series.getView();
const columnOptions = getDataKeys(children.data.getView()).map((key) => ({
label: key,
value: key,
}));
const uiModePropertyView = (
<>
<Section name={trans("chart.data")}>
{children.chartConfig.getPropertyView()}
<Dropdown
value={children.xAxisKey.getView()}
options={columnOptions}
label={trans("chart.xAxis")}
onChange={(value) => {
dispatch(changeChildAction("xAxisKey", value));
}}
/>
{children.chartConfig.getView().subtype === "waterfall" && children.xAxisData.propertyView({
label: "X-Label-Data"
})}
<Option
items={series}
title={trans("chart.chartSeries")}
itemTitle={(s) => s.getView().seriesName}
popoverTitle={(s) => s.getView().columnName}
content={(s, index) => (
<>
{s.getPropertyViewWithData(columnOptions)}
{
<RedButton
onClick={() => {
CustomModal.confirm({
title: trans("chart.delete"),
content: trans("chart.confirmDelete") + `${s.getView().seriesName}?`,
onConfirm: () =>
children.series.dispatch(children.series.deleteAction(index)),
confirmBtnType: "delete",
okText: trans("chart.delete"),
});
}}
>
{trans("chart.delete")}
</RedButton>
}
</>
)}
onAdd={() => {
if (columnOptions.length <= 0) {
return;
}
children.series.dispatch(
children.series.pushAction(
newSeries(trans("chart.customSeries"), columnOptions[0].value)
)
);
}}
onMove={(fromIndex, toIndex) => {
const action = children.series.arrayMoveAction(fromIndex, toIndex);
children.series.dispatch(action);
}}
hide={(s) => s.getView().hide}
onHide={(s, hide) => s.children.hide.dispatchChangeValueAction(hide)}
dataIndex={(s) => s.getView().dataIndex}
/>
</Section>
<Section name={sectionNames.interaction}>
<div style={{display: 'flex', flexDirection: 'column', gap: '8px'}}>
{children.onUIEvent.propertyView({title: trans("chart.chartEventHandlers")})}
</div>
<div style={{display: 'flex', flexDirection: 'column', gap: '8px'}}>
{children.onEvent.propertyView()}
</div>
</Section>
<Section name={sectionNames.layout}>
{children.echartsTitleConfig.getPropertyView()}
{children.echartsTitleVerticalConfig.getPropertyView()}
{children.legendConfig.getPropertyView()}
{children.title.propertyView({ label: trans("chart.title") })}
{children.left.propertyView({ label: trans("chart.left"), tooltip: trans("echarts.leftTooltip") })}
{children.right.propertyView({ label: trans("chart.right"), tooltip: trans("echarts.rightTooltip") })}
{children.top.propertyView({ label: trans("chart.top"), tooltip: trans("echarts.topTooltip") })}
{children.bottom.propertyView({ label: trans("chart.bottom"), tooltip: trans("echarts.bottomTooltip") })}
{children.chartConfig.children.compType.getView() !== "pie" && (
<>
{children.xAxisDirection.propertyView({
label: trans("chart.xAxisDirection"),
radioButton: true,
})}
{children.xConfig.getPropertyView()}
{children.yConfig.getPropertyView()}
</>
)}
{hiddenPropertyView(children)}
{children.tooltip.propertyView({label: trans("echarts.tooltip"), tooltip: trans("echarts.tooltipTooltip")})}
</Section>
<Section name={sectionNames.chartStyle}>
{children.chartStyle?.getPropertyView()}
</Section>
<Section name={sectionNames.titleStyle}>
{children.titleStyle?.getPropertyView()}
</Section>
<Section name={sectionNames.xAxisStyle}>
{children.xAxisStyle?.getPropertyView()}
</Section>
<Section name={sectionNames.yAxisStyle}>
{children.yAxisStyle?.getPropertyView()}
</Section>
<Section name={sectionNames.legendStyle}>
{children.legendStyle?.getPropertyView()}
</Section>
<Section name={sectionNames.advanced}>
{children.data.propertyView({
label: trans("chart.data"),
})}
</Section>
</>
);
const getChatConfigByMode = (mode: string) => {
switch(mode) {
case "ui":
return uiModePropertyView;
}
}
return (
<>
{getChatConfigByMode(children.mode.getView())}
</>
);
}

View File

@@ -0,0 +1,420 @@
import {
CharOptionCompType,
ChartCompPropsType,
ChartSize,
noDataAxisConfig,
noDataPieChartConfig,
} from "comps/barChartComp/barChartConstants";
import { getPieRadiusAndCenter } from "comps/basicChartComp/chartConfigs/pieChartConfig";
import { EChartsOptionWithMap } from "../basicChartComp/reactEcharts/types";
import _ from "lodash";
import { chartColorPalette, isNumeric, JSONObject, loadScript } from "lowcoder-sdk";
import { calcXYConfig } from "comps/basicChartComp/chartConfigs/cartesianAxisConfig";
import Big from "big.js";
import { googleMapsApiUrl } from "../basicChartComp/chartConfigs/chartUrls";
import opacityToHex from "../../util/opacityToHex";
import parseBackground from "../../util/gradientBackgroundColor";
import {ba} from "@fullcalendar/core/internal-common";
import {chartStyleWrapper, styleWrapper} from "../../util/styleWrapper";
export function transformData(
originData: JSONObject[],
xAxis: string,
seriesColumnNames: string[]
) {
// aggregate data by x-axis
const transformedData: JSONObject[] = [];
originData.reduce((prev, cur) => {
if (cur === null || cur === undefined) {
return prev;
}
const groupValue = cur[xAxis] as string;
if (!prev[groupValue]) {
// init as 0
const initValue: any = {};
seriesColumnNames.forEach((name) => {
initValue[name] = 0;
});
prev[groupValue] = initValue;
transformedData.push(prev[groupValue]);
}
// remain the x-axis data
prev[groupValue][xAxis] = groupValue;
seriesColumnNames.forEach((key) => {
if (key === xAxis) {
return;
} else if (isNumeric(cur[key])) {
const bigNum = Big(cur[key]);
prev[groupValue][key] = bigNum.add(prev[groupValue][key]).toNumber();
} else {
prev[groupValue][key] += 1;
}
});
return prev;
}, {} as any);
return transformedData;
}
const notAxisChartSet: Set<CharOptionCompType> = new Set(["pie"] as const);
const notAxisChartSubtypeSet: Set<string> = new Set(["polar"] as const);
export const echartsConfigOmitChildren = [
"hidden",
"selectedPoints",
"onUIEvent",
"mapInstance"
] as const;
type EchartsConfigProps = Omit<ChartCompPropsType, typeof echartsConfigOmitChildren[number]>;
export function isAxisChart(type: CharOptionCompType, subtype: string) {
return !notAxisChartSet.has(type) && !notAxisChartSubtypeSet.has(subtype);
}
export function getSeriesConfig(props: EchartsConfigProps) {
let visibleSeries = props.series.filter((s) => !s.getView().hide);
if(props.chartConfig.subtype === "waterfall") {
const seriesOn = visibleSeries[0];
const seriesPlaceholder = visibleSeries[0];
visibleSeries = [seriesPlaceholder, seriesOn];
}
const seriesLength = visibleSeries.length;
return visibleSeries.map((s, index) => {
if (isAxisChart(props.chartConfig.type, props.chartConfig.subtype)) {
let encodeX: string, encodeY: string;
const horizontalX = props.xAxisDirection === "horizontal";
let itemStyle = props.chartConfig.itemStyle;
// FIXME: need refactor... chartConfig returns a function with paramters
if (props.chartConfig.type === "bar") {
// barChart's border radius, depend on x-axis direction and stack state
const borderRadius = horizontalX ? [2, 2, 0, 0] : [0, 2, 2, 0];
if (props.chartConfig.stack && index === visibleSeries.length - 1) {
itemStyle = { ...itemStyle, borderRadius: borderRadius };
} else if (!props.chartConfig.stack) {
itemStyle = { ...itemStyle, borderRadius: borderRadius };
}
if(props.chartConfig.subtype === "waterfall" && index === 0) {
itemStyle = {
borderColor: 'transparent',
color: 'transparent'
}
}
}
if (horizontalX) {
encodeX = props.xAxisKey;
encodeY = s.getView().columnName;
} else {
encodeX = s.getView().columnName;
encodeY = props.xAxisKey;
}
return {
name: props.chartConfig.subtype === "waterfall" && index === 0?" ":s.getView().seriesName,
columnName: props.chartConfig.subtype === "waterfall" && index === 0?" ":s.getView().columnName,
selectedMode: "single",
select: {
itemStyle: {
borderColor: "#000",
},
},
encode: {
x: encodeX,
y: encodeY,
},
// each type of chart's config
...props.chartConfig,
itemStyle: itemStyle,
label: {
...props.chartConfig.label,
...(!horizontalX && { position: "outside" }),
},
};
} else {
const radiusAndCenter = getPieRadiusAndCenter(seriesLength, index, props.chartConfig);
return {
...props.chartConfig,
columnName: s.getView().columnName,
radius: radiusAndCenter.radius,
center: radiusAndCenter.center,
name: s.getView().seriesName,
selectedMode: "single",
encode: {
itemName: props.xAxisKey,
value: s.getView().columnName,
},
};
}
});
}
// https://echarts.apache.org/en/option.html
export function getEchartsConfig(
props: EchartsConfigProps,
chartSize?: ChartSize,
theme?: any,
): EChartsOptionWithMap {
// axisChart
const axisChart = isAxisChart(props.chartConfig.type, props.chartConfig.subtype);
const gridPos = {
left: `${props?.left}%`,
right: `${props?.right}%`,
bottom: `${props?.bottom}%`,
top: `${props?.top}%`,
};
let config: any = {
title: {
text: props.title,
top: props.echartsTitleVerticalConfig.top,
left:props.echartsTitleConfig.top,
textStyle: {
...styleWrapper(props?.titleStyle, theme?.titleStyle)
}
},
backgroundColor: parseBackground( props?.chartStyle?.background || theme?.chartStyle?.backgroundColor || "#FFFFFF"),
legend: {
...props.legendConfig,
textStyle: {
...styleWrapper(props?.legendStyle, theme?.legendStyle, 15)
}
},
tooltip: props.tooltip && {
trigger: "axis",
axisPointer: {
type: "line",
lineStyle: {
color: "rgba(0,0,0,0.2)",
width: 2,
type: "solid"
}
}
},
grid: {
...gridPos,
containLabel: true,
},
};
if(props.chartConfig.race) {
config = {
...config,
// Disable init animation.
animationDuration: 0,
animationDurationUpdate: 2000,
animationEasing: 'linear',
animationEasingUpdate: 'linear',
}
} else {
// Ensure proper animation settings when race is disabled
config = {
...config,
animationDuration: 1000,
animationDurationUpdate: 1000,
animationEasing: 'cubicOut',
animationEasingUpdate: 'cubicOut',
}
}
if (props.data.length <= 0) {
// no data
return {
...config,
...(axisChart ? noDataAxisConfig : noDataPieChartConfig),
};
}
const yAxisConfig = props.yConfig();
const seriesColumnNames = props.series
.filter((s) => !s.getView().hide)
.map((s) => s.getView().columnName);
// y-axis is category and time, data doesn't need to aggregate
let transformedData =
yAxisConfig.type === "category" || yAxisConfig.type === "time" ? props.echartsOption.length && props.echartsOption || props.data : transformData(props.echartsOption.length && props.echartsOption || props.data, props.xAxisKey, seriesColumnNames);
if(props.chartConfig.subtype === "waterfall") {
config.legend = undefined;
let sum = transformedData.reduce((acc, item) => {
if(typeof item[seriesColumnNames[0]] === 'number') return acc + item[seriesColumnNames[0]];
else return acc;
}, 0)
const total = sum;
transformedData.map(d => {
d[` `] = sum - d[seriesColumnNames[0]];
sum = d[` `];
})
transformedData = [{[" "]: 0, [seriesColumnNames[0]]: total, [props.xAxisKey]: "Total"}, ...transformedData]
}
if(props.chartConfig.subtype === "polar") {
config = {
...config,
polar: {
radius: [props.chartConfig.polarData.polarRadiusStart, props.chartConfig.polarData.polarRadiusEnd],
},
radiusAxis: {
type: props.chartConfig.polarData.polarIsTangent?'category':undefined,
data: props.chartConfig.polarData.polarIsTangent && props.chartConfig.polarData.labelData.length!==0?props.chartConfig.polarData.labelData:undefined,
max: props.chartConfig.polarData.polarIsTangent?undefined:props.chartConfig.polarData.radiusAxisMax || undefined,
},
angleAxis: {
type: props.chartConfig.polarData.polarIsTangent?undefined:'category',
data: !props.chartConfig.polarData.polarIsTangent && props.chartConfig.polarData.labelData.length!==0?props.chartConfig.polarData.labelData:undefined,
max: props.chartConfig.polarData.polarIsTangent?props.chartConfig.polarData.radiusAxisMax || undefined:undefined,
startAngle: props.chartConfig.polarData.polarStartAngle,
endAngle: props.chartConfig.polarData.polarEndAngle,
},
}
}
config = {
...config,
dataset: [
{
source: transformedData,
sourceHeader: false,
},
],
series: getSeriesConfig(props).map(series => ({
...series,
encode: {
...series.encode,
y: series.name,
},
itemStyle: {
...series.itemStyle,
...chartStyleWrapper(props?.chartStyle, theme?.chartStyle)
},
lineStyle: {
...chartStyleWrapper(props?.chartStyle, theme?.chartStyle)
},
data: transformedData.map((i: any) => i[series.columnName])
})),
};
if (axisChart) {
// pure chart's size except the margin around
let chartRealSize;
if (chartSize) {
const rightSize =
typeof gridPos.right === "number"
? gridPos.right
: (chartSize.w * parseFloat(gridPos.right)) / 100.0;
chartRealSize = {
// actually it's self-adaptive with the x-axis label on the left, not that accurate but work
w: chartSize.w - gridPos.left - rightSize,
// also self-adaptive on the bottom
h: chartSize.h - gridPos.top - gridPos.bottom,
right: rightSize,
};
}
const finalXyConfig = calcXYConfig(
props.xConfig,
yAxisConfig,
props.xAxisDirection,
transformedData.map((d) => d[props.xAxisKey]),
chartRealSize
);
config = {
...config,
// @ts-ignore
xAxis: {
...finalXyConfig.xConfig,
axisLabel: {
...styleWrapper(props?.xAxisStyle, theme?.xAxisStyle, 11)
},
data: finalXyConfig.xConfig.type === "category" && (props.xAxisData as []).length!==0?props?.xAxisData:transformedData.map((i: any) => i[props.xAxisKey]),
},
// @ts-ignore
yAxis: {
...finalXyConfig.yConfig,
axisLabel: {
...styleWrapper(props?.yAxisStyle, theme?.yAxisStyle, 11)
},
data: finalXyConfig.yConfig.type === "category" && (props.xAxisData as []).length!==0?props?.xAxisData:transformedData.map((i: any) => i[props.xAxisKey]),
},
};
if(props.chartConfig.race) {
config = {
...config,
xAxis: {
...config.xAxis,
animationDuration: 300,
animationDurationUpdate: 300
},
yAxis: {
...config.yAxis,
animationDuration: 300,
animationDurationUpdate: 300
},
}
} else {
// Reset axis animations when race is disabled
config = {
...config,
xAxis: {
...config.xAxis,
animationDuration: undefined,
animationDurationUpdate: undefined
},
yAxis: {
...config.yAxis,
animationDuration: undefined,
animationDurationUpdate: undefined
},
}
}
}
// console.log("Echarts transformedData and config", transformedData, config);
return config;
}
export function getSelectedPoints(param: any, option: any) {
const series = option.series;
const dataSource = _.isArray(option.dataset) && option.dataset[0]?.source;
if (series && dataSource) {
return param.selected.flatMap((selectInfo: any) => {
const seriesInfo = series[selectInfo.seriesIndex];
if (!seriesInfo || !seriesInfo.encode) {
return [];
}
return selectInfo.dataIndex.map((index: any) => {
const commonResult = {
seriesName: seriesInfo.name,
};
if (seriesInfo.encode.itemName && seriesInfo.encode.value) {
return {
...commonResult,
itemName: dataSource[index][seriesInfo.encode.itemName],
value: dataSource[index][seriesInfo.encode.value],
};
} else {
return {
...commonResult,
x: dataSource[index][seriesInfo.encode.x],
y: dataSource[index][seriesInfo.encode.y],
};
}
});
});
}
return [];
}
export function loadGoogleMapsScript(apiKey: string) {
const mapsUrl = `${googleMapsApiUrl}?key=${apiKey}`;
const scripts = document.getElementsByTagName('script');
// is script already loaded
let scriptIndex = _.findIndex(scripts, (script) => script.src.endsWith(mapsUrl));
if(scriptIndex > -1) {
return scripts[scriptIndex];
}
// is script loaded with diff api_key, remove the script and load again
scriptIndex = _.findIndex(scripts, (script) => script.src.startsWith(googleMapsApiUrl));
if(scriptIndex > -1) {
scripts[scriptIndex].remove();
}
const script = document.createElement("script");
script.type = "text/javascript";
script.src = mapsUrl;
script.async = true;
script.defer = true;
window.document.body.appendChild(script);
return script;
}

View File

@@ -0,0 +1,119 @@
import {
BoolControl,
StringControl,
list,
JSONObject,
isNumeric,
genRandomKey,
Dropdown,
OptionsType,
MultiCompBuilder,
valueComp,
} from "lowcoder-sdk";
import { trans } from "i18n/comps";
import { ConstructorToComp, ConstructorToDataType, ConstructorToView } from "lowcoder-core";
import { CompAction, CustomAction, customAction, isMyCustomAction } from "lowcoder-core";
export type SeriesCompType = ConstructorToComp<typeof SeriesComp>;
export type RawSeriesCompType = ConstructorToView<typeof SeriesComp>;
type SeriesDataType = ConstructorToDataType<typeof SeriesComp>;
type ActionDataType = {
type: "chartDataChanged";
chartData: Array<JSONObject>;
};
export function newSeries(name: string, columnName: string): SeriesDataType {
return {
seriesName: name,
columnName: columnName,
dataIndex: genRandomKey(),
};
}
const seriesChildrenMap = {
columnName: StringControl,
seriesName: StringControl,
hide: BoolControl,
// unique key, for sort
dataIndex: valueComp<string>(""),
};
const SeriesTmpComp = new MultiCompBuilder(seriesChildrenMap, (props) => {
return props;
})
.setPropertyViewFn(() => {
return <></>;
})
.build();
class SeriesComp extends SeriesTmpComp {
getPropertyViewWithData(columnOptions: OptionsType): React.ReactNode {
return (
<>
{this.children.seriesName.propertyView({
label: trans("chart.seriesName"),
})}
<Dropdown
value={this.children.columnName.getView()}
options={columnOptions}
label={trans("chart.dataColumns")}
onChange={(value) => {
this.children.columnName.dispatchChangeValueAction(value);
}}
/>
</>
);
}
}
const SeriesListTmpComp = list(SeriesComp);
export class SeriesListComp extends SeriesListTmpComp {
override reduce(action: CompAction): this {
if (isMyCustomAction<ActionDataType>(action, "chartDataChanged")) {
// auto generate series
const actions = this.genExampleSeriesActions(action.value.chartData);
return this.reduce(this.multiAction(actions));
}
return super.reduce(action);
}
private genExampleSeriesActions(chartData: Array<JSONObject>) {
const actions: CustomAction[] = [];
if (!chartData || chartData.length <= 0 || !chartData[0]) {
return actions;
}
let delCnt = 0;
const existColumns = this.getView().map((s) => s.getView().columnName);
// delete series not in data
existColumns.forEach((columnName) => {
if (chartData[0]?.[columnName] === undefined) {
actions.push(this.deleteAction(0));
delCnt++;
}
});
if (existColumns.length > delCnt) {
// don't generate example if exists
return actions;
}
// generate example series
const exampleKeys = Object.keys(chartData[0])
.filter((key) => {
return !existColumns.includes(key) && isNumeric(chartData[0][key]);
})
.slice(0, 3);
exampleKeys.forEach((key) => actions.push(this.pushAction(newSeries(key, key))));
return actions;
}
dispatchDataChanged(chartData: Array<JSONObject>): void {
this.dispatch(
customAction<ActionDataType>({
type: "chartDataChanged",
chartData: chartData,
})
);
}
}

View File

@@ -0,0 +1,303 @@
import {
changeChildAction,
changeValueAction,
CompAction,
CompActionTypes,
wrapChildAction,
} from "lowcoder-core";
import { AxisFormatterComp, EchartsAxisType } from "./chartConfigs/cartesianAxisConfig";
import { chartChildrenMap, ChartSize, getDataKeys } from "./chartConstants";
import { chartPropertyView } from "./chartPropertyView";
import _ from "lodash";
import { useContext, useEffect, useMemo, useRef, useState } from "react";
import { useResizeDetector } from "react-resize-detector";
import ReactECharts from "./reactEcharts";
import {
childrenToProps,
depsConfig,
genRandomKey,
NameConfig,
UICompBuilder,
withDefault,
withExposingConfigs,
withMethodExposing,
withViewFn,
ThemeContext,
chartColorPalette,
getPromiseAfterDispatch,
dropdownControl,
} from "lowcoder-sdk";
import { getEchartsLocale, trans } from "i18n/comps";
import { ItemColorComp } from "comps/chartComp/chartConfigs/lineChartConfig";
import {
echartsConfigOmitChildren,
getEchartsConfig,
getSelectedPoints,
} from "./chartUtils";
import 'echarts-extension-gmap';
import log from "loglevel";
let clickEventCallback = () => {};
const chartModeOptions = [
{
label: trans("chart.UIMode"),
value: "ui",
}
] as const;
let BasicChartTmpComp = (function () {
return new UICompBuilder({mode:dropdownControl(chartModeOptions,'ui'),...chartChildrenMap}, () => null)
.setPropertyViewFn(chartPropertyView)
.build();
})();
BasicChartTmpComp = withViewFn(BasicChartTmpComp, (comp) => {
const mode = comp.children.mode.getView();
const onUIEvent = comp.children.onUIEvent.getView();
const onEvent = comp.children.onEvent.getView();
const echartsCompRef = useRef<ReactECharts | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [chartSize, setChartSize] = useState<ChartSize>();
const firstResize = useRef(true);
const theme = useContext(ThemeContext);
const defaultChartTheme = {
color: chartColorPalette,
backgroundColor: "#fff",
};
let themeConfig = defaultChartTheme;
try {
themeConfig = theme?.theme.chart ? JSON.parse(theme?.theme.chart) : defaultChartTheme;
} catch (error) {
log.error('theme chart error: ', error);
}
const triggerClickEvent = async (dispatch: any, action: CompAction<JSONValue>) => {
await getPromiseAfterDispatch(
dispatch,
action,
{ autoHandleAfterReduce: true }
);
onEvent('click');
}
useEffect(() => {
// bind events
const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance();
if (!echartsCompInstance) {
return _.noop;
}
echartsCompInstance?.on("selectchanged", (param: any) => {
const option: any = echartsCompInstance?.getOption();
document.dispatchEvent(new CustomEvent("clickEvent", {
bubbles: true,
detail: {
action: param.fromAction,
data: getSelectedPoints(param, option)
}
}));
if (param.fromAction === "select") {
comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false));
onUIEvent("select");
} else if (param.fromAction === "unselect") {
comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false));
onUIEvent("unselect");
}
triggerClickEvent(
comp.dispatch,
changeChildAction("lastInteractionData", getSelectedPoints(param, option), false)
);
});
// unbind
return () => {
echartsCompInstance?.off("selectchanged");
document.removeEventListener('clickEvent', clickEventCallback)
};
}, [onUIEvent]);
const echartsConfigChildren = _.omit(comp.children, echartsConfigOmitChildren);
const childrenProps = childrenToProps(echartsConfigChildren);
const option = useMemo(() => {
return getEchartsConfig(
childrenProps as ToViewReturn<typeof echartsConfigChildren>,
chartSize,
themeConfig
);
}, [childrenProps, chartSize, ...Object.values(echartsConfigChildren)]);
useEffect(() => {
comp.children.mapInstance.dispatch(changeValueAction(null, false))
}, [option])
useResizeDetector({
targetRef: containerRef,
onResize: ({width, height}) => {
if (width && height) {
setChartSize({ w: width, h: height });
}
if (!firstResize.current) {
// ignore the first resize, which will impact the loading animation
echartsCompRef.current?.getEchartsInstance().resize();
} else {
firstResize.current = false;
}
}
})
return (
<div ref={containerRef} style={{height: '100%'}}>
<ReactECharts
ref={(e) => (echartsCompRef.current = e)}
style={{ height: "100%" }}
notMerge
lazyUpdate
opts={{ locale: getEchartsLocale() }}
option={option}
theme={themeConfig}
mode={mode}
/>
</div>
);
});
function getYAxisFormatContextValue(
data: Array<JSONObject>,
yAxisType: EchartsAxisType,
yAxisName?: string
) {
const dataSample = yAxisName && data.length > 0 && data[0][yAxisName];
let contextValue = dataSample;
if (yAxisType === "time") {
// to timestamp
const time =
typeof dataSample === "number" || typeof dataSample === "string"
? new Date(dataSample).getTime()
: null;
if (time) contextValue = time;
}
return contextValue;
}
BasicChartTmpComp = class extends BasicChartTmpComp {
private lastYAxisFormatContextVal?: JSONValue;
private lastColorContext?: JSONObject;
updateContext(comp: this) {
// the context value of axis format
let resultComp = comp;
const data = comp.children.data.getView();
const sampleSeries = comp.children.series.getView().find((s) => !s.getView().hide);
const yAxisContextValue = getYAxisFormatContextValue(
data,
comp.children.yConfig.children.yAxisType.getView(),
sampleSeries?.children.columnName.getView()
);
if (yAxisContextValue !== comp.lastYAxisFormatContextVal) {
comp.lastYAxisFormatContextVal = yAxisContextValue;
resultComp = comp.setChild(
"yConfig",
comp.children.yConfig.reduce(
wrapChildAction(
"formatter",
AxisFormatterComp.changeContextDataAction({ value: yAxisContextValue })
)
)
);
}
// item color context
const colorContextVal = {
seriesName: sampleSeries?.children.seriesName.getView(),
value: yAxisContextValue,
};
if (
comp.children.chartConfig.children.comp.children.hasOwnProperty("itemColor") &&
!_.isEqual(colorContextVal, comp.lastColorContext)
) {
comp.lastColorContext = colorContextVal;
resultComp = resultComp.setChild(
"chartConfig",
comp.children.chartConfig.reduce(
wrapChildAction(
"comp",
wrapChildAction("itemColor", ItemColorComp.changeContextDataAction(colorContextVal))
)
)
);
}
return resultComp;
}
override reduce(action: CompAction): this {
const comp = super.reduce(action);
if (action.type === CompActionTypes.UPDATE_NODES_V2) {
const newData = comp.children.data.getView();
// data changes
if (comp.children.data !== this.children.data) {
setTimeout(() => {
// update x-axis value
const keys = getDataKeys(newData);
if (keys.length > 0 && !keys.includes(comp.children.xAxisKey.getView())) {
comp.children.xAxisKey.dispatch(changeValueAction(keys[0] || ""));
}
// pass to child series comp
comp.children.series.dispatchDataChanged(newData);
}, 0);
}
return this.updateContext(comp);
}
return comp;
}
override autoHeight(): boolean {
return false;
}
};
let BasicChartComp = withExposingConfigs(BasicChartTmpComp, [
depsConfig({
name: "selectedPoints",
desc: trans("chart.selectedPointsDesc"),
depKeys: ["selectedPoints"],
func: (input) => {
return input.selectedPoints;
},
}),
depsConfig({
name: "lastInteractionData",
desc: trans("chart.lastInteractionDataDesc"),
depKeys: ["lastInteractionData"],
func: (input) => {
return input.lastInteractionData;
},
}),
depsConfig({
name: "data",
desc: trans("chart.dataDesc"),
depKeys: ["data", "mode"],
func: (input) => input.data,
}),
new NameConfig("title", trans("chart.titleDesc")),
]);
export const BasicChartCompWithDefault = withDefault(BasicChartComp, {
xAxisKey: "date",
series: [
{
dataIndex: genRandomKey(),
seriesName: trans("chart.spending"),
columnName: "spending",
},
{
dataIndex: genRandomKey(),
seriesName: trans("chart.budget"),
columnName: "budget",
},
],
});

View File

@@ -0,0 +1,141 @@
import {
BoolControl,
NumberControl,
StringControl,
withDefault,
dropdownControl,
MultiCompBuilder,
showLabelPropertyView,
ColorControl,
Dropdown,
toArray,
jsonControl,
} from "lowcoder-sdk";
import { changeChildAction, CompAction } from "lowcoder-core";
import { BarSeriesOption } from "echarts";
import { i18nObjs, trans } from "i18n/comps";
const BarTypeOptions = [
{
label: trans("chart.basicBar"),
value: "basicBar",
},
{
label: trans("chart.waterfallBar"),
value: "waterfall",
},
{
label: trans("chart.polar"),
value: "polar",
},
] as const;
export const BarChartConfig = (function () {
return new MultiCompBuilder(
{
showLabel: withDefault(BoolControl, true),
type: dropdownControl(BarTypeOptions, "basicBar"),
barWidth: withDefault(NumberControl, 40),
showBackground: withDefault(BoolControl, false),
backgroundColor: withDefault(ColorControl, i18nObjs.defaultBarChartOption.barBg),
radiusAxisMax: NumberControl,
polarRadiusStart: withDefault(StringControl, '30'),
polarRadiusEnd: withDefault(StringControl, '80%'),
polarStartAngle: withDefault(NumberControl, 90),
polarEndAngle: withDefault(NumberControl, -180),
polarIsTangent: withDefault(BoolControl, false),
stack: withDefault(BoolControl, false),
race: withDefault(BoolControl, false),
labelData: jsonControl(toArray, []),
},
(props): BarSeriesOption => {
const config: BarSeriesOption = {
type: "bar",
subtype: props.type,
realtimeSort: props.race,
seriesLayoutBy: props.race?'column':'row',
label: {
show: props.showLabel,
position: "top",
valueAnimation: props.race,
},
barWidth: `${props.barWidth}%`,
showBackground: props.showBackground,
backgroundStyle: {
color: props.backgroundColor,
},
polarData: {
radiusAxisMax: props.radiusAxisMax,
polarRadiusStart: props.polarRadiusStart,
polarRadiusEnd: props.polarRadiusEnd,
polarStartAngle: props.polarStartAngle,
polarEndAngle: props.polarEndAngle,
labelData: props.labelData,
polarIsTangent: props.polarIsTangent,
},
race: props.race,
};
if (props.stack) {
config.stack = "stackValue";
}
if (props.type === "waterfall") {
config.label = undefined;
config.stack = "stackValue";
}
if (props.type === "polar") {
config.coordinateSystem = 'polar';
}
return config;
}
)
.setPropertyViewFn((children, dispatch: (action: CompAction) => void) => (
<>
<Dropdown
value={children.type.getView()}
options={BarTypeOptions}
label={trans("chart.barType")}
onChange={(value) => {
dispatch(changeChildAction("type", value));
}}
/>
{showLabelPropertyView(children)}
{children.barWidth.propertyView({
label: trans("barChart.barWidth"),
})}
{children.type.getView() !== "waterfall" && children.race.propertyView({
label: trans("barChart.race"),
})}
{children.type.getView() !== "waterfall" && children.stack.propertyView({
label: trans("barChart.stack"),
})}
{children.showBackground.propertyView({
label: trans("barChart.showBg"),
})}
{children.showBackground.getView() && children.backgroundColor.propertyView({
label: trans("barChart.bgColor"),
})}
{children.type.getView() === "polar" && children.polarIsTangent.propertyView({
label: trans("barChart.polarIsTangent"),
})}
{children.type.getView() === "polar" && children.polarStartAngle.propertyView({
label: trans("barChart.polarStartAngle"),
})}
{children.type.getView() === "polar" && children.polarEndAngle.propertyView({
label: trans("barChart.polarEndAngle"),
})}
{children.type.getView() === "polar" && children.radiusAxisMax.propertyView({
label: trans("barChart.radiusAxisMax"),
})}
{children.type.getView() === "polar" && children.polarRadiusStart.propertyView({
label: trans("barChart.polarRadiusStart"),
})}
{children.type.getView() === "polar" && children.polarRadiusEnd.propertyView({
label: trans("barChart.polarRadiusEnd"),
})}
{children.type.getView() === "polar" && children.labelData.propertyView({
label: trans("barChart.polarLabelData"),
})}
</>
))
.build();
})();

View File

@@ -0,0 +1,35 @@
import {
BoolControl,
MultiCompBuilder,
showLabelPropertyView,
} from "lowcoder-sdk";
import { CandlestickSeriesOption } from "echarts";
import { trans } from "i18n/comps";
export const CandleStickChartConfig = (function () {
return new MultiCompBuilder(
{
showLabel: BoolControl,
},
(props): CandlestickSeriesOption => {
const config: CandlestickSeriesOption = {
type: "candlestick",
label: {
show: props.showLabel,
position: "top",
},
};
return config;
}
)
.setPropertyViewFn((children) => (
<>
{showLabelPropertyView(children)}
{children.type.propertyView({
label: trans("candleStickChart.candleStickType"),
radioButton: true,
})}
</>
))
.build();
})();

View File

@@ -0,0 +1,307 @@
import { XAXisComponentOption, YAXisComponentOption } from "echarts";
import { ChartSize, XAxisDirectionType } from "../chartConstants";
import { i18n } from "lowcoder-core";
import {
MultiCompBuilder,
withContext,
NumberControl,
StringControl,
dropdownControl,
JSONValue,
isNumeric,
} from "lowcoder-sdk";
import { i18nObjs, trans } from "i18n/comps";
import _, { isNil } from "lodash";
import { xAxisTypeUrl } from "./chartUrls";
const XAxisTypeOptions = [
{
label: trans("chart.auto"),
value: "default",
},
{
label: trans("chart.categoryAxis"),
value: "category",
},
{
label: trans("chart.valueAxis"),
value: "value",
},
{
label: trans("chart.timeAxis"),
value: "time",
},
{
label: trans("chart.logAxis"),
value: "log",
},
] as const;
const YAxisTypeOptions = [
{
label: trans("chart.valueAxis"),
value: "value",
},
{
label: trans("chart.categoryAxis"),
value: "category",
},
{
label: trans("chart.timeAxis"),
value: "time",
},
{
label: trans("chart.logAxis"),
value: "log",
},
] as const;
export type EchartsAxisType = "category" | "value" | "time" | "log";
const axisCommonMap = {
axisName: StringControl,
logBase: NumberControl,
};
export const AxisFormatterComp = withContext(
new MultiCompBuilder({ value: StringControl }, (props) => props.value)
.setPropertyViewFn((children) =>
children.value.propertyView({
label: trans("chart.yAxisDataFormat"),
placeholder: "{{value}}",
tooltip: trans("chart.yAxisDataFormatTooltip"),
})
)
.build(),
["value"] as const
);
export const XAxisConfig = (function () {
return new MultiCompBuilder(
{
...axisCommonMap,
type: dropdownControl(XAxisTypeOptions, "default"),
},
(props): XAXisComponentOption => {
const config: XAXisComponentOption = {
name: props.axisName,
nameGap: 22,
// @ts-ignore
nameLocation: "middle",
};
if (props.type !== "default") {
// don't assign value for default value, compute it in the end
config.type = props.type;
}
return config;
}
)
.setPropertyViewFn((children) => (
<>
{children.axisName.propertyView({
label: trans("chart.xAxisName"),
})}
{children.type.propertyView({
label: trans("chart.xAxisType"),
tooltip: (
<>
{trans("chart.xAxisTypeTooltip")}
<a href={xAxisTypeUrl} target="_blank" rel="noreferrer">
{trans("chart.xAxisType")}
</a>
</>
),
})}
{children.type.getView() === "log" &&
children.logBase.propertyView({
label: trans("chart.logBase"),
})}
</>
))
.build();
})();
export const YAxisConfig = (function () {
return new MultiCompBuilder(
{
...axisCommonMap,
// the old data has "type" field with default value "category". change field name to "yAxisType" for compatibility
yAxisType: dropdownControl(YAxisTypeOptions, "value"),
formatter: AxisFormatterComp,
},
(props) => () => {
const config: YAXisComponentOption = {
name: props.axisName,
type: props.yAxisType,
nameTextStyle: {
align: "left",
},
};
const numberFormat = new Intl.NumberFormat(i18n.locales, {
notation: "compact",
});
(config.axisLabel as any) = {
formatter: (value: string | number) => {
const res = (props.formatter as any)({ value: value });
if (!isNil(res) && res !== "") {
return res;
}
if (
(props.yAxisType === "value" || props.yAxisType === "log") &&
typeof value === "number"
) {
return numberFormat.format(value);
}
return value + "";
},
};
if (props.yAxisType === "log") {
(config as any).logBase = props.logBase || 10;
}
return config;
}
)
.setPropertyViewFn((children) => (
<>
{children.axisName.propertyView({
label: trans("chart.yAxisName"),
})}
{children.yAxisType.propertyView({
label: trans("chart.yAxisType"),
})}
{children.yAxisType.getView() === "log" &&
children.logBase.propertyView({
label: trans("chart.logBase"),
})}
{children.formatter.getPropertyView()}
</>
))
.build();
})();
function calcXAxisType(xAxisData: Array<JSONValue | undefined>): EchartsAxisType {
if (!xAxisData || xAxisData.length <= 0) {
return "category";
}
const sampleData = xAxisData[0];
if (!sampleData) {
return "category";
}
if (isNumeric(sampleData)) {
return "value";
} else if (!isNaN(new Date(sampleData.toString()).getDate())) {
return "time";
} else {
return "category";
}
}
const dateInterval = {
year: 3600 * 24 * 1000 * 365,
month: 3600 * 24 * 1000 * 28,
day: 3600 * 24 * 1000,
};
function calcTimeInterval(xAxisData: Array<JSONValue | undefined>) {
const minIntervals = xAxisData.map((data) => {
if (!data) {
// 1 is echarts default value, to make sure axis tick is integer
return 1;
}
const dataLen = data.toString().length;
if (dataLen === 4) {
// year 2022
return dateInterval.year;
} else if (dataLen === 6 || dataLen === 7) {
// month 2022-01 222201
return dateInterval.month;
} else if (dataLen === 10 || dataLen === 8) {
// day 2022-01-01 20220101
return dateInterval.day;
} else {
return 1;
}
});
return _.min(minIntervals);
}
let measureCanvas: HTMLCanvasElement;
// calculate x-axis text width
function getXAxisDataLength(xAxisData: Array<JSONValue | undefined>) {
const canvas = measureCanvas || (measureCanvas = document.createElement("canvas"));
const context = canvas.getContext("2d");
if (!context) {
return [];
}
// echarts default font
context.font = "normal 12px sans-serif";
return xAxisData.map((d) => (d ? context.measureText(d.toString()).width + 2 : 0));
}
export function calcXYConfig(
xConfig: XAXisComponentOption,
yConfig: YAXisComponentOption,
xAxisDirection: XAxisDirectionType,
xAxisData: Array<JSONValue | undefined>,
chartSize?: ChartSize & { right: number }
) {
const resXConfig = { ...xConfig };
const resYConfig = { ...yConfig };
if (!resXConfig.type) {
// simple calculate x-axis type
resXConfig.type = calcXAxisType(xAxisData);
}
// x-axis label style adaptive
if (resXConfig.type === "category" && chartSize) {
const xAxisDataLenList = getXAxisDataLength(xAxisData);
// get x-axis single data's max width
const maxDataWidth = _.max(xAxisDataLenList);
const lastDataWidth = xAxisDataLenList[xAxisDataLenList.length - 1];
// grid width
let eachDataWidth = chartSize.w / xAxisData.length;
let rotate = 0;
let labelWidth = maxDataWidth;
// rotate when width is not enough
if (maxDataWidth && eachDataWidth < maxDataWidth && xAxisDirection === "horizontal") {
labelWidth = Math.min(maxDataWidth, 150);
// vertical rotate 0.87 => sin(60) when exceeding the right boundary
const verticalRotate =
lastDataWidth && lastDataWidth * 0.87 > eachDataWidth / 2 + chartSize.right;
rotate = verticalRotate ? 270 : 330;
// to keep x-axis name under label, nameGap is related to label rotation angle
resXConfig.nameGap = verticalRotate ? labelWidth + 5 : labelWidth / 2 + 10;
} else if (xAxisDirection === "vertical" && maxDataWidth) {
// vertical direction
resXConfig.nameGap = maxDataWidth + 10;
}
resXConfig.axisLabel = {
interval: 0,
width: labelWidth,
// @ts-ignore
overflow: "truncate",
rotate: rotate,
};
} else if (resXConfig.type === "time") {
(resXConfig as any).minInterval = calcTimeInterval(xAxisData);
const timeXAxisLabel = i18nObjs.timeXAxisLabel;
if (timeXAxisLabel) {
resXConfig.axisLabel = timeXAxisLabel;
}
}
if (xAxisDirection === "vertical") {
resYConfig.nameLocation = "middle";
resYConfig.nameGap = 25;
}
return xAxisDirection === "horizontal"
? {
xConfig: resXConfig,
yConfig: resYConfig,
}
: {
xConfig: resYConfig,
yConfig: resXConfig,
};
}

View File

@@ -0,0 +1,9 @@
import { language } from "i18n/comps";
const echartsUrlLocale = language === "zh" ? "zh" : "en";
export const optionUrl = `https://echarts.apache.org/${echartsUrlLocale}/option.html`;
export const examplesUrl = `https://echarts.apache.org/examples/${echartsUrlLocale}/index.html`;
export const xAxisTypeUrl = `${optionUrl}#xAxis.type`;
export const googleMapsApiUrl = `https://maps.googleapis.com/maps/api/js`;
export const mapOptionUrl = `https://github.com/plainheart/echarts-extension-gmap`;
export const mapExamplesUrl = `https://codepen.io/plainheart/pen/VweLGbR`;

View File

@@ -0,0 +1,49 @@
import {
AlignClose,
AlignRight,
AlignLeft,
dropdownControl,
MultiCompBuilder,
} from "lowcoder-sdk";
import { LegendComponentOption } from "echarts";
import { trans } from "i18n/comps";
const LabelPositionOptions = [
{
label: <AlignClose />,
value: "inside",
},
{
label: <AlignRight />,
value: "right",
},
{
label: <AlignLeft />,
value: "left",
},
] as const;
export const EchartsLabelConfig = (function () {
return new MultiCompBuilder(
{
position: dropdownControl(LabelPositionOptions, "inside"),
},
(props): LegendComponentOption => {
const config: LegendComponentOption = {
top: "inside",
type: "scroll",
};
config.top = props.position
return config;
}
)
.setPropertyViewFn((children) => (
<>
{children.position.propertyView({
label: trans("echarts.labelPosition"),
radioButton: true,
})}
</>
))
.build();
})();

View File

@@ -0,0 +1,44 @@
import {
AlignBottom,
AlignTop,
dropdownControl,
MultiCompBuilder,
} from "lowcoder-sdk";
import { LegendComponentOption } from "echarts";
import { trans } from "i18n/comps";
const LegendPositionOptions = [
{
label: <AlignBottom />,
value: "bottom",
},
{
label: <AlignTop />,
value: "top",
},
] as const;
export const EchartsLegendConfig = (function () {
return new MultiCompBuilder(
{
position: dropdownControl(LegendPositionOptions, "bottom"),
},
(props): LegendComponentOption => {
const config: LegendComponentOption = {
top: "bottom",
type: "scroll",
};
config.top = props.position
return config;
}
)
.setPropertyViewFn((children) => (
<>
{children.position.propertyView({
label: trans("echarts.legendPosition"),
radioButton: true,
})}
</>
))
.build();
})();

View File

@@ -0,0 +1,49 @@
import {
AlignClose,
AlignRight,
AlignLeft,
dropdownControl,
MultiCompBuilder,
} from "lowcoder-sdk";
import { LegendComponentOption } from "echarts";
import { trans } from "i18n/comps";
const TitlePositionOptions = [
{
label: <AlignClose />,
value: "center",
},
{
label: <AlignRight />,
value: "right",
},
{
label: <AlignLeft />,
value: "left",
},
] as const;
export const EchartsTitleConfig = (function () {
return new MultiCompBuilder(
{
position: dropdownControl(TitlePositionOptions, "center"),
},
(props): LegendComponentOption => {
const config: LegendComponentOption = {
top: "center",
type: "scroll",
};
config.top = props.position
return config;
}
)
.setPropertyViewFn((children) => (
<>
{children.position.propertyView({
label: trans("echarts.titlePosition"),
radioButton: true,
})}
</>
))
.build();
})();

View File

@@ -0,0 +1,35 @@
import {
BoolControl,
MultiCompBuilder,
showLabelPropertyView,
} from "lowcoder-sdk";
import { FunnelSeriesOption } from "echarts";
import { trans } from "i18n/comps";
export const FunnelChartConfig = (function () {
return new MultiCompBuilder(
{
showLabel: BoolControl,
},
(props): FunnelSeriesOption => {
const config: FunnelSeriesOption = {
type: "funnel",
label: {
show: props.showLabel,
position: "top",
},
};
return config;
}
)
.setPropertyViewFn((children) => (
<>
{showLabelPropertyView(children)}
{children.type.propertyView({
label: trans("funnelChart.funnelType"),
radioButton: true,
})}
</>
))
.build();
})();

View File

@@ -0,0 +1,31 @@
import {
BoolControl,
MultiCompBuilder,
showLabelPropertyView,
} from "lowcoder-sdk";
import { GaugeSeriesOption } from "echarts";
import { trans } from "i18n/comps";
export const GaugeChartConfig = (function () {
return new MultiCompBuilder(
{
showLabel: BoolControl,
},
(props): GaugeSeriesOption => {
const config: GaugeSeriesOption = {
type: "gauge",
};
return config;
}
)
.setPropertyViewFn((children) => (
<>
{showLabelPropertyView(children)}
{children.type.propertyView({
label: trans("gaugeChart.gaugeType"),
radioButton: true,
})}
</>
))
.build();
})();

View File

@@ -0,0 +1,31 @@
import {
BoolControl,
MultiCompBuilder,
showLabelPropertyView,
} from "lowcoder-sdk";
import { GraphSeriesOption } from "echarts";
import { trans } from "i18n/comps";
export const GraphChartConfig = (function () {
return new MultiCompBuilder(
{
showLabel: BoolControl,
},
(props): GraphSeriesOption => {
const config: GraphSeriesOption = {
type: "graph",
};
return config;
}
)
.setPropertyViewFn((children) => (
<>
{showLabelPropertyView(children)}
{children.type.propertyView({
label: trans("graphChart.graphType"),
radioButton: true,
})}
</>
))
.build();
})();

Some files were not shown because too many files have changed in this diff Show More