Updates
This commit is contained in:
1
lowcoder/.gitbook.yaml
Normal file
1
lowcoder/.gitbook.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
root: ./docs/
|
||||||
24
lowcoder/.gitignore
vendored
Normal file
24
lowcoder/.gitignore
vendored
Normal 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
661
lowcoder/LICENSE
Normal 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
20
lowcoder/MANIFESTO.md
Normal 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
116
lowcoder/README.md
Normal 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
|
||||||
|
[](https://deploy.stitch.tech/lowcoder/lowcoder)
|
||||||
|
|
||||||
|
[](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
215
lowcoder/app.json
Normal 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
55
lowcoder/client/.gitignore
vendored
Normal 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
|
||||||
@@ -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' }],
|
||||||
|
- }],
|
||||||
|
+ }
|
||||||
|
+ ],
|
||||||
|
});
|
||||||
@@ -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
|
||||||
28
lowcoder/client/.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
28
lowcoder/client/.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal 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
874
lowcoder/client/.yarn/releases/yarn-3.6.4.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
9
lowcoder/client/.yarnrc.yml
Normal file
9
lowcoder/client/.yarnrc.yml
Normal 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
188
lowcoder/client/README.md
Normal 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 you’ve 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 Netlify’s build memory limits.
|
||||||
1
lowcoder/client/VERSION
Normal file
1
lowcoder/client/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
2.7.5
|
||||||
45
lowcoder/client/config/test/jest.config.js
Normal file
45
lowcoder/client/config/test/jest.config.js
Normal 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,
|
||||||
|
};
|
||||||
59
lowcoder/client/config/test/jest.setup-after-env.js
Normal file
59
lowcoder/client/config/test/jest.setup-after-env.js
Normal 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;
|
||||||
3
lowcoder/client/config/test/jest.setup.js
Normal file
3
lowcoder/client/config/test/jest.setup.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
if (typeof window !== "undefined") {
|
||||||
|
require("whatwg-fetch");
|
||||||
|
}
|
||||||
1
lowcoder/client/config/test/mocks/markdown-url-module.js
Normal file
1
lowcoder/client/config/test/mocks/markdown-url-module.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export default "";
|
||||||
5
lowcoder/client/config/test/mocks/react-markdown.js
vendored
Normal file
5
lowcoder/client/config/test/mocks/react-markdown.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
function ReactMarkdown({ children }) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReactMarkdown;
|
||||||
18
lowcoder/client/config/test/setup.ts
Normal file
18
lowcoder/client/config/test/setup.ts
Normal 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"));
|
||||||
|
// };
|
||||||
21
lowcoder/client/config/test/transform/babelTransform.js
Normal file
21
lowcoder/client/config/test/transform/babelTransform.js
Normal 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,
|
||||||
|
});
|
||||||
16
lowcoder/client/config/test/transform/cssTransform.js
Normal file
16
lowcoder/client/config/test/transform/cssTransform.js
Normal 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",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
43
lowcoder/client/config/test/transform/fileTransform.js
Normal file
43
lowcoder/client/config/test/transform/fileTransform.js
Normal 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};`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
4
lowcoder/client/jest.config.js
Normal file
4
lowcoder/client/jest.config.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default {
|
||||||
|
projects: ["<rootDir>/packages/lowcoder", "<rootDir>/packages/lowcoder-core"],
|
||||||
|
};
|
||||||
|
// we use this for testing.
|
||||||
8
lowcoder/client/netlify.toml
Normal file
8
lowcoder/client/netlify.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[[redirects]]
|
||||||
|
from = "/*"
|
||||||
|
to = "/"
|
||||||
|
status = 200
|
||||||
|
[build]
|
||||||
|
base = "client"
|
||||||
|
command = "yarn workspace lowcoder build"
|
||||||
|
publish = "client/packages/lowcoder/build"
|
||||||
94
lowcoder/client/package.json
Normal file
94
lowcoder/client/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
lowcoder/client/packages/lowcoder-cli-template-typescript/.gitignore
vendored
Normal file
4
lowcoder/client/packages/lowcoder-cli-template-typescript/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
*.tgz
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
*.zip
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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>
|
||||||
@@ -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 |
@@ -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 |
@@ -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>
|
||||||
@@ -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 />);
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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")),
|
||||||
|
]);
|
||||||
@@ -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>
|
||||||
3
lowcoder/client/packages/lowcoder-cli-template-typescript/src/app-env.d.ts
vendored
Normal file
3
lowcoder/client/packages/lowcoder-cli-template-typescript/src/app-env.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="lowcoder-cli/client" />
|
||||||
|
|
||||||
|
declare module "lowcoder-sdk";
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -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";
|
||||||
@@ -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",
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -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>;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export const zh = {
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { I18nObjects } from "./types";
|
||||||
|
|
||||||
|
export const zhObj: I18nObjects | undefined = undefined;
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import HillchartsComp from "./HillchartsComp";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
hillcharts: HillchartsComp
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.wrapper {
|
||||||
|
padding: 5px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
/* height: 100%; */
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
218
lowcoder/client/packages/lowcoder-cli-template-typescript/src/vendors/Chart.jsx
vendored
Normal file
218
lowcoder/client/packages/lowcoder-cli-template-typescript/src/vendors/Chart.jsx
vendored
Normal 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;
|
||||||
1
lowcoder/client/packages/lowcoder-cli-template-typescript/src/vendors/index.jsx
vendored
Normal file
1
lowcoder/client/packages/lowcoder-cli-template-typescript/src/vendors/index.jsx
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Chart } from './Chart'
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import config from "lowcoder-cli/config/vite.config";
|
||||||
|
export default {
|
||||||
|
...config,
|
||||||
|
server: {
|
||||||
|
open: true,
|
||||||
|
port: 9000,
|
||||||
|
},
|
||||||
|
};
|
||||||
2
lowcoder/client/packages/lowcoder-cli/.gitignore
vendored
Normal file
2
lowcoder/client/packages/lowcoder-cli/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.out
|
||||||
|
node_modules
|
||||||
3
lowcoder/client/packages/lowcoder-cli/README.md
Normal file
3
lowcoder/client/packages/lowcoder-cli/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# lowcoder-cli
|
||||||
|
|
||||||
|
CLI tool used to start build and publish lowcoder component library.
|
||||||
133
lowcoder/client/packages/lowcoder-cli/actions/build.js
Normal file
133
lowcoder/client/packages/lowcoder-cli/actions/build.js
Normal 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!`);
|
||||||
|
}
|
||||||
109
lowcoder/client/packages/lowcoder-cli/actions/init.js
Normal file
109
lowcoder/client/packages/lowcoder-cli/actions/init.js
Normal 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();
|
||||||
|
}
|
||||||
41
lowcoder/client/packages/lowcoder-cli/client.d.ts
vendored
Normal file
41
lowcoder/client/packages/lowcoder-cli/client.d.ts
vendored
Normal 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;
|
||||||
119
lowcoder/client/packages/lowcoder-cli/config/modules.js
Normal file
119
lowcoder/client/packages/lowcoder-cli/config/modules.js
Normal 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();
|
||||||
57
lowcoder/client/packages/lowcoder-cli/config/paths.js
Normal file
57
lowcoder/client/packages/lowcoder-cli/config/paths.js
Normal 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;
|
||||||
67
lowcoder/client/packages/lowcoder-cli/config/vite.config.js
Normal file
67
lowcoder/client/packages/lowcoder-cli/config/vite.config.js
Normal 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),
|
||||||
|
});
|
||||||
58
lowcoder/client/packages/lowcoder-cli/dev-utils/buildVars.js
Normal file
58
lowcoder/client/packages/lowcoder-cli/dev-utils/buildVars.js
Normal 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: "",
|
||||||
|
},
|
||||||
|
];
|
||||||
102
lowcoder/client/packages/lowcoder-cli/dev-utils/external.js
Normal file
102
lowcoder/client/packages/lowcoder-cli/dev-utils/external.js
Normal 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");
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
28
lowcoder/client/packages/lowcoder-cli/dev-utils/util.js
Normal file
28
lowcoder/client/packages/lowcoder-cli/dev-utils/util.js
Normal 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));
|
||||||
|
}
|
||||||
8
lowcoder/client/packages/lowcoder-cli/global.d.ts
vendored
Normal file
8
lowcoder/client/packages/lowcoder-cli/global.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
printPerf: () => void;
|
||||||
|
__LOWCODER_ORG__?: {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
28
lowcoder/client/packages/lowcoder-cli/index.js
Executable file
28
lowcoder/client/packages/lowcoder-cli/index.js
Executable 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();
|
||||||
49
lowcoder/client/packages/lowcoder-cli/package.json
Normal file
49
lowcoder/client/packages/lowcoder-cli/package.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
30
lowcoder/client/packages/lowcoder-cli/util/log.js
Normal file
30
lowcoder/client/packages/lowcoder-cli/util/log.js
Normal 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.`);
|
||||||
|
};
|
||||||
4
lowcoder/client/packages/lowcoder-comps/.gitignore
vendored
Normal file
4
lowcoder/client/packages/lowcoder-comps/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/lib
|
||||||
|
/node_modules
|
||||||
|
/package
|
||||||
|
*.tgz
|
||||||
120
lowcoder/client/packages/lowcoder-comps/README.md
Normal file
120
lowcoder/client/packages/lowcoder-comps/README.md
Normal 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
|
||||||
15
lowcoder/client/packages/lowcoder-comps/icons/icon-chart.svg
Normal file
15
lowcoder/client/packages/lowcoder-comps/icons/icon-chart.svg
Normal 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 |
@@ -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 |
@@ -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 |
26
lowcoder/client/packages/lowcoder-comps/index.html
Normal file
26
lowcoder/client/packages/lowcoder-comps/index.html
Normal 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>
|
||||||
21
lowcoder/client/packages/lowcoder-comps/index.tsx
Normal file
21
lowcoder/client/packages/lowcoder-comps/index.tsx
Normal 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 />);
|
||||||
6
lowcoder/client/packages/lowcoder-comps/jest.config.js
Normal file
6
lowcoder/client/packages/lowcoder-comps/jest.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import config from "../../config/test/jest.config.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...config,
|
||||||
|
setupFiles: [...config.setupFiles],
|
||||||
|
};
|
||||||
268
lowcoder/client/packages/lowcoder-comps/package.json
Normal file
268
lowcoder/client/packages/lowcoder-comps/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
3
lowcoder/client/packages/lowcoder-comps/src/app-env.d.ts
vendored
Normal file
3
lowcoder/client/packages/lowcoder-comps/src/app-env.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="lowcoder-cli/client" />
|
||||||
|
|
||||||
|
declare module "lowcoder-sdk";
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
@@ -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,
|
||||||
|
]);
|
||||||
@@ -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,
|
||||||
|
]);
|
||||||
@@ -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,
|
||||||
|
]);
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -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>;
|
||||||
@@ -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())}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
})();
|
||||||
@@ -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();
|
||||||
|
})();
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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`;
|
||||||
@@ -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();
|
||||||
|
})();
|
||||||
@@ -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();
|
||||||
|
})();
|
||||||
@@ -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();
|
||||||
|
})();
|
||||||
@@ -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();
|
||||||
|
})();
|
||||||
@@ -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();
|
||||||
|
})();
|
||||||
@@ -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
Reference in New Issue
Block a user