diff --git a/.babelrc b/.babelrc index b0b9a96e..eaf32387 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,3 @@ { - "stage": 0 + "presets": ["es2015", "stage-0"] } diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..be540e11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +npm-debug.log +test \ No newline at end of file diff --git a/.npmignore b/.npmignore index 84603ad4..81dc7674 100644 --- a/.npmignore +++ b/.npmignore @@ -1,9 +1,11 @@ +.git +.gitignore .idea build-conf -images +docs node_modules -src -test-project -.babelrc -.npmignore +src-client +src-server +test structor.iml +structor-logo-big.png diff --git a/LICENSE b/LICENSE index 733c0723..d6456956 100644 --- a/LICENSE +++ b/LICENSE @@ -1,675 +1,202 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is 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. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - 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. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - 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 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. Use with the GNU Affero General Public License. - - 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 Affero 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 special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU 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 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 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 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 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - {project} Copyright (C) {year} {fullname} - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - 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 GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index df544379..344373b1 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,41 @@ -Structor - a user interface builder for React ----------- - +[![npm version](https://img.shields.io/npm/v/structor.svg?style=flat)](https://www.npmjs.com/package/structor) +[![npm downloads](https://img.shields.io/npm/dt/structor.svg?style=flat)](https://www.npmjs.com/package/structor) -### Description -Structor is former React UI Builder. If you are not familiar with React UI Builder you can watch the video here: [https://www.youtube.com/watch?v=5nqOFSjXKPI](https://www.youtube.com/watch?v=5nqOFSjXKPI). +

+ +

-But, it is better to read the following description and Wiki docs here because the builder has absolutely redesigned look and feel and has a lot of new features. +

Structor - React UI Builder

+

Install Components  >  Prototype UI  >  Generate Source Code  >  Build Application

-First of all, I need to mention that Structor is a visual development environment for node.js Web applications with React UI. +
+
-The essential part of the builder is a project boilerplate. The boilerplate is a prepacked source code of node.js application in which metainfo included. Using this info Structor knows how to use components included into the package. There is a place where you can download boilerplates - a Structor Market [http://helmetrex.com](http://helmetrex.com). +* **Library** - install presets of components from [Structor Market](https://github.com/ipselon/structor-market). +* **Prototyping** - construct React components of any complexity right on the page. +* **Scaffolding** - generate scaffolds for React components, Redux containers/reducers/selectors and sagas. +* **Playground** - develop UI components in isolation from the application using a separate Webpack Dev server with hot-reloading. -It absolutely doesn't mean that you has to strictly follow the rules by which the boilerplate was designed and change the development process you used to follow. Each project is completely hackable and you can change almost everything. For example, if you don't want to use Redux or React Bootstrap in the project you may remove them from the source code. The builder is only the environment which uses metainfo of the project and acts as it was prescripted. +### Requirements +* NodeJS > 5 +* Webpack > 2.2.x +* React > 15.5.4 -More about the structure of prepack and how it is used by Structor please read the description of the sample tutorial project here [http://helmetrex.com/details/1567](http://helmetrex.com/details/1567); +### [Structor's User Guide](https://github.com/ipselon/structor/blob/master/docs/README.md) -The builder runs as HTTP server with ```webpack-dev-middleware``` + ```webpack-hot-middleware``` + ```react-transform-hmr``` inside. -So, the builder can be used as an HTTP server with all hot reloading capabilities from the box. +### Ask Questions -Switching between two modes: edit mode and live-preview mode gives a feeling as if you are creating the Web app right in the browser. Of course, the source code you can edit in your favorite IDE or text editor and don't worry about page reloading (maybe in rear cases). +[![facebook group](https://img.shields.io/badge/facebook%20group-join-blue.svg?style=social)](https://www.facebook.com/groups/structor/) +[![Join the chat at https://gitter.im/ipselon/structor](https://badges.gitter.im/ipselon/structor.svg)](https://gitter.im/ipselon/structor?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Apparently, we can admit that this tool having such features can be used not only in starter prototyping phase of development process, but used during all development process instead of HTTP backend server for Web app. +
-### Getting started +#### Created By +[Alex Pustovalov](https://twitter.com/alex_pustovalov) -Install Structor in global scope: -``` -npm install structor -g -``` - -Then you have two ways to start working in the builder. - -The first way: -* Create an empty folder on local machine. -* Enter in this folder and run command: ```structor```. -* Open the browser and enter the address: ```http://localhost:2222/structor```. -* Choose suitable prepack (the only one so far) and click clone option. -* Start composing UI... - -The second way: -* Go to Structor Market [http://helmetrex.com](http://helmetrex.com), choose suitable boilerplate (the only one so far is there). -* Download package on localhost and unpack it in some empty folder. -* Enter into this folder and run ```npm install``` command. -* Once installation is finished run ```structor```. -* Open the browser and enter the address: ```http://localhost:2222/structor```. -* Start composing UI... - -The next time you want to open project in Structor, just go to the folder where project is and run ```structor```. - - -### Documentation - -Please read Wiki docs here in order to get familiar with Structor interface and its features. - -Also, if you find any mistake in the text please kindly fix it. Thank you. +#### Sponsors +Many thanks to Vlad Magdalin ([@callmevlad](https://twitter.com/callmevlad)) for his support on [Patreon](https://www.patreon.com/ipselon) #### License -GNU GENERAL PUBLIC LICENSE Version 3 +Apache License, Version 2.0 (Apache-2.0) diff --git a/build-conf/gulpfile.js b/build-conf/gulpfile.js index 5535981f..360958a3 100644 --- a/build-conf/gulpfile.js +++ b/build-conf/gulpfile.js @@ -4,42 +4,47 @@ var gulp = require('gulp'), autoprefixer = require('gulp-autoprefixer'), babel = require('gulp-babel'); uglify = require('gulp-uglify'); - watch = require('gulp-watch'); + path = require('path') + // watch = require('gulp-watch'); gulp.task('default', function() { // place code for your default task here }); +gulp.task('clean-server-dev', function(){ + return del([path.resolve('../test/node_modules/structor/server')], {force: true}); +}); + gulp.task('clean-server', function(){ return del(['../server'], {force: true}); }); -gulp.task('build-server-dev', ['clean-server'], function() { - return gulp.src('../src/server/src/**/*.js') - .pipe(watch('../src/server/src/**/*.js')) +gulp.task('build-server-dev', ['clean-server-dev'], function() { + return gulp.src('../src-server/**/*.js') .pipe(babel()) - .pipe(gulp.dest('../server')); + .pipe(gulp.dest(path.resolve('../test/node_modules/structor/server'))); }); gulp.task('build-server', ['clean-server'], function() { - return gulp.src('../src/server/src/**/*.js') + return gulp.src('../src-server/**/*.js') .pipe(babel()) .pipe(uglify()) .pipe(gulp.dest('../server')); }); -gulp.task('clean-client-less', function() { - return del(['../src/client/lib/bootstrap/css/custom'], {force: true}); -}); - -gulp.task('build-client-less', ['clean-client-less'], function() { - var config = { - src: '../src/client/lib/bootstrap/less/bootstrap.less', - dest: '../src/client/lib/bootstrap/css/custom' - }; - return gulp.src(config.src) - .pipe(less()) - .pipe(autoprefixer({cascade: false, browsers: ['last 2 versions']})) - .pipe(gulp.dest(config.dest, {overwrite: true})); -}); +// +// gulp.task('clean-client-less', function() { +// return del(['../src/client/lib/bootstrap/css/custom'], {force: true}); +// }); +// +// gulp.task('build-client-less', ['clean-client-less'], function() { +// var config = { +// src: '../src/client/lib/bootstrap/less/bootstrap.less', +// dest: '../src/client/lib/bootstrap/css/custom' +// }; +// return gulp.src(config.src) +// .pipe(less()) +// .pipe(autoprefixer({cascade: false, browsers: ['last 2 versions']})) +// .pipe(gulp.dest(config.dest, {overwrite: true})); +// }); diff --git a/build-conf/webpack-dev.config.js b/build-conf/webpack-dev.config.js index 465ccb2a..2f609051 100644 --- a/build-conf/webpack-dev.config.js +++ b/build-conf/webpack-dev.config.js @@ -1,32 +1,64 @@ -var webpack = require("webpack"); -var ExtractTextPlugin = require("extract-text-webpack-plugin"); +var webpack = require("webpack"), + ExtractTextPlugin = require("extract-text-webpack-plugin"), + path = require('path') + module.exports = [ { name: "browser", entry: { - main: './src/client/src/main.js', - docs: './src/client/src/docs.js' + main: './src-client/main.js' }, output: { - path: './static', + path: path.resolve('test/node_modules/structor/static'), filename: '[name].js' }, devtool: 'inline-source-map', - debug: true, module: { loaders: [ - { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }, - { test: /\.css$/, exclude: /node_modules/, loader: ExtractTextPlugin.extract("style-loader", "css-loader") }, - { test: /\.(eot|woff|woff2|ttf|svg|png|jpg|gif)([\?]?.*)$/, exclude: /node_modules/, loader: 'url-loader' } + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader', + query: { + presets: [ + [ + "es2015", + { + "modules": false + } + ], + "react", + "stage-0" + ] + } + }, + { + test: /\.css$/, + exclude: /node_modules/, + loader: ExtractTextPlugin.extract({fallbackLoader: 'style-loader', loader: 'css-loader'}) + }, + { + test: /\.(eot|woff|woff2|ttf|svg|png|jpg|gif)([\?]?.*)$/, + exclude: /node_modules/, + loader: 'url-loader' + } ] }, plugins: [ - new ExtractTextPlugin("styles.css") + new ExtractTextPlugin({filename: "styles.css"}) ], externals: { "jquery": "jQuery" - } + }, + resolve: { + modules: ['src-client', 'node_modules'], + extensions: [ + '.js', + '.jsx', + '.react.js', + ], + }, } ]; diff --git a/build-conf/webpack.config.js b/build-conf/webpack.config.js index 49660a99..53b7f870 100644 --- a/build-conf/webpack.config.js +++ b/build-conf/webpack.config.js @@ -1,3 +1,4 @@ +var path = require("path"); var webpack = require("webpack"); var ExtractTextPlugin = require("extract-text-webpack-plugin"); @@ -5,39 +6,67 @@ module.exports = [ { name: "browser", entry: { - main: './src/client/src/main.js', - docs: './src/client/src/docs.js' + main: './src-client/main.js' }, output: { - path: './static', + path: path.resolve(__dirname, '../static'), filename: '[name].js' }, module: { loaders: [ { - test: /\.js$/, exclude: /node_modules/, loader: 'babel' + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader', + query: { + presets: [ + [ + "es2015", + { + "modules": false + } + ], + "react", + "stage-0" + ] + } }, - { test: /\.css$/, exclude: /node_modules/, loader: ExtractTextPlugin.extract("style-loader", "css-loader") }, - { test: /\.(eot|woff|woff2|ttf|svg|png|jpg|gif)([\?]?.*)$/, exclude: /node_modules/, loader: 'url-loader' } + { + test: /\.css$/, + exclude: /node_modules/, + loader: ExtractTextPlugin.extract({fallbackLoader: 'style-loader', loader: 'css-loader'}) + }, + { + test: /\.(eot|woff|woff2|ttf|svg|png|jpg|gif)([\?]?.*)$/, + exclude: /node_modules/, + loader: 'url-loader' + } ] }, plugins: [ - new ExtractTextPlugin("styles.css"), - new webpack.optimize.DedupePlugin(), + new webpack.DefinePlugin({ + "process.env": { + NODE_ENV: JSON.stringify("production") + } + }), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }), - new webpack.DefinePlugin({ - "process.env": { - NODE_ENV: JSON.stringify("production") - } - }) + new ExtractTextPlugin({filename: "styles.css"}) ], externals: { "jquery": "jQuery" - } + }, + resolve: { + modules: ['src-client', 'node_modules'], + extensions: [ + '.js', + '.jsx', + '.react.js', + ], + }, } ]; diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..5f79f9e7 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,351 @@ +# Structor's User Guide + +* [Getting Started](https://github.com/ipselon/structor/blob/master/docs#getting-started) +* [Integrating with Create React App](https://github.com/ipselon/structor/blob/master/docs/integrating-with-create-react-app.md) +* [Designing UI](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#designing-ui) + * [A component model](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#a-component-model) + * [Combine and modify component models](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#combine-and-modify-component-models) + * [How to select a component](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-select-a-component) + * [How to paste new component from library](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-paste-new-component-from-library) + * [How to cut, copy and paste components](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-cut-copy-and-paste-components) + * [Navigation through pages](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#navigation-through-pages) +* [Working with code](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#working-with-code) + * [Dev environment](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#dev-environment) + * [The code structure](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#the-code-structure) + * [Component library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#component-library) + * [React components in library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#react-components-in-library) + * [Redux containers in library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#redux-containers-in-library) + * [Component default models](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#component-default-models) + * [Generating the source code](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#generating-the-source-code) + * [React component scaffold](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#react-component-scaffold) + * [Redux container scaffold](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#redux-container-scaffold) + * [Troubleshooting](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#troubleshooting) +* [Structor Market](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#structor-market) + * [Extracting and publishing components presets](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#extracting-and-publishing-components-presets) + * [Extending existing presets on the market](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#extending-existing-presets-on-the-market) +* Exporting + * Export pages + * Export application + +## Getting started + +Structor does not depend on any starter/seed/boilerplate project. It runs even in an empty folder with `package.json`. + +But before you get familiar with Structor we suggest to run through a brief tutorial. +This tutorial is using our starter project which lets you quickly try all cool features of Structor. + +### Installation + +* Clone or download the repo `structor-starter` from GitHub here: + +> [https://github.com/ipselon/structor-starter](https://github.com/ipselon/structor-starter). + +* Run command from cloned or unpacked directory: +``` +npm install +``` +* Run command: +``` +npm install structor +``` +* When the prompt is appeared in the command line, choose `app` as a directory where the source code will be located. +``` +Specify a directory name where the source code of the generated components will be. +If the directory does not exist in the current project structure it will be created. +(app) +``` +* From this point you are able to start Structor: +``` +npm run structor +``` + +* Open the following address in the browser: +``` +http://localhost:2222/structor +``` +Now you should see the Structor's workspace. If not, please create an issue. + +

+ +

+ +### Getting familiar with Structor's Workspace + +After the first opening you'll see the workspace page. We may call it a page bacause it behaves as the real page in the browser. But there are two modes in the workspace. + +The first mode is an editing mode. In the editing mode we can cut, paste, duplicate, delete, replace, change style of any component on the page. + +> __On a separate note__, we should understand what is a component and component model in terms of the Structor's workspace. The page in the workspace consists of multiple React components. Although, the page is not a React component itself, as you may thought, the page is just a tree of components models which are written in JSON. See detailed explanation of what is a component model in section [A component model](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#a-component-model) of this guide. + +The second mode is a preview mode. In the preview mode you are able to see how page looks and try how it works in the browser. + +On the left vertical toolbar you may find two buttons which are responsible for switching between modes: + +

+ +

+ +On this stage of the tutorial the current page should be in the edit mode. If not, please set the edit mode on. + +### Installing component presets + +However we can work in Structor with only those components which are shipped with Structor distribution, in this tutorial we will install a package of [Material UI](https://github.com/ipselon/mui-next-spkg) components from [Structor Market](https://github.com/ipselon/structor-market). + +Activate a library button with plus icon as it is shown on the screenshot below. The library panel shows the groups of available components in the current project. Find the `Install` button at the top of the left-side panel and click on it. + +

+ +

+ +We are forwarded to the Structor Market gallery, here we can see what packages of component presets are avaialble to install. Let's install a `mui-next-spkg` package, which includes components from the next version of Material UI lib. + +

+ +

+ +Installation process will take some time because Structor installs dependencies via npm. Once package is installed we will see new pages in the workspace and new groups of components. Just try to open other pages using the page selector component in the top toolbar. + +

+ +

+ +### Composing a new component + +Let's start to compose something interesting. Make sure that the current page is `/home` page and click somewhere on the text in the center of the page. You'll see that a border appeared around some area - it means that you selected a component. There is a few ways to understand what the component is selected now. + +We may find it in a breadcrumbs control at the top of the workspace as it is shown on the screenshot below. Or you can take a look at the context menu which appeared at the place you clicked right mouse button. + +

+ +

+ +Additionally, there is a more common method which will show us the entire structure of the page. There is a bottom panel with the components tree on the current page. Click on the button with code icon on the left vertical toolbar as it is shown on the screenshot below. + +

+ +

+ +> __Note__ Moving mouse cursor around on the page will show us a boundaries of components which we are able to select. + +Now let's place the some components from library onto the page. + +There is a requirement in the Material UI lib to place `MuiThemeProvider` as a root component of any MUI components combination. So, we can find a `ThemeProvider` component in the `MUI` group (this is a wrapper of `MuiThemeProvider` here) and place it as a container for our future combination. + +Just open the `MUI` group in the library panel and click on `ThemeProvider` item in the list. We will see that there is a sublist. The sublist in the library shows that this component has several models. Let's click on `default` model and see that `ThemeProvider` component's model was copied to clipboard. + +Now we have to replace `h3` component in the tree with the component from clipboard. If you do not have `h3` selected, select it by clicking on it on the page or just select on the treeview (or in the breadcrumbs control). + +

+ +

+ +Then find `Replace` button in the top toolbar, or click right mouse and hit a `Replace` button in the context menu. + +

+ +

+ +As the result we will have the composition as it is shown on the screenshot below. + +

+ +

+ +Now replace a `Placeholder` component on the page with `raised primary` model of `Button` component. And then place `h2` component (from __HTML__ group) before `Button` inside of `LayoutItem` component. + +Placing a component before or after another component can be provided in the same manner as we were replacing components - you have to pick a component from the library and then use `Before` or `After` button in the top toolbar or context menu for selected component. + +In the result we should have the following composition: + +

+ +

+ +### Creating the component + +Structor ships with a couple source code generators for React component scaffolds. We are not going to discuss here how it works and how it can be customized. Although, please keep in mind that any scaffold template may be changed to fit your requirements easily. + +Now we are going to generate a scaffold for simple React component which will have equal look and feel as our composition which we made above. + +Select the topmost `div` in the hierarchy of the current page, and click `Generate Component` button on the top toolbar of the workspace. We will see the first page of the generators gallery, where we have to choose what component we want to generate. + +> __Note__: You may choose any component on the page as a root node to generate a new React component. But in this tutorial we will select the topmost one. + +Here we will choose a `react-component` generator - click on `Generate` button to start a generation wizard. + +

+ +

+ +The next step requires to enter a name of new component and/or a namespace for component. + +

+ +

+ +On this note we should step aside a bit and try to understand what the source code and its structure we will have after generation. The only requirement from Structor is to use a certain type of the source code structure. We will discuss the structure and motives to use it in the [The code structure](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#the-code-structure) guide's chapters. But on this step please keep in mind the following structure of components which is possible to use with Structor: + +``` + + components/ + + index.js - React component + containers/ + + index.js - Redux container + actions.js + constants.js + reducer.js + sagas.js + selectors.js + modules/ + / + components/ + + index.js - React component + containers/ + + index.js - Redux container + actions.js + constants.js + reducer.js + sagas.js + selectors.js + index.js - components and containers index + reducer.js - composition of containers' reducers + sagas.js - composition of containers' sagas +``` + +Enter `Counter` as a new name of React component, and enter `Tutorial` as a namespace. We are going to create a new namespace with `Counter` component. Although, we can create a component without any namespace and that will put it in `components` folder. + +Next step gives us options for a few variants of the future scaffold. Let's leave the options as they are and proceed to the next step. + +Here is a preview of the generated source code. After reviewing just click on `Install component` button. + +

+ +

+ + +Now we have to switch to the preview mode and try to click on the button - nothing happened. That is why we need to change the source code to make it something valuable. + +Open `index.js` file in the directory... +``` +/app/modules/Tutorial/components/Counter +``` + +...and made chages in generated code as it is shown below. Also, please observe that your changes are applied in the preview page almost immediatelly. + +```diff +/** +* +* Counter +* +*/ + +import React, { Component } from "react"; + +import PropTypes from "prop-types"; + +import { + ThemeProvider, + LayoutContainer, + LayoutItem, + Button +} from "modules/MUI"; + +const style = { + display: "flex", + justifyContent: "center", + alignItems: "center", + padding: "1em" +}; + +class Counter extends Component { + constructor(props) { + super(props); +++ this.handleIncrease = this.handleIncrease.bind(this); + this.state = { +-- exampleValue: "" +++ counterValue: 0, + }; + } + +++ handleIncrease(e) { +++ e.stopPropagation(); +++ e.preventDefault(); +++ this.setState({counterValue: this.state.counterValue + 1}); +++ } + + render() { +-- const { exampleValue } = this.state; // eslint-disable-line +-- const { exampleProp } = this.props; // eslint-disable-line +++ const {counterValue} = this.state; // eslint-disable-line + return ( +
+ +
+ + + +--

Empty h2

+++

Count: {counterValue}

+-- +
+
+
+
+ +
+ ); + } +} + +-- Counter.propTypes = { +-- exampleProp: PropTypes.string +-- }; +-- Counter.defaultProps = { +-- exampleProp: "" +-- }; + +export default Counter; +``` + +Switch again to the preview mode and try to click on the button - you will see that the counter value is increasing. + +

+ +

+ +### Exporting pages and building App + +This step is not necessary to the projects which have own building process and requre to add components manually into appliation pages. Structor Starter App does not have a sophisticated app building process in terms of granularity, optimisation, etc. This step is mostly for the learning purpose. + +Click on the button with book icon on on the left vertical toolbar. This will show us a list of available pages. + +In the top of the list we may see two buttons: `Export Pages` and `Export App`. `Export Pages` responsible for generating the source code for selected pages. And `Export App` button generates not only the pages' source code, but also the source code for entry point in terms of Webpack compiler. + +

+ +

+ +As far as we are going to build application, we are choosing `Export App`. The modal dialog will worn us about what files will be generated. + +After we will see a green message about a successful exporting. Now we can find a new `/routes` folder along with files for React/Redux application. Additionally, please find the compiled bundle of the application in `/build` directory. + +Finally we can run our application in the local Express server, go to the terminal and run following command: +``` +npm run start:production +``` + +This command initiates a building process of our application and starts an Express server instance on `3000` port. After the server is started we may open `http://localhost:3000` address in our browser. + + + diff --git a/docs/designing-ui.md b/docs/designing-ui.md new file mode 100644 index 00000000..45be53a7 --- /dev/null +++ b/docs/designing-ui.md @@ -0,0 +1,221 @@ +# Structor's User Guide + +* [Getting Started](https://github.com/ipselon/structor/blob/master/docs#getting-started) +* [Integrating with Create React App](https://github.com/ipselon/structor/blob/master/docs/integrating-with-create-react-app.md) +* [Designing UI](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#designing-ui) + * [A component model](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#a-component-model) + * [Combine and modify component models](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#combine-and-modify-component-models) + * [How to select a component](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-select-a-component) + * [How to paste new component from library](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-paste-new-component-from-library) + * [How to cut, copy and paste components](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-cut-copy-and-paste-components) + * [Navigation through pages](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#navigation-through-pages) +* [Working with code](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#working-with-code) + * [Dev environment](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#dev-environment) + * [The code structure](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#the-code-structure) + * [Component library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#component-library) + * [React components in library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#react-components-in-library) + * [Redux containers in library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#redux-containers-in-library) + * [Component default models](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#component-default-models) + * [Generating the source code](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#generating-the-source-code) + * [React component scaffold](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#react-component-scaffold) + * [Redux container scaffold](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#redux-container-scaffold) + * [Troubleshooting](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#troubleshooting) +* [Structor Market](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#structor-market) + * [Extracting and publishing components presets](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#extracting-and-publishing-components-presets) + * [Extending existing presets on the market](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#extending-existing-presets-on-the-market) +* Exporting + * Export pages + * Export application + +## Designing UI + +### A component model + +A component model is a metadata used by Structor, it tells +how to render a React component and what properties should be applied to the component. +Each model is written in JSON format and have a pretty simple structure: + +```json5 + { + "type": "Link", + "props": { + "to": "/main" + }, + "children": [ + { + "type": "span", + "text": "go to /main" + } + ] + } +``` + +Where `type` tells which React component should be rendered, `props` - what properties will be applied, +`children` - an array of nested components. + +As you may already guessed, placing other models into `children` array gives us a tree of components +that is rendered on the page. +Adding, removing, replacing models in the tree can be done right in the workspace. + +___Note___ You have to select a component on the page before starting any modification of the model. + +In practice, a selecting of some component on the page is the selecting of a model in the tree. +Structor lets us modify any selected model through the tools in the workspace, +such as: `Component Options` dialog, `Quick Style` panel. + +For example, select a component on the page. +Then click again on selected component, now you can see and modify `props` in a `Component Options` dialog. +Also, we can open this dialog by clicking on `Edit` button on the top toolbar. +Find a JSON editor in `Properties` tab in the dialog. + +

+ +

+ +Another way to add or change property in the model is to use the `Quick Style` panel. +Click on the button with brush icon on the left vertical toolbar. Go to the `Props` tab in the appeared panel. +There you can add or change properties one by one. + +

+ +

+ +Once we changed the properties in the model, Structor applies immediately these changes on the page. +It does not require any extra compilation or rebuilding of the page. + +A different trees of models are encapsulated into pages in terms of Structor's workspace. +Structor can hold many pages, and their models are placed in one JSON file. +We may find this file in Structor's meta folder here: `.structor/desk/model.json`; + +### Combine and modify component models + +Structor's workspace lets us combine any amount of models on the page. +It is suggested to use several ways to combine and modify a page model. +First of all, we have to select a component on the page - this selection should serve as a starting point of a combination. + +#### How to select a component + +Structor has a few ways to select a component on the page. + +The first, we may see the tree structure of the page by clicking on the button with code sign icon in the left vertical toolbar. + +Here we can: +* see properties applied to the component, +* edit text right in place, +* copy, paste, delete, move, replace and etc. + +The second, we can see how components are nested in `Breadcrumbs` control on the top toolbar. +Here we can select any component in the path by clicking on it. + +

+ +

+ +And of course, we can just click on the component right on the page. +Useful: the hovering over a component shows the boundaries of the component and its name. +That helps find components on the page quicker. + +#### How to paste new component from library + +Where we can find components which can be placed onto the page? There is a library of components. +Literally, this is a list of models and each of the model can be pasted into the page's tree. + +Activate the plus button on the left vertical toolbar. +Now we can see a panel with groups of components on the left side of the workspace. +Clicking on the group will show its content. + +Initially we can see only two groups of components: `HTML` and `Components`. +`HTML` group includes all HTML components, and `Components` group includes `Link` and `IndexLink` components +from React Router. + +_Please read in "The code structure" chapter about how components appear in Structor's library, +and how to add own components into the library._ + +By clicking on an item in the group we are copying the component model into a clipboard. + +___Clipboard___ is a buffer for component models. +Find a clipboard control on the top toolbar where you can see what components are in the clipboard at this moment. + +

+ +

+ +Now we can paste model from clipboard into the page as a sibling or as a child of any selected component, +or replace it with the clipboard content. +Just find controls `Before`, `First`, `Last`, `After`, `Replace` on the following screenshot: + +

+ +

+ +Also, here is a convenient way to place a component in the hierarchy - +a small circle placeholders in page's tree view, clicking by which we can place there a new component. + +

+ +

+ +Additionally, there is one quick way to add new component on the page: +* Make sure that the clipboard is empty (if not just click on close icon on the clipboard control in the top toolbar) +* Select the component on the page +* Click on one of pasting buttons: `Before`, `First`, `Last`, `After`, `Replace` or on placeholders in the tree view. +* Type the name of component in the appeared dialog. And click `Submit`. + +

+ +

+ +**Important** You are able to add new models to the component in the library. +Just select a component you want to have a new model +(for example, you changed style property in certain model and want to save it as a template), +and click on `Save Model` button on the top toolbar. That will lets you to save selected model as different model. +This is very helpful in case you want to share your library models on Structor Market. +Please read about how to extract the library in "Namespaces" chapter. + +

+ +

+ +#### How to cut, copy and paste components + +As well as we copied components from library we are able to cut or copy any component on the page into the clipboard. +That will allow us to paste component from clipboard in any other place. + +Find highlighted buttons, which cut and copy components into the clipboard, on the following screenshot: + +

+ +

+ +Additionally, Structor lets select, copy, cut and paste multiple components on the page. +To select multiple components we have to press `Ctrl` or `Command` and click on the component. +Once you selected many components you see that in the top toolbar instead of `Breadcrumbs` control. + +Then you can just click on copy or cut button on the top toolbar. +All selected component models will be copied into clipboard. +Clipboard is able to hold not only a single component's model, but multiple models as well. + +And now you may paste the clipboard content somewhere else. + +

+ +

+ +### Navigation through pages + +As you already know, the workspace can have many pages. +Each page in the workspace has own address, and we can navigate between pages. +There is a panel with pages list, where we can switch the current page to another one. + +Also, there is a page control on the top toolbar where we can add new pages, change the width of page's viewport, +and delete the current page. + +

+ +

+ +As far as the page has the address, we can link it to another page and navigate between pages right in the preview mode. +Place a `Link` component from React Router somewhere on one page. +Then open `Props` tab in `Quick Style` panel, and set `to` property value to address of another page. +Now you may switch workspace to the preview mode and try to navigate to another page by the link. + diff --git a/docs/img/design-ui-copy-paste-controls.png b/docs/img/design-ui-copy-paste-controls.png new file mode 100644 index 00000000..6d5bc780 Binary files /dev/null and b/docs/img/design-ui-copy-paste-controls.png differ diff --git a/docs/img/design-ui-library-panel.png b/docs/img/design-ui-library-panel.png new file mode 100644 index 00000000..e66ea411 Binary files /dev/null and b/docs/img/design-ui-library-panel.png differ diff --git a/docs/img/design-ui-multiple-selection.png b/docs/img/design-ui-multiple-selection.png new file mode 100644 index 00000000..b1ec6bc8 Binary files /dev/null and b/docs/img/design-ui-multiple-selection.png differ diff --git a/docs/img/design-ui-options-dialog.png b/docs/img/design-ui-options-dialog.png new file mode 100644 index 00000000..8ff9b230 Binary files /dev/null and b/docs/img/design-ui-options-dialog.png differ diff --git a/docs/img/design-ui-page-controls.png b/docs/img/design-ui-page-controls.png new file mode 100644 index 00000000..6ce7e149 Binary files /dev/null and b/docs/img/design-ui-page-controls.png differ diff --git a/docs/img/design-ui-paste-controls.png b/docs/img/design-ui-paste-controls.png new file mode 100644 index 00000000..0f741939 Binary files /dev/null and b/docs/img/design-ui-paste-controls.png differ diff --git a/docs/img/design-ui-quick-add-dialog.png b/docs/img/design-ui-quick-add-dialog.png new file mode 100644 index 00000000..e4738b04 Binary files /dev/null and b/docs/img/design-ui-quick-add-dialog.png differ diff --git a/docs/img/design-ui-quick-style-panel.png b/docs/img/design-ui-quick-style-panel.png new file mode 100644 index 00000000..b92c8a98 Binary files /dev/null and b/docs/img/design-ui-quick-style-panel.png differ diff --git a/docs/img/design-ui-savemodel-dialog.png b/docs/img/design-ui-savemodel-dialog.png new file mode 100644 index 00000000..151d41ee Binary files /dev/null and b/docs/img/design-ui-savemodel-dialog.png differ diff --git a/docs/img/design-ui-treeview-panel-placeholders.png b/docs/img/design-ui-treeview-panel-placeholders.png new file mode 100644 index 00000000..9c34b48d Binary files /dev/null and b/docs/img/design-ui-treeview-panel-placeholders.png differ diff --git a/docs/img/design-ui-treeview-panel.png b/docs/img/design-ui-treeview-panel.png new file mode 100644 index 00000000..a5b7c0c3 Binary files /dev/null and b/docs/img/design-ui-treeview-panel.png differ diff --git a/docs/img/export-page-list-panel.png b/docs/img/export-page-list-panel.png new file mode 100644 index 00000000..31751609 Binary files /dev/null and b/docs/img/export-page-list-panel.png differ diff --git a/docs/img/first-tutorial-add-component.png b/docs/img/first-tutorial-add-component.png new file mode 100644 index 00000000..2a0c1619 Binary files /dev/null and b/docs/img/first-tutorial-add-component.png differ diff --git a/docs/img/first-tutorial-breadcrumbs.png b/docs/img/first-tutorial-breadcrumbs.png new file mode 100644 index 00000000..db09b453 Binary files /dev/null and b/docs/img/first-tutorial-breadcrumbs.png differ diff --git a/docs/img/first-tutorial-component-preview.png b/docs/img/first-tutorial-component-preview.png new file mode 100644 index 00000000..87c0e656 Binary files /dev/null and b/docs/img/first-tutorial-component-preview.png differ diff --git a/docs/img/first-tutorial-component-replaced.png b/docs/img/first-tutorial-component-replaced.png new file mode 100644 index 00000000..763a0b0b Binary files /dev/null and b/docs/img/first-tutorial-component-replaced.png differ diff --git a/docs/img/first-tutorial-composed-ui.png b/docs/img/first-tutorial-composed-ui.png new file mode 100644 index 00000000..6fd3f05f Binary files /dev/null and b/docs/img/first-tutorial-composed-ui.png differ diff --git a/docs/img/first-tutorial-export-app.png b/docs/img/first-tutorial-export-app.png new file mode 100644 index 00000000..ef134bbd Binary files /dev/null and b/docs/img/first-tutorial-export-app.png differ diff --git a/docs/img/first-tutorial-generated-code.png b/docs/img/first-tutorial-generated-code.png new file mode 100644 index 00000000..8fafdc21 Binary files /dev/null and b/docs/img/first-tutorial-generated-code.png differ diff --git a/docs/img/first-tutorial-generators-list.png b/docs/img/first-tutorial-generators-list.png new file mode 100644 index 00000000..dbb07bb4 Binary files /dev/null and b/docs/img/first-tutorial-generators-list.png differ diff --git a/docs/img/first-tutorial-install-package.png b/docs/img/first-tutorial-install-package.png new file mode 100644 index 00000000..7cd2bf66 Binary files /dev/null and b/docs/img/first-tutorial-install-package.png differ diff --git a/docs/img/first-tutorial-open-library.png b/docs/img/first-tutorial-open-library.png new file mode 100644 index 00000000..c457a59c Binary files /dev/null and b/docs/img/first-tutorial-open-library.png differ diff --git a/docs/img/first-tutorial-page-switcher.png b/docs/img/first-tutorial-page-switcher.png new file mode 100644 index 00000000..9cb90859 Binary files /dev/null and b/docs/img/first-tutorial-page-switcher.png differ diff --git a/docs/img/first-tutorial-replace-buttons.png b/docs/img/first-tutorial-replace-buttons.png new file mode 100644 index 00000000..95b2561d Binary files /dev/null and b/docs/img/first-tutorial-replace-buttons.png differ diff --git a/docs/img/first-tutorial-treeview-panel.png b/docs/img/first-tutorial-treeview-panel.png new file mode 100644 index 00000000..89b6cbdc Binary files /dev/null and b/docs/img/first-tutorial-treeview-panel.png differ diff --git a/docs/img/generators-component-preview.png b/docs/img/generators-component-preview.png new file mode 100644 index 00000000..e95157b2 Binary files /dev/null and b/docs/img/generators-component-preview.png differ diff --git a/docs/img/generators-enter-name.png b/docs/img/generators-enter-name.png new file mode 100644 index 00000000..d7b6000f Binary files /dev/null and b/docs/img/generators-enter-name.png differ diff --git a/docs/img/generators-generate-component-btn.png b/docs/img/generators-generate-component-btn.png new file mode 100644 index 00000000..bb8b3db7 Binary files /dev/null and b/docs/img/generators-generate-component-btn.png differ diff --git a/docs/img/generators-list.png b/docs/img/generators-list.png new file mode 100644 index 00000000..3eacfbf6 Binary files /dev/null and b/docs/img/generators-list.png differ diff --git a/docs/img/generators-source-review.png b/docs/img/generators-source-review.png new file mode 100644 index 00000000..e836c9e8 Binary files /dev/null and b/docs/img/generators-source-review.png differ diff --git a/docs/img/integrate-with-cra-component-name.png b/docs/img/integrate-with-cra-component-name.png new file mode 100644 index 00000000..282e3743 Binary files /dev/null and b/docs/img/integrate-with-cra-component-name.png differ diff --git a/docs/img/integrate-with-cra-finish.png b/docs/img/integrate-with-cra-finish.png new file mode 100644 index 00000000..1a1b9d60 Binary files /dev/null and b/docs/img/integrate-with-cra-finish.png differ diff --git a/docs/img/integrate-with-cra-generators.png b/docs/img/integrate-with-cra-generators.png new file mode 100644 index 00000000..75042ac3 Binary files /dev/null and b/docs/img/integrate-with-cra-generators.png differ diff --git a/docs/img/integrate-with-cra-install-button.png b/docs/img/integrate-with-cra-install-button.png new file mode 100644 index 00000000..0444f0c0 Binary files /dev/null and b/docs/img/integrate-with-cra-install-button.png differ diff --git a/docs/img/integrate-with-cra-livepreview.png b/docs/img/integrate-with-cra-livepreview.png new file mode 100644 index 00000000..34ff23f5 Binary files /dev/null and b/docs/img/integrate-with-cra-livepreview.png differ diff --git a/docs/img/integrate-with-cra-market.png b/docs/img/integrate-with-cra-market.png new file mode 100644 index 00000000..6674ab74 Binary files /dev/null and b/docs/img/integrate-with-cra-market.png differ diff --git a/docs/img/integrate-with-cra-open-page.png b/docs/img/integrate-with-cra-open-page.png new file mode 100644 index 00000000..d8ffd7d6 Binary files /dev/null and b/docs/img/integrate-with-cra-open-page.png differ diff --git a/docs/img/integrate-with-cra-prompt.png b/docs/img/integrate-with-cra-prompt.png new file mode 100644 index 00000000..8ed2510e Binary files /dev/null and b/docs/img/integrate-with-cra-prompt.png differ diff --git a/docs/img/integrate-with-cra-treeview.png b/docs/img/integrate-with-cra-treeview.png new file mode 100644 index 00000000..b868011c Binary files /dev/null and b/docs/img/integrate-with-cra-treeview.png differ diff --git a/docs/img/source-code-component-preview.png b/docs/img/source-code-component-preview.png new file mode 100644 index 00000000..a2027e20 Binary files /dev/null and b/docs/img/source-code-component-preview.png differ diff --git a/docs/img/source-code-container-preview.png b/docs/img/source-code-container-preview.png new file mode 100644 index 00000000..8f43f3a4 Binary files /dev/null and b/docs/img/source-code-container-preview.png differ diff --git a/docs/img/source-code-enter-component-name.png b/docs/img/source-code-enter-component-name.png new file mode 100644 index 00000000..a00c95f9 Binary files /dev/null and b/docs/img/source-code-enter-component-name.png differ diff --git a/docs/img/source-code-generate-component-button.png b/docs/img/source-code-generate-component-button.png new file mode 100644 index 00000000..863e0bde Binary files /dev/null and b/docs/img/source-code-generate-component-button.png differ diff --git a/docs/img/source-code-generator-list.png b/docs/img/source-code-generator-list.png new file mode 100644 index 00000000..8f03daf0 Binary files /dev/null and b/docs/img/source-code-generator-list.png differ diff --git a/docs/img/source-code-generator-options.png b/docs/img/source-code-generator-options.png new file mode 100644 index 00000000..484350f6 Binary files /dev/null and b/docs/img/source-code-generator-options.png differ diff --git a/docs/img/source-code-library-shows-button.png b/docs/img/source-code-library-shows-button.png new file mode 100644 index 00000000..d49fe91e Binary files /dev/null and b/docs/img/source-code-library-shows-button.png differ diff --git a/docs/img/source-code-library-shows-container-1.png b/docs/img/source-code-library-shows-container-1.png new file mode 100644 index 00000000..e13c1dcc Binary files /dev/null and b/docs/img/source-code-library-shows-container-1.png differ diff --git a/docs/img/source-code-library-shows-container.png b/docs/img/source-code-library-shows-container.png new file mode 100644 index 00000000..395dc783 Binary files /dev/null and b/docs/img/source-code-library-shows-container.png differ diff --git a/docs/img/source-code-library-shows-new.png b/docs/img/source-code-library-shows-new.png new file mode 100644 index 00000000..f32ec2cd Binary files /dev/null and b/docs/img/source-code-library-shows-new.png differ diff --git a/docs/img/source-code-library-shows-two-buttons.png b/docs/img/source-code-library-shows-two-buttons.png new file mode 100644 index 00000000..0ac3f2c3 Binary files /dev/null and b/docs/img/source-code-library-shows-two-buttons.png differ diff --git a/docs/img/strcutor-market-install-from-dir-path.png b/docs/img/strcutor-market-install-from-dir-path.png new file mode 100644 index 00000000..ec1e7bad Binary files /dev/null and b/docs/img/strcutor-market-install-from-dir-path.png differ diff --git a/docs/img/structor-market-enter-dirpath.png b/docs/img/structor-market-enter-dirpath.png new file mode 100644 index 00000000..8f271b5a Binary files /dev/null and b/docs/img/structor-market-enter-dirpath.png differ diff --git a/docs/img/structor-market-extract-button.png b/docs/img/structor-market-extract-button.png new file mode 100644 index 00000000..84e93a8b Binary files /dev/null and b/docs/img/structor-market-extract-button.png differ diff --git a/docs/img/structor-market-install-from-dir.png b/docs/img/structor-market-install-from-dir.png new file mode 100644 index 00000000..65cd47c3 Binary files /dev/null and b/docs/img/structor-market-install-from-dir.png differ diff --git a/docs/img/structor-market-select-to-extract.png b/docs/img/structor-market-select-to-extract.png new file mode 100644 index 00000000..45037887 Binary files /dev/null and b/docs/img/structor-market-select-to-extract.png differ diff --git a/docs/img/structor-workspace-first-opening.png b/docs/img/structor-workspace-first-opening.png new file mode 100644 index 00000000..ee9a2cbb Binary files /dev/null and b/docs/img/structor-workspace-first-opening.png differ diff --git a/docs/img/structor-workspace-mode-switch-btns.png b/docs/img/structor-workspace-mode-switch-btns.png new file mode 100644 index 00000000..81c28b99 Binary files /dev/null and b/docs/img/structor-workspace-mode-switch-btns.png differ diff --git a/docs/integrating-with-create-react-app.md b/docs/integrating-with-create-react-app.md new file mode 100644 index 00000000..c2a7e778 --- /dev/null +++ b/docs/integrating-with-create-react-app.md @@ -0,0 +1,400 @@ +# Structor's User Guide + +* [Getting Started](https://github.com/ipselon/structor/blob/master/docs#getting-started) +* [Integrating with Create React App](https://github.com/ipselon/structor/blob/master/docs/integrating-with-create-react-app.md) +* [Designing UI](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#designing-ui) + * [A component model](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#a-component-model) + * [Combine and modify component models](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#combine-and-modify-component-models) + * [How to select a component](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-select-a-component) + * [How to paste new component from library](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-paste-new-component-from-library) + * [How to cut, copy and paste components](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-cut-copy-and-paste-components) + * [Navigation through pages](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#navigation-through-pages) +* [Working with code](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#working-with-code) + * [Dev environment](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#dev-environment) + * [The code structure](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#the-code-structure) + * [Component library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#component-library) + * [React components in library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#react-components-in-library) + * [Redux containers in library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#redux-containers-in-library) + * [Component default models](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#component-default-models) + * [Generating the source code](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#generating-the-source-code) + * [React component scaffold](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#react-component-scaffold) + * [Redux container scaffold](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#redux-container-scaffold) + * [Troubleshooting](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#troubleshooting) +* [Structor Market](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#structor-market) + * [Extracting and publishing components presets](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#extracting-and-publishing-components-presets) + * [Extending existing presets on the market](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#extending-existing-presets-on-the-market) +* Exporting + * Export pages + * Export application + +## Integration with Create React App + +In case you missed, Structor does not depend on any starter/seed/boilerplate project. +Instead it can be integrated with any other project. But there is a requirement to the structure of the source code. + +> **Note** Structor uses a self-contained structure for React/Redux components. +You may learn more about this structure from the [Working with code](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#working-with-code) chapter of the Structor's User Guide. +And if you are going to use the source code generators in Structor or install presets of components from Structor Market, +you should use the recommended source code structure. + +An integration point with a project is a root directory of the source code. +Webpack 2 in the Structor's guts wants to know where all files are and how they are related to each other. + +That's why Structor is asking you about the source root directory during the installation, +and then it puts the name of directory into `resolve.modules` setting in Structor's Webpack config. + +Having such information Webpack is able to resolve global paths in imports. +And we can instead of `../../components/Button` path use this `components/Button`. + +CRA is using Webpack for compiling and building the application too, +so we have to make `resolve.modules` consistent in CRA and Structor. +As you may know, it's not possible to change Webpack's config in CRA manually. +But there is a way to do that through the `NODE_PATH` environment variable. + +From this point I suggest to run a brief tutorial +where we will learn how to build a simple component in Structor, and then import this component into our CRA project. + +### Installation and creating an app + +First of all we need to install `create-react-app` globally: +``` +npm install -g create-react-app +``` + +Then create a new app: +``` +create-react-app my-notes +cd my-notes +``` + +This will generate initial project structure in `my-notes`. +> You may learn more about project structure from [Getting Started](https://github.com/facebookincubator/create-react-app#getting-started) of CRA documentation. + +Now install Structor: +``` +npm install structor +``` +And when the prompt is appeared, type `src` as a directory name: + +

+ +

+ +### Set environment + +Here we need to set `NODE_PATH` variable. The best way to do this is to use `cross-env` utility. +Install `cross-env`: +``` +npm install -D cross-env +``` +Then just edit the `start` script line in `package.json` file: +``` +"scripts": { + "start": "cross-env NODE_PATH=src react-scripts start" +} +``` + +### Create a React component + +Run Structor... +``` +npm run structor +``` + +... and open browser with address `localhost:2222/structor`. + +Structor distribution does not ship with any component presets. +So, we are going to install some presets from Structor Market. + +Click on the plus icon in left vertical toolbar and find `Install` button on the appeared panel. + +

+ +

+ +Here we can see the marketplace from which we should install `mui-next-spkg` components preset. + +

+ +

+ +We are not going to create a prototype of a new component in this tutorial, +instead we just select the page with the already prepared prototype of our new component. + +

+ +

+ +If we try to click somewhere on the page we can see that we can select components on the page and somehow manipulate them. +This tutorial is not intended to teach how to create UI in Structor, but if you want to learn about this please read the [Getting Started](https://github.com/ipselon/structor/blob/master/docs#getting-started) chapter of the Structor's User Guide. + +Now we need to select a root element of our new React component on the page. +Click on the code icon in the left vertical toolbar and find `ThemeProvider` component in the bottom tree view. + +

+ +

+ +Keeping `ThemeProvider` selected, find and click on the `New Component` button in the top toolbar. +Here we should choose what type of component we want to generate. Choose a `react-component` generator. + +

+ +

+ +On the first page of the generator wizard type `MyNotes` in the name field. And type `Tutorial` in the namespace field. + +> To learn more about the structure of the generated source code please read the [Working with code](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#working-with-code) chapter of the Structor's User Guide. + +

+ +

+ +Do not change any options in the following steps of the wizard. Just click on `Install component` in the last step. + +Now we will see that we have `MyNotes` component instead of `ThemeProvider` on the page. + +At this point we should change the components source code to make it functional. + +Open the `/src/modules/Tutorial/components/MyNotes/index.js` file and make changes as it is shown below: + +```javascript +/** + * + * MyNotes + * + */ + +import React, { Component } from 'react'; + +import { + ThemeProvider, + LayoutContainer, + LayoutItem, + TextField, + Button +} from 'modules/MUI'; +import { DataList } from 'modules/DataAware'; + +const style = {textAlign: 'center'}; +const style1 = { + width: '50em', + padding: '2em 1em', + borderStyle: 'solid', + borderWidth: '1px', + borderColor: '#e8d8d8' +}; +const style2 = {height: '22em', overflow: 'auto'}; +const style3 = { + borderLeftStyle: 'solid', + borderLeftWidth: '1px', + borderLeftColor: '#e8d8d8' +}; +const style4 = {height: '200px', width: '100%'}; +const style5 = {width: '100%'}; +const style6 = {width: '100%'}; +const style7 = {width: '100%'}; + +const newNote = {primaryText: 'New', secondaryText: 'New text'}; +let indexCounter = 0; + +class MyNotes extends Component { + constructor (props) { + super(props); + this.handleSelectNote = this.handleSelectNote.bind(this); + this.handleAddNote = this.handleAddNote.bind(this); + this.handleDeleteNote = this.handleDeleteNote.bind(this); + this.handleSaveNote = this.handleSaveNote.bind(this); + this.handleChangeTitle = this.handleChangeTitle.bind(this); + this.handleChangeText = this.handleChangeText.bind(this); + this.state = { + notes: [], + editNote: Object.assign({}, newNote), + }; + } + + handleSelectNote (id, primaryText, secondaryText) { + this.setState({editNote: {id, primaryText, secondaryText}}); + } + + handleAddNote () { + this.setState({editNote: Object.assign({}, newNote)}); + } + + handleDeleteNote () { + const {editNote, notes} = this.state; + if (editNote && editNote.id > 0) { + let newNotes = [].concat(notes); + const foundIndex = newNotes.findIndex(i => i.id === editNote.id); + if (foundIndex >= 0) { + newNotes.splice(foundIndex, 1); + this.setState({notes: newNotes, editNote: Object.assign({}, newNote)}); + } + } + } + + handleSaveNote () { + const {editNote, notes} = this.state; + if (editNote) { + let newNotes = [].concat(notes); + if (editNote.id > 0) { + const foundIndex = newNotes.findIndex(i => i.id === editNote.id); + if (foundIndex >= 0) { + newNotes[foundIndex] = Object.assign({}, editNote); + } + } else { + newNotes.push(Object.assign({}, editNote, {id: ++indexCounter})); + } + this.setState({notes: newNotes, editNote: Object.assign({}, newNote)}); + } + } + + handleChangeTitle (e) { + this.setState({ + editNote: Object.assign({}, this.state.editNote, {primaryText: e.target.value}) + }); + } + + handleChangeText (e) { + this.setState({ + editNote: Object.assign({}, this.state.editNote, {secondaryText: e.target.value}) + }); + } + + render () { + const {notes, editNote} = this.state; + const {primaryText, secondaryText} = editNote; + return ( + + +

My Notes

+ +
+ + +
+ +
+
+ +
+ + +
+ + + + + + + + + + + +
+
+
+ +
+ ); + } +} + +export default MyNotes; + +``` + +The changes should be applied by Webpack Dev Server automatically, +if not, please reload page by clicking on the refresh icon in the left vertical toolbar. + +Now we can try component in live preview mode. Click on the hand icon in the left vertical toolbar. + +

+ +

+ +### Import new component into the application + +It's time to inject our component into application and run it by CRA. + +Open `App.js` file in `/src` directory and add `MyNotes` component as it is shown below: + +```javascript +import React, { Component } from 'react'; +import logo from './logo.svg'; +import './App.css'; +import { MyNotes } from 'modules/Tutorial'; + +class App extends Component { + render () { + return ( +
+
+
+ logo +

Welcome to React

+
+
+
+ +
+
+ ); + } +} + +export default App; +``` + +### Run application + +Run application from project directory: + +``` +npm start +``` + +In the browser window you should see the following: + +

+ +

+ +If you encountered some issues please add their explanation in the Structor's repo. diff --git a/docs/structor-market.md b/docs/structor-market.md new file mode 100644 index 00000000..f06421eb --- /dev/null +++ b/docs/structor-market.md @@ -0,0 +1,102 @@ +# Structor's User Guide + +* [Getting Started](https://github.com/ipselon/structor/blob/master/docs#getting-started) +* [Integrating with Create React App](https://github.com/ipselon/structor/blob/master/docs/integrating-with-create-react-app.md) +* [Designing UI](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#designing-ui) + * [A component model](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#a-component-model) + * [Combine and modify component models](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#combine-and-modify-component-models) + * [How to select a component](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-select-a-component) + * [How to paste new component from library](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-paste-new-component-from-library) + * [How to cut, copy and paste components](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-cut-copy-and-paste-components) + * [Navigation through pages](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#navigation-through-pages) +* [Working with code](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#working-with-code) + * [Dev environment](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#dev-environment) + * [The code structure](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#the-code-structure) + * [Component library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#component-library) + * [React components in library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#react-components-in-library) + * [Redux containers in library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#redux-containers-in-library) + * [Component default models](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#component-default-models) + * [Generating the source code](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#generating-the-source-code) + * [React component scaffold](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#react-component-scaffold) + * [Redux container scaffold](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#redux-container-scaffold) + * [Troubleshooting](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#troubleshooting) +* [Structor Market](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#structor-market) + * [Extracting and publishing components presets](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#extracting-and-publishing-components-presets) + * [Extending existing presets on the market](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#extending-existing-presets-on-the-market) +* Exporting + * Export pages + * Export application + +## Structor Market + +If you have not tried to install component presets from Structor Market gallery yet, +please go through [Getting Started](https://github.com/ipselon/structor/blob/master/docs#getting-started) tutorial to feel how it works. + +When Structor opens the Structor Market gallery it reads an `index.json` file in [structor-market](https://github.com/ipselon/structor-market) repo +where all available repos with components presets are listed. + +But how to create a repository with components presets? + +### Extracting and publishing components presets + +Stuctor has a convenient mechanism for sharing components presets between projects. +Open the library panel in Structor's workspace and find `Extract` button at the top of the panel. + +

+ +

+ +After clicking on this button you will be prompted to choose namespaces and pages you want to extract. + +

+ +

+ +In the next step of the wizard Structor is discovering all components in selected namespaces and pages. + +>__Note:__ The only one restriction for the extraction is to use components only from project's `modules` directory. +Simply saying, extracted components, component models or pages should not contain components out of any namespace. +Read detailed explanation of what is a namespace in [The code structure](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#the-code-structure) chapter. + +Structor genuinely investigates how components are linked with each other and shows what namespaces will be additionally added into the package. +If you see errors which were not explained in this section please create an issue in Structor's repo. + +Here you have to enter the absolute path of a directory where package's files will be extracted and click on `Extract namespaces` button. + +

+ +

+ +Once files were successfully extracted you may find them in specified directory. +Now we can install this package into another project right from the local directory. + +If you want to share your package on Structor Market you can create a new repo on GitHub and push there the directory content as it is. +Then just make changes in `index.json` in [structor-market](https://github.com/ipselon/structor-market) repo with the URL of your repository. + +### Extending existing presets on the market + +Creating own package is not reasonable if you are going to make an extension of components presets in some package. + +So I'd like to suggest the following steps to provide the package extension: + * Fork a repo with Structor's presets and clone it to the local folder; + * Install Structor into an empty folder: + * Init `package.json` by command `npm -y init`; + * Install Structor by command: `install structor`; + * Run Structor and open browser with Structor's workspace; + * Go to the Structor Market gallery; + * Choose installation from the local dir; + +

+ +

+ + * Enter the absolute path to the local folder of the cloned repo at the first step; + +

+ +

+ + * Implement some new components/features/models; + * Extract namespaces with your changes into the folder of cloned repo; + * Push changes in cloned repo on GitHub; + * Make a PR to the main repo from your fork. diff --git a/docs/working-with-code.md b/docs/working-with-code.md new file mode 100644 index 00000000..ba6fac44 --- /dev/null +++ b/docs/working-with-code.md @@ -0,0 +1,500 @@ +# Structor's User Guide + +* [Getting Started](https://github.com/ipselon/structor/blob/master/docs#getting-started) +* [Integrating with Create React App](https://github.com/ipselon/structor/blob/master/docs/integrating-with-create-react-app.md) +* [Designing UI](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#designing-ui) + * [A component model](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#a-component-model) + * [Combine and modify component models](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#combine-and-modify-component-models) + * [How to select a component](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-select-a-component) + * [How to paste new component from library](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-paste-new-component-from-library) + * [How to cut, copy and paste components](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#how-to-cut-copy-and-paste-components) + * [Navigation through pages](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#navigation-through-pages) +* [Working with code](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#working-with-code) + * [Dev environment](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#dev-environment) + * [The code structure](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#the-code-structure) + * [Component library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#component-library) + * [React components in library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#react-components-in-library) + * [Redux containers in library](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#redux-containers-in-library) + * [Component default models](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#component-default-models) + * [Generating the source code](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#generating-the-source-code) + * [React component scaffold](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#react-component-scaffold) + * [Redux container scaffold](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#redux-container-scaffold) + * [Troubleshooting](https://github.com/ipselon/structor/blob/master/docs/working-with-code.md#troubleshooting) +* [Structor Market](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#structor-market) + * [Extracting and publishing components presets](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#extracting-and-publishing-components-presets) + * [Extending existing presets on the market](https://github.com/ipselon/structor/blob/master/docs/structor-market.md#extending-existing-presets-on-the-market) +* Exporting + * Export pages + * Export application + +## Working with code + +This chapter assumes that you have a basic knowledge of React, Redux and Redux Saga, Webpack and Hot Reloading. + +Also, it assumes that you understand what is a component model in terms of Structor. +If not, please read [Designing UI](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#designing-ui) chapter of this guide. + +During the installation Structor is asking to provide a directory name where the source code of all components is +or will be in the project. +If specified folder is missing Structor will create it. +It is suggested to use a commonly used [component/container](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) +file structure in this folder. + +If you are not going to make design in Structor and generate scaffolds for new React components, you may not +follow the rules for the source code structure which is described here. +Although, by using Structor only as a playground for React components, just like React Storybook, +you are cutting out the major capabilities of Structor. + +### Dev environment + +Structor runs own Express server on 2222 port. However we can set another port using `p` option, for example: +`npm run structor -p 3443` +The server utilises Webpack's Dev and Hot middlewares to make a hot reloading available in the Structor's workspace. + +As you might have guessed, there should be a Webpack configuration somewhere. +Yes, you can find the webpack config in the metadata folder `.structor/webpack.base.js`. + +This config file is a basic configuration for Webpack dev server which exposes an endpoint for Structor's workspace. +Accordingly there is another configuration file which plays a role of an extension for basic config: `.structor/webpack.app.js`. +So, if you want to customise a webpack config, do it in `webpack.app.js` file - it will not be updated +when you install a new version of Structor. + +We may consider such a development environment as an isolated Web application. +But there are a few files which have references to the project's source code: +`components.js`, `reducers.js` and `sagas.js` files in `.structor/app` folder. + +If Structor wants to work with Redux containers it should have own Redux state configuration. +`.structor/app/store.js` file is a Redux state configuration utility, +and `reducers.js` and `sagas.js` are index files for reducers and sagas in the project. + +There is one more file in `.structor/app` folder - `components.js` file which we discuss in the next section. + +But before we can proceed, we should bear in mind that files: +`components.js`, `reducers.js` and `sagas.js` are connecting points which connect Structor with project's components. + +Taking in account all above, we can say that Stuctor runs as a standalone application. +And it does not depend on any seed or boilerplate project. It can be utilized with any existing React Web application project. +However the only one requirement is to use the Webpack version > 2.2.x. + +### The code structure + +As we already know, it is recommended to use a component/container file structure in a React application. +Moreover, each component will be encapsulated in a self-contained structure. You may read about such structure in +[Three Rules For Structuring (Redux) Applications](https://jaysoo.ca/2016/02/28/organizing-redux-application/) article +or read docs for [React Boilerpate](https://github.com/react-boilerplate/react-boilerplate) project which is using a similar architecture. + +A self-contained structure may be described by the following schema: +``` +components/ + + index.js - React component +containers/ + + index.js - Redux container + actions.js + constants.js + reducer.js + sagas.js + selectors.js +``` + +It does not mean that we can not add extra files in there or remove some files, +the only requirement is to keep files inside of component's folder. + +Such structure gives big advantages in extending and refactoring a complex Web application. +But there is an issue when the application grows bigger - components may have conflicting names. + +In this case we should encapsulate conflicting components in namespaces. +Read [Additional Guidelines For (Redux) Project Structure](https://jaysoo.ca/2016/12/12/additional-guidelines-for-project-structure/) +article about such an approach. Then our structure will look as following: +``` +components/ + + index.js - React component +containers/ + + index.js - Redux container + actions.js + constants.js + reducer.js + sagas.js + selectors.js +modules/ + / + components/ + + index.js - React component + containers/ + + index.js - Redux container + actions.js + constants.js + reducer.js + sagas.js + selectors.js + index.js - namespace components and containers index + reducer.js - composition of containers reducers in namespace + sagas.js - composition of containers' sagas in namespace +``` + +Using this structure for project components is important if we are going to use Structor's generators +or install component packages from Structor Market. + +**Important Note** The source code files should use paths relative to `PROJECT_APP_DIR` folder. +For example: `import Button from 'components/Button'`. +This works because Webpack configuration (`.structor/webpack.base.js`) defines `PROJECT_APP_DIR` folder as +a module in `resolve.modules` settings. + +### Component library + +Any React component included into `.structor/app/components.js` file appears in `Component Library` panel in the Stuctor's workspace. +Let's review the format of this file. Below is an initial file content right after the installation. + +```javascript +import { + Link, + IndexLink +} from 'react-router'; +export { + Link, + IndexLink +}; +``` + +As we can see, this is a regular JS file which makes reexporting of components. + +#### React components in library + +Let's see how it will look if there a React component is included. +Assuming that we are using the file structure described above. + +``` +components/ + Button + index.js +``` + +And the `components.js` file will be: + +```javascript +import { + Link, + IndexLink +} from 'react-router'; +import Button from 'components/Button'; +export { + Link, + IndexLink, + Button, +}; +``` + +`Component Library` panel will show `Button` component in `Components` group: + +

+ +

+ +***Component in namespace*** + +But how it will look if we want to add another `Button` component? We can do that using a namespace module. +See how the file structure should be in this case: +``` +components/ + Button/ + index.js +modules/ + mylib/ + components/ + Button/ + index.js + index.js +``` + +And `components.js` file is: +```javascript +import { + Link, + IndexLink +} from 'react-router'; +import Button from 'components/Button'; +import * as mylib from 'modules/mylib'; +export { + Link, + IndexLink, + Button, + mylib, +}; +``` + +Additionally, we need to review the content of namespace's `index.js` file (`modules/mylib/index.js`). +```javascript +import Button from './components/Button'; +export { + Button +}; +``` + +As we can see, it has the same structure as `components.js` file has. +But how `Component Library` panel will show two `Button` components? +Please find it on the screenshot below: + +

+ +

+ +**Note** It is required to use described format of `component.js` and namespace's `index.js` files - +they are parsed by Structor in order to discover components for the library. + +#### Redux containers in library + +Including Redux containers into library has a few additional steps. +We need to modify `components.js` file along with `reducers.js` and `sagas.js` in `.structor/app` folder. + +Let's start from Redux container which is not in any namespace. + +``` +containers/ + SmartPanel/ + index.js + actions.js + constants.js + reducer.js + sagas.js + selectors.js +``` + +Add `SmartPanel` component in `components.js` file in the same way as it was for React component. +The only difference is that path to the component should start with `containers` folder. + +```javascript +import { + Link, + IndexLink +} from 'react-router'; +import SmartPanel from 'containers/SmartPanel'; +export { + Link, + IndexLink, + SmartPanel, +}; +``` + +After Structor discovered that a new record appeared we will see `SmartPanel` in `Components` group in `Component Library` panel. + +

+ +

+ +Now we need to inject container's reducer and sagas by editing `.structor/app/reducers.js` and `.structor/app/sagas.js`. + +Changes in `reducers.js` file: + +```javascript +import smartPanelReducer from 'containers/SmartPanel/reducer.js'; +export default { smartPanel: smartPanelReducer }; +``` + +And `sagas.js` should be: +```javascript +import SmartPanelSagas from 'containers/SmartPanel/sagas.js'; +export default [...SmartPanelSagas]; +``` + +From now `SmartPanel` reducer is connected to the global state. As a reminder, Structor has own Redux state instance. + +***Container in namespace*** + +It will be a bit more complicated to add Redux container from a namespace. As you already noticed, a namespace uses own index file for components. +Similarly the namespace has own `sagas.js` and `reducer.js` files. Let's review them first in order to understand why we need them. + +Namespace's `sagas.js` file is just an index file which gathers all sagas in the namespace and expose it as an array. +```javascript +import SmartPanelSagas from './containers/SmartPanel/sagas.js'; +export default [ + ...SmartPanelSagas +]; +``` + +On contrary, a namespace's `reducer.js` file provides a combination of reducers of the namespace, +which is injected as a single endpoint in the global Redux state. +It means that reducer of each container will be injected into a namespace state, +then the namespace's state will be injected into the global state. It looks like the following schema: +``` +globalState -> namespaceState -> containerState +``` + +So, namespace's `reducer.js` file will include the following: +```javascript +import { combineReducers } from 'redux'; +import smartPanelReducer from './containers/SmartPanel/reducer.js'; + +export default combineReducers({ + smartPanel: smartPanelReducer +}); +``` + +Consequently, containers in namespace should select state from namespace endpoint rather than directly from global state. +For example, we can create a selector in our `SmartPanel` container like this: +```javascript +const selectSmartpanel = () => (state) => state.mylib.smartPanel; +``` + +Adding namespace's reducer and sagas is similar to the adding a container without namespace, +except that we are adding the namespace reducer in this case. + +`.structor/app/reducers.js`: +```javascript +import smartPanelReducer from 'containers/SmartPanel/reducer.js'; +import mylibReducer from 'modules/mylib/reducer.js'; +export default { + smartPanel: smartPanelReducer, + mylib: mylibReducer +}; +``` + +`.structor/app/sagas.js`: +```javascript +import SmartPanelSagas from 'containers/SmartPanel/sagas.js'; +import mylibSagas from 'modules/mylib/sagas.js'; +export default [ + ...SmartPanelSagas, + ...mylibSagas +]; +``` + +#### Component default models + +We already discussed in [Designing UI](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#designing-ui) +chapter how Structor renders pages with components in its workspace. +And we mentioned in [A component model](https://github.com/ipselon/structor/blob/master/docs/designing-ui.md#a-component-model) +section that there should be a model definition for each component in the library. + +Such definitions, or it's more accurate to say metadata, of component models are located in `.structor/defaults` folder. +Here we can find JSON files for each component accordingly. Models for namespaces are in nested folders. + +``` +.structor/ + defaults/ + Button.json + SmartPanel.json + mylib/ + Button.json + SmartPanel.json +``` + +Don't worry about adding model files for components you added manually - +Structor will render your component with the default model definition. +But if you want to create more models in the library you always can create it easily by `Save Model` action in the Structor's workspace. + +### Generating the source code + +In previous section we learned about what the file structure is recommended to use with Structor. + +Although, you may not follow that rules if you are not going to generate new components by source code +generators or install new components from Structor Market. + +But if you decide to give a shot for Srtuctor's generators, this section will help you understand this powerful feature of Stuctor. + +Frankly saying, the code generation makes life easier a lot. +Of course, it is not suggested that generator will generate ready to go component from the scratch. +But generator is able to generate scaffolds for all needed files. Especially this is true for Redux container files. + +Structor ships with a couple generators for React component and Redux container. +We can find generators code files in `.structor/gengine/scaffolds`. + +Hopefully the number of generators will grow in the future due to the simplicity of creating them. +Generator uses `lodash` template for generating the output files, and it's easy to change the generator output code right in the template. +Find generator's templates in folder: `.structor/gengine/scaffolds//templates`. + + +#### React component scaffold + +Before we start let's understand why Structor makes code generation is extremely useful. + +As we already know, we can easily combine any amount of components in the Structor's workspace. +We can modify components look and feel right on the page. But we don't get a real source code of a component in the result. +That's why we need a generator that is capable to generate the source code of the component. + +A `react-component` generator takes the model of the selected component and produces the source code for React +component with all nested components inside. Moreover, the source code will include all properties and styles as well. + +This generator is smart enough to use correct paths to other components in imports, +and even resolves conflicting names from different namespaces. + +Select any component on the page and click on `Generate Component` button in the top toolbar. + +

+ +

+ +We will see a generator wizard page. Here we should choose what component we want to have in the result. +As we are going to generate simple React component, let's choose `react-component` generator. + +

+ +

+ +In the next step we should enter the name of our future component. Also, there is the possibility to specify a component's namespace. + +

+ +

+ +Then generator asks about additional options which will tell the generator what you want to see in the generated code. + +

+ +

+ +* `Select type of component` - that will tell if we want to inherit Pure component or not. +* `Add default constructor to the source code` - include in the code a constructor +* `Inject children into the source code of the component` - you may not include children into the source code, +in this case we will get an empty component with the same model. This is useful if you want to create kind of a layout grid component for Stuctor. +* `Add an example of the property declaration in the source code` - adds example of property declaration in the React component. + +The next step will show us all source code files which were generated or modified in order to respect +dependencies for Webpack compiler. Here you might see that generator made exactly the same changes in `.structor/app/components.js` +file as we have seen in previous section. Also, there is additional JSON file for a new component model. + +

+ +

+ +Now click `Install component` button to write the code into project's file structure. + +

+ +

+ +#### Redux container scaffold + +The same result we would get in case we chose `redux-container` generator. +Additionally, you'll be prompted to enter a reducer key name property under which container's reducer +will be injected into the global store or the namespace's store in case you specify the namespace. +Also there will be a way more generated files at the preview stage. + +

+ +

+ +**Note** All generated components or containers will appear in corresponding groups in `Component Library` pane in the Stuctor's workspace. + +

+ +

+ +### Troubleshooting + +In case you see that Structor is loading a page too long, or there is an error in the console which tells that Structor endpoint is not found (404). +Then you can switch Structor to the verbose mode and see the Webpack output in the terminal console. + +Find in `package.json` file a line in `scripts` section with name: `structor` and change it to the following: +``` +"structor": "structor -v" +``` + +If you need to change the port of the Structor's server, you may set the `p` option: +``` +"structor": "structor -p 3443" +``` diff --git a/images/title-background.png b/images/title-background.png deleted file mode 100644 index cb0eb7c9..00000000 Binary files a/images/title-background.png and /dev/null differ diff --git a/package.json b/package.json index 40795009..c4e816a3 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,22 @@ { "name": "structor", - "version": "0.1.0", - "description": "A user interface constructor for React", + "version": "2.3.2", + "description": "Structor - React UI Builder", "main": "structor.js", "scripts": { "build-server": "gulp --gulpfile ./build-conf/gulpfile.js build-server", "build-server-dev": "gulp --gulpfile ./build-conf/gulpfile.js build-server-dev", "build-client-less": "gulp --gulpfile ./build-conf/gulpfile.js build-client-less", - "build-client": "webpack --config ./build-conf/webpack.config.js --progress --colors", - "build-client-dev": "webpack --config ./build-conf/webpack-dev.config.js --progress --colors --watch" - }, - "babel": { - "stage": 0, - "plugins": [] + "build-client": "webpack --config ./build-conf/webpack.config.js --progress", + "build-client-dev": "webpack --config ./build-conf/webpack-dev.config.js --progress --watch", + "postinstall": "node ./postexec.js" }, "repository": { "type": "git", "url": "git+https://github.com/ipselon/structor.git" }, - "author": "Alexander Pustovalov", - "license": "GPLv3", + "author": "Alex Pustovalov", + "license": "Apache-2.0", "bugs": { "url": "https://github.com/ipselon/structor/issues" }, @@ -28,55 +25,74 @@ "structor": "structor.js" }, "engines": { - "node": ">=0.12.2" + "npm": ">=3", + "node": ">=6" }, "dependencies": { - "babel-plugin-react-transform": "^1.1.1", - "body-parser": "^1.14.0", - "escodegen": "^1.7.0", - "esformatter": "^0.7.3", - "esformatter-jsx": "^2.3.6", - "esprima": "^2.6.0", - "esprima-fb": "^15001.1001.0-dev-harmony-fb", - "express": "^4.13.3", - "express-urlrewrite": "^1.2.0", - "fs-extra": "^0.24.0", - "http-proxy": "^1.11.2", - "immutable": "^3.7.5", - "lodash": "^3.10.1", + "babel-core": "^6.21.0", + "babel-loader": "^6.2.10", + "babel-polyfill": "^6.22.0", + "babel-preset-es2015": "^6.22.0", + "babel-preset-react": "^6.16.0", + "babel-preset-stage-0": "^6.16.0", + "body-parser": "1.15.0", + "css-loader": "^0.26.1", + "eventsource-polyfill": "^0.9.6", + "exports-loader": "^0.6.3", + "express": "4.13.4", + "express-urlrewrite": "1.2.0", + "file-loader": "^0.9.0", + "html-loader": "^0.4.4", + "http-proxy": "1.13.2", + "image-webpack-loader": "^2.0.0", + "imports-loader": "^0.6.5", + "inquirer": "2.0.0", + "json-loader": "^0.5.4", + "lodash": "^4.16.4", + "marked": "0.3.6", "minimist": "^1.2.0", - "react-transform-hmr": "^1.0.1", - "request": "^2.63.0", - "socket.io": "^1.3.7", - "tar-fs": "^1.8.1", - "webpack": "^1.12.2", - "webpack-dev-middleware": "^1.2.0", - "webpack-hot-middleware": "^2.2.1" + "prop-types": "^15.5.8", + "react": "^15.5.4", + "react-dom": "^15.5.4", + "react-hot-loader": "3.0.0-beta.7", + "react-redux": "^5.0.1", + "redux": "^3.6.0", + "redux-saga": "^0.12.0", + "request": "^2.70.0", + "reselect": "^2.5.4", + "socket.io": "1.4.5", + "structor-commons": "0.0.8", + "style-loader": "^0.13.1", + "url-loader": "^0.5.7", + "webpack": "^2.2.1", + "webpack-dev-middleware": "^1.10.0", + "webpack-hot-middleware": "^2.16.0", + "whatwg-fetch": "^2.0.1" }, "devDependencies": { - "babel-core": "^5.8.25", - "babel-loader": "^5.3.2", - "css-loader": "^0.19.0", - "del": "^2.0.2", - "extract-text-webpack-plugin": "^0.8.2", - "file-loader": "^0.8.4", - "gulp": "^3.9.0", - "gulp-autoprefixer": "^3.0.2", - "gulp-babel": "^5.2.1", - "gulp-less": "^3.0.3", - "gulp-uglify": "^1.4.1", - "gulp-watch": "^4.3.5", - "isomorphic-fetch": "^2.1.1", - "marked": "^0.3.5", - "react": "^0.14.0", - "react-bootstrap": "^0.27.1", - "react-dom": "^0.14.0", - "react-redux": "^3.1.0", - "react-widgets": "file:../../thirdparty/react-widgets-master", - "redux": "^3.0.2", - "redux-thunk": "^1.0.0", - "style-loader": "^0.12.4", - "url-loader": "^0.5.6", - "validator": "^4.1.0" - } + "del": "2.2.0", + "extract-text-webpack-plugin": "2.1.0", + "graphlib": "2.1.1", + "gulp": "3.9.1", + "gulp-autoprefixer": "3.1.1", + "gulp-babel": "6.1.2", + "gulp-less": "3.1.0", + "gulp-uglify": "2.0.0", + "gulp-watch": "4.3.9", + "react-bootstrap": "0.30.6", + "react-color": "2.11.7", + "redux-thunk": "^2.1.0", + "validator": "5.2.0" + }, + "keywords": [ + "react", + "ui", + "components", + "visual", + "editor", + "builder", + "generator", + "hot-reload", + "structor" + ] } diff --git a/postexec.js b/postexec.js new file mode 100755 index 00000000..6f61e8fa --- /dev/null +++ b/postexec.js @@ -0,0 +1,120 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var path = require('path'); +var currentDir = process.cwd(); +if (currentDir) { + currentDir = currentDir.replace(/\\/g, '/'); +} +var pathParts = currentDir.split('/'); + +var PACKAGE_NAME = 'structor'; +var NODE_DIR_NAME = 'node_modules'; +var META_REPO_NAME = 'structor-meta'; +var META_REPO_ARCHIVE_EXT = '.tar.gz'; +var META_REPO_URL = 'https://github.com/ipselon/' + META_REPO_NAME; +var META_REPO_ARCHIVE_URL = META_REPO_URL + '/archive'; + +var questions = [ + { + name: 'srcPath', + message: 'Specify a directory name where the source code of the generated components will be.\n ' + + 'If the directory does not exist in the current project structure it will be created.\n', + default: 'app', + validate: (input) => /^\w+$/i.test(input) ? true : 'Alphanumeric value is acceptable only.' + } +]; + +var downloadController; +try{ + downloadController = require(currentDir + '/server/structor/downloadManager.js'); +} catch (e) { + // do nothing; +} +if (process.env.npm_config_global) { + console.error('Structor must be installed locally.'); +} else if (downloadController) { + if (pathParts && pathParts.length > 0) { + var lastDir = pathParts[pathParts.length - 1]; + var nodeDir = pathParts[pathParts.length - 2]; + if (lastDir === PACKAGE_NAME && nodeDir === NODE_DIR_NAME) { + + const packageVersion = process.env.npm_package_version; + const versionParts = packageVersion.split('-'); + let packageFileName = packageVersion + META_REPO_ARCHIVE_EXT; + if (versionParts.length > 1) { + packageFileName = versionParts[1] + META_REPO_ARCHIVE_EXT; + } + + const appDirPath = path.resolve(currentDir, '../..'); + + downloadController.checkMetaFolderVersion(appDirPath, packageVersion) + .then((isVersionMatch) => { + if (!isVersionMatch) { + downloadController.downloadMetaDistr(META_REPO_ARCHIVE_URL + '/' + packageFileName, appDirPath) + .then(paths => { + return downloadController.updateMetaFolder(paths.innerDirPath, appDirPath) + .then(() => { + return downloadController.removeFile(paths.tempDirPath); + }); + }) + .then(() => { + return downloadController.setMetaFolderVersion(appDirPath, packageVersion) + }) + .then(() => { + console.log('Meta data folder is updated successfully to the version: ' + packageVersion); + }) + .catch(error => { + console.error("Can not download " + META_REPO_ARCHIVE_URL + '/' + packageFileName, error); + }); + } + }) + .catch(error => { + downloadController.downloadMetaDistr(META_REPO_ARCHIVE_URL + '/' + packageFileName, appDirPath) + .then(paths => { + var inquirer = require('inquirer'); + return inquirer.prompt(questions) + .then(answers => { + const options = { + srcPath: answers.srcPath, + }; + return downloadController.createMetaFolder(paths.innerDirPath, appDirPath, options); + }) + .then(() => { + return downloadController.setMetaFolderVersion(appDirPath, packageVersion) + }) + .then(() => { + console.log('Meta data folder is created successfully. Version: ' + packageVersion); + }) + .catch(error => { + console.error("Can not download " + META_REPO_ARCHIVE_URL + '/' + packageFileName, error); + }) + .then(() => { + return downloadController.removeFile(paths.tempDirPath); + }); + }) + .catch(error => { + console.error(error); + }); + }); + } else { + // Installation of some local module. + // do nothing; + } + } else { + console.error('Can not parse current dir path.'); + } +} diff --git a/src-client/api/app/coockiesApi.js b/src-client/api/app/coockiesApi.js new file mode 100644 index 00000000..a35f86e5 --- /dev/null +++ b/src-client/api/app/coockiesApi.js @@ -0,0 +1,74 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as utils from '../utils/utils.js'; + +const RECENT_GENERATORS_LIST = 'structor-recent-generators-list'; +const RECENT_INSTALLER_DIR_PATHS = 'structor-recent-installer-dir-paths'; +const DESK_SETTINGS = 'structor-desk-settings'; + +export function addToRecentGenerators (generatorId) { + let recentGenerators = utils.retrieveCookiesObject(RECENT_GENERATORS_LIST); + recentGenerators = recentGenerators || []; + const existingIndex = recentGenerators.indexOf(generatorId); + if (existingIndex >= 0) { + recentGenerators.splice(existingIndex, 1); + } + recentGenerators.splice(0, 0, generatorId); + utils.saveCookiesObject(RECENT_GENERATORS_LIST, recentGenerators); + return recentGenerators; +} + +export function removeFromRecentGenerators (generatorId) { + let recentGenerators = utils.retrieveCookiesObject(RECENT_GENERATORS_LIST); + recentGenerators = recentGenerators || []; + const existingIndex = recentGenerators.indexOf(generatorId); + if (existingIndex >= 0) { + recentGenerators.splice(existingIndex, 1); + } + utils.saveCookiesObject(RECENT_GENERATORS_LIST, recentGenerators); + return recentGenerators; +} + +export function getRecentGenerators () { + return utils.retrieveCookiesObject(RECENT_GENERATORS_LIST); +} + +export function addToRecentInstallerDirPaths (dirPath) { + let recentPaths = utils.retrieveCookiesObject(RECENT_INSTALLER_DIR_PATHS); + recentPaths = recentPaths || []; + const existingIndex = recentPaths.indexOf(dirPath); + if (existingIndex >= 0) { + recentPaths.splice(existingIndex, 1); + } + recentPaths.splice(0, 0, dirPath); + utils.saveCookiesObject(RECENT_INSTALLER_DIR_PATHS, recentPaths); + return recentPaths; +} + +export function getRecentInstallerDirPaths () { + return utils.retrieveCookiesObject(RECENT_INSTALLER_DIR_PATHS); +} + +export function getDeskSettings () { + return utils.retrieveCookiesObject(DESK_SETTINGS); +} + +export function saveDeskSettings (newSettings) { + let deskSettings = getDeskSettings(); + deskSettings = Object.assign({}, deskSettings, newSettings); + utils.saveCookiesObject(DESK_SETTINGS, deskSettings); +} diff --git a/src-client/api/app/restApi.js b/src-client/api/app/restApi.js new file mode 100644 index 00000000..6b46f047 --- /dev/null +++ b/src-client/api/app/restApi.js @@ -0,0 +1,75 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import _ from 'lodash'; +import 'isomorphic-fetch'; + +export function makeRequest(url, method, options = {}){ + + let fetchOptions = { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json;charset=UTF-8' + } + }; + if(options){ + fetchOptions.body = JSON.stringify({ + methodName: method, + data: options + }); + } + return fetch(url, fetchOptions) + .then( response => { + //console.log('Received response: ' + JSON.stringify(response, null, 4)); + //console.log('Received response: ' + response.status); + //console.log('Received response: ' + response.statusText); + //console.log(response.headers.get('Content-Type')); + if (response.status >= 200 && response.status < 300) { + + return response.text() + .then(responseText => { + //console.log('Server response: ' + responseText); + let jsonData = undefined; + try{ + jsonData = JSON.parse(responseText); + } catch(e){ + } + if(jsonData.error === true){ + let errorText = ''; + if(_.isArray(jsonData.errors)){ + jsonData.errors.forEach(errText => { + errorText += '\n' + errText; + }); + } else { + errorText = JSON.stringify(jsonData.errors); + } + throw Error(errorText); + } else if(jsonData.data !== undefined){ + jsonData = jsonData.data; + } + return jsonData; + }); + } else { + throw Error(response.statusText); + } + }); + +} + +export function invokeStructor(method, options){ + return makeRequest('/structor-invoke', method, options); +} diff --git a/src-client/api/app/serverApi.js b/src-client/api/app/serverApi.js new file mode 100644 index 00000000..edd9f853 --- /dev/null +++ b/src-client/api/app/serverApi.js @@ -0,0 +1,143 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {forOwn, isObject} from 'lodash'; +import {invokeStructor} from './restApi.js'; + +export function getProjectStatus() { + return invokeStructor('getProjectStatus', {}); +} + +export function getProjectInfo() { + return invokeStructor('getConfig'); +} + +export function getProjectModel() { + return invokeStructor('getModel'); +} + +export function saveProjectModel(model) { + return invokeStructor('saveProjectModel', {model: model}); +} + +export function exportProjectModel(model) { + return invokeStructor('saveProjectModel', {model: model}).then(() => { + return invokeStructor('exportPages', {model: model}); + }); +} + +export function setProxyURL(proxyURL) { + return invokeStructor('setProxyURL', {proxyURL}); +} + +export function loadComponentTree() { + return invokeStructor('getComponentTree', {}) +} + +export function loadComponentOptions(componentName, namespace, sourceCodeFilePath) { + let result = {}; + return invokeStructor('getComponentNotes', {componentName, namespace}) + .then(response => { + result.readmeText = response.readmeText; + if (sourceCodeFilePath) { + return invokeStructor('getComponentSourceCode', {filePath: sourceCodeFilePath}) + .then(response => { + result.sourceCode = response; + return result; + }); + } else { + return result; + } + }); +} + +export function loadComponentNotes(componentName, namespace) { + return invokeStructor('getComponentNotes', {componentName, namespace}); +} + +export function writeComponentSource(sourceCodeFilePath, sourceCode) { + return invokeStructor('writeComponentSourceCode', {filePath: sourceCodeFilePath, sourceCode}); +} + +export function writeComponentDefaults(componentName, namespace, defaults) { + return invokeStructor('writeComponentDefaults', {componentName, namespace, defaults}); +} + +export function getAvailableGeneratorsList() { + return invokeStructor('getScaffoldGenerators'); +} + +export function pregenerate(generatorName, + generatorDirPath, + namespace, + componentName, + model) { + return invokeStructor( + 'pregenerate', + {generatorName, generatorDirPath, namespace, componentName, model} + ); +} +export function generate(generatorName, + generatorDirPath, + namespace, + componentName, + model, + metadata) { + return invokeStructor( + 'generate', + {generatorName, generatorDirPath, namespace, componentName, model, metadata} + ); +} + +export function saveGenerated(files, dependencies) { + return invokeStructor('saveGenerated', {files, dependencies}); +} + +export function generateApplication(pagesModel, hasApplicationFiles) { + return invokeStructor('generateApplication', {pagesModel, hasApplicationFiles}); +} + +export function preExtractNamespaces(namespaces, pages) { + return invokeStructor('preExtractNamespaces', {namespaces, pages}); +} + +export function extractNamespaces(namespaces, dependencies, pages, dirPath) { + return invokeStructor('extractNamespaces', {namespaces, dependencies, pages, dirPath}); +} + +export function getMarketList() { + return invokeStructor('getMarketList', {}); +} + +export function getGHRepoInfo(gitHubRepo, gitHubOwner) { + return invokeStructor('getGHRepoInfo', {gitHubRepo, gitHubOwner}); +} + +export function preInstallFromLocalDir(dirPath) { + return invokeStructor('preInstall', {dirPath}); +} + +export function preInstallFromUrl(url) { + return invokeStructor('preInstall', {url}); +} + +export function cancelInstall() { + return invokeStructor('cancelInstall', {}); +} + +export function installFromLocalDir(dirPath) { + return invokeStructor('installFromLocalDir', {dirPath}); +} diff --git a/src-client/api/index.js b/src-client/api/index.js new file mode 100644 index 00000000..50889bfe --- /dev/null +++ b/src-client/api/index.js @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as restApi from './app/restApi.js'; +import * as serverApi from './app/serverApi.js'; +import * as utils from './utils/utils.js'; +import * as utilsStore from './utils/utilsStore.js'; +import * as print from './utils/printProps.js'; +import * as cookies from './utils/cookies.js'; +import * as graphApi from './model/graphApi.js'; +import * as coockiesApi from './app/coockiesApi.js'; + +export { + restApi, + serverApi, + utils, + utilsStore, + print, + cookies, + graphApi, + coockiesApi +}; diff --git a/src-client/api/model/graphApi.js b/src-client/api/model/graphApi.js new file mode 100644 index 00000000..3fb22f60 --- /dev/null +++ b/src-client/api/model/graphApi.js @@ -0,0 +1,1163 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {forOwn, isObject, takeRight, last, initial} from 'lodash'; +import {Graph} from 'graphlib'; +import {fulex} from '../utils/utils.js'; +import {getAvailableRoute} from './reactRouterApi.js'; +import {mapModel, mapModelForNode, makeNodeWrapper, traverseGraphBranch, adjustIndices} from './graphUtils.js'; +import {detachGraphNode, detachGraphNodes, copyGraphNode, copyGraphNodes} from './graphUtils.js'; + +let bufferKey = undefined; + +let graphObject = { + graph: undefined, + model: undefined, + pageNodes: [] +}; + +let history = []; + +export function pushHistory(pagePath) { + if (history.length >= 50) { + history = takeRight(history, 50); + } + history.push({ + model: fulex(graphObject.model), + markedKeys: getAllMarkedKeys(), + pagePath: pagePath + }); +} + +export function popHistory() { + if (history.length > 0) { + const historyObject = last(history); + initGraph(historyObject.model); + history = initial(history); + return historyObject; + } + return undefined; +} + +export function getHistorySize() { + return history.length; +} + +export function initGraph(initialModel) { + if (graphObject.graph) { + delete graphObject.graph; + } + if (graphObject.model) { + delete graphObject.model; + } + if (graphObject.pageNodes) { + delete graphObject.pageNodes; + } + graphObject.model = fulex(initialModel); + if (graphObject.model && graphObject.model.pages && graphObject.model.pages.length > 0) { + graphObject.graph = new Graph({compound: true}); + graphObject.graph.setDefaultEdgeLabel('link'); + graphObject.pageNodes = []; + let pageKey; + graphObject.model.pages.forEach((page, index) => { + pageKey = mapModel(graphObject.graph, page, index); + graphObject.pageNodes.push({ + pagePath: page.pagePath, + pageName: page.pageName, + pageKey: pageKey + }); + }); + } +} + +export function getGraph() { + return graphObject.graph; +} + +export function getModel() { + return graphObject.model; +} + +export function getPages() { + return graphObject.pageNodes; +} + +export function getNode(key) { + return graphObject.graph.node(key); +} + +export function getPagePath(pathname) { + let paths = []; + graphObject.pageNodes.forEach(pageNode => { + paths.push(pageNode.pagePath); + }); + return getAvailableRoute(paths, pathname); +} + +export function getPageModelByPagePath(pathname) { + let found = undefined; + if (graphObject.model && graphObject.model.pages) { + found = graphObject.model.pages.find(page => page.pagePath === pathname); + } + return found; +} + +export function getWrappedModelByPagePath(pathname) { + let wrappedModel = undefined; + let paths = []; + graphObject.pageNodes.forEach(pageNode => { + paths.push(pageNode.pagePath); + }); + const path = getAvailableRoute(paths, pathname); + const pageNode = graphObject.pageNodes.find(pageNode => { + return pageNode.pagePath === path; + }); + if (pageNode) { + wrappedModel = traverseGraph(pageNode.pageKey); + } + return wrappedModel; +} + +export function getMarkedKeysByPagePath(pathname) { + let paths = []; + graphObject.pageNodes.forEach(pageNode => { + paths.push(pageNode.pagePath); + }); + const path = getAvailableRoute(paths, pathname); + const pageNode = graphObject.pageNodes.find(pageNode => { + return pageNode.pagePath === path; + }); + return getMarkedKeys(pageNode ? pageNode.pageKey : undefined); +} + +export function getChildNodes(key) { + let result = []; + const children = graphObject.graph.children(key); + if (children && children.length > 0) { + children.forEach(child => { + result.push(makeNodeWrapper(child, graphObject.graph.node(child))); + }); + result.sort((a, b) => a.index - b.index); + } + return result; +} + +export function hasNode(key) { + return graphObject.graph.hasNode(key); +} + +function traverseGraph(rootNodeKey, result) { + const {graph} = graphObject; + let rootNode = graph.node(rootNodeKey); + if (rootNode) { + let resultNode = makeNodeWrapper(rootNodeKey, rootNode); + const parentKey = graph.parent(rootNodeKey); + if (!parentKey) { + resultNode.isRoot = true; + } + let children = graph.children(rootNodeKey); + if (children && children.length > 0) { + children.forEach(child => { + traverseGraph(child, resultNode); + }); + if (resultNode.children && resultNode.children.length > 0) { + resultNode.children.sort((a, b) => a.index - b.index); + } + } + if (!result) { + result = resultNode; + } else if (rootNode.prop) { + result.props = result.props || {}; + result.props[rootNode.prop] = resultNode; + } else { + result.children = result.children || []; + result.children.push(resultNode); + } + } + return result; +} + +export function traverseAllGraph() { + const {pageNodes} = graphObject; + let result = []; + pageNodes.forEach(pNode => { + result = result.concat(traverseGraph(pNode.pageKey)); + }); + return result; +} + +export function getParentsList(childNodeKey, result) { + const {graph} = graphObject; + let childNode = graph.node(childNodeKey); + if (childNode) { + let childModelNode = makeNodeWrapper(childNodeKey, childNode); + result = result || []; + result.push(childModelNode); + const parentNodeKey = graph.parent(childNodeKey); + if (parentNodeKey) { + getParentsList(parentNodeKey, result); + } else { + childModelNode.isRoot = true; + } + } + return result; +} + +export function changePagePathAndName(pagePath, nextPagePath, nextPageName) { + const {graph, pageNodes} = graphObject; + const pageNode = pageNodes.find(pNode => { + return pNode.pagePath === pagePath; + }); + if (!pageNode) { + throw Error('Change page path and name: specified path ' + pagePath + ' was not found.'); + } + let rootNode = graph.node(pageNode.pageKey); + if (rootNode) { + let modelNode = rootNode.modelNode; + modelNode.pageName = nextPageName; + modelNode.pagePath = nextPagePath; + pageNode.pageName = nextPageName; + pageNode.pagePath = nextPagePath; + } + return graphObject.pageNodes; +} + +export function addNewPage(initialModel, pagePath, pageName) { + let {graph, model, pageNodes} = graphObject; + let pageModel = fulex(initialModel); + pageModel.pagePath = pagePath; + pageModel.pageName = pageName; + model.pages.push(pageModel); + const pageKey = mapModel(graph, pageModel, pageNodes.length, true); + pageNodes.push({ + pagePath: pagePath, + pageName: pageName, + pageKey: pageKey + }); + return pageNodes; +} + +export function duplicatePage(pagePath, nextPagePath, nextPageName) { + let {graph, model, pageNodes} = graphObject; + const pageNode = pageNodes.find(pNode => { + return pNode.pagePath === pagePath; + }); + if (!pageNode) { + throw Error('Duplicate page: specified path ' + pagePath + ' was not found.'); + } + let rootNode = graph.node(pageNode.pageKey); + if (rootNode) { + let pageModel = fulex(rootNode.modelNode); + pageModel.pagePath = nextPagePath; + pageModel.pageName = nextPageName; + model.pages.push(pageModel); + const pageKey = mapModel(graph, pageModel, pageNodes.length, true); + pageNodes.push({ + pagePath: pageModel.pagePath, + pageName: pageModel.pageName, + pageKey: pageKey + }); + } + return pageNodes; +} + +export function setIndexPage(pagePath) { + let {graph, model, pageNodes} = graphObject; + + const pageNode = pageNodes.find(pNode => { + return pNode.pagePath === pagePath; + }); + if (!pageNode) { + throw Error('Set index page: specified path ' + pagePath + ' was not found.'); + } + + let rootNode = graph.node(pageNode.pageKey); + if (rootNode) { + const tempModel = model.pages.splice(rootNode.index, 1)[0]; + if (tempModel) { + model.pages.splice(0, 0, tempModel); + } else { + console.error('Page model was not found in pages model index: ' + rootNode.index); + } + let newPageNodes = []; + let newPageNode; + let graphNode; + model.pages.forEach((page, index) => { + newPageNode = pageNodes.find(pNode => { + return pNode.pagePath === page.pagePath; + }); + if (newPageNode) { + graphNode = graph.node(newPageNode.pageKey); + graphNode.index = index; + newPageNodes.push(newPageNode); + } + }); + graphObject.pageNodes = newPageNodes; + } + return graphObject.pageNodes; +} + +export function deletePage(pagePath) { + let {graph, model, pageNodes} = graphObject; + + if (model.pages && model.pages.length === 1) { + throw Error('The current page is the only page in the project and can not be deleted.'); + } + + const pageNode = pageNodes.find(pNode => { + return pNode.pagePath === pagePath; + }); + if (!pageNode) { + throw Error('Delete page: specified path ' + pagePath + ' was not found.'); + } + + let rootNode = graph.node(pageNode.pageKey); + if (rootNode) { + model.pages.splice(rootNode.index, 1); + traverseGraphBranch(graph, pageNode.pageKey, (nodeKey => { + graph.removeNode(nodeKey); + })); + + let newPageNodes = []; + let newPageNode; + let graphNode; + model.pages.forEach((page, index) => { + newPageNode = pageNodes.find(pNode => { + return pNode.pagePath === page.pagePath; + }); + if (newPageNode) { + graphNode = graph.node(newPageNode.pageKey); + graphNode.index = index; + newPageNodes.push(newPageNode); + } + }); + graphObject.pageNodes = newPageNodes; + } + return graphObject.pageNodes; +} + + +function getDetachedKeysForCutting() { + const {graph, pageNodes} = graphObject; + const testFunc = node => node.isForCutting; + let detachedKeys = []; + pageNodes.forEach(pNode => { + detachGraphNodes(graph, pNode.pageKey, testFunc, detachedKeys); + }); + return detachedKeys; +} + +function getDetachedKeysForCopying() { + const {graph, pageNodes} = graphObject; + const testFunc = node => node.isForCopying; + let detachedKeys = []; + pageNodes.forEach(pNode => { + copyGraphNodes(graph, pNode.pageKey, testFunc, detachedKeys); + }); + return detachedKeys; +} + +export function isCutPasteAvailable(nodeKey) { + let childNode = graphObject.graph.node(nodeKey); + return !!(childNode && !childNode.isForCuttingChild && !childNode.isForCutting); +} + +export function cutPasteBeforeOrAfter(isAfter) { + const {selected} = getAllMarkedKeys(); + let resultKeys = []; + if (selected && selected.length === 1 && isCutPasteAvailable(selected[0])) { + const {graph} = graphObject; + const nodeKey = selected[0]; + const node = graph.node(nodeKey); + if (!node) { + console.error('Cut & paste before or after node: node with key ' + nodeKey + ' was not found.'); + } else if (!node.prop) { + let detachedKeys = getDetachedKeysForCutting(); + if (detachedKeys.length > 0) { + const parentKey = graph.parent(nodeKey); + if (parentKey) { + let detachedModelNodes = []; + let detachedNode; + detachedKeys.forEach(detachedKey => { + detachedNode = graph.node(detachedKey); + if (detachedNode && detachedNode.modelNode) { + graph.setParent(detachedKey, parentKey); + detachedModelNodes.push(detachedNode.modelNode); + } + }); + let parentNode = graph.node(parentKey); + let {modelNode} = parentNode; + let modelNodesArgs = [node.index + (isAfter ? 1 : 0), 0].concat(detachedModelNodes); + modelNode.children.splice.apply(modelNode.children, modelNodesArgs); + adjustIndices(graph, parentKey); + } + } + resultKeys = resultKeys.concat(detachedKeys); + } else { + resultKeys.push(nodeKey); + } + } else { + console.error('Cut & paste before or after node: pasting can not be provided for the same component or its children, or a multiple selection'); + } + return resultKeys; +} + +export function cutPasteFirstOrLast(isFirst) { + const {selected} = getAllMarkedKeys(); + let resultKeys = []; + if (selected && selected.length === 1 && isCutPasteAvailable(selected[0])) { + const {graph} = graphObject; + const nodeKey = selected[0]; + const node = graph.node(nodeKey); + if (!node) { + console.error('Cut & paste first or last node: node with key ' + nodeKey + ' was not found.'); + } else { + let detachedKeys = getDetachedKeysForCutting(); + if (detachedKeys.length > 0) { + let detachedModelNodes = []; + let detachedNode; + detachedKeys.forEach(detachedKey => { + detachedNode = graph.node(detachedKey); + if (detachedNode && detachedNode.modelNode) { + graph.setParent(detachedKey, nodeKey); + detachedModelNodes.push(detachedNode.modelNode); + } + }); + let {modelNode} = node; + modelNode.children = modelNode.children || []; + const lastIndex = modelNode.children.length > 0 ? modelNode.children.length : 0; + let modelNodesArgs = [(isFirst ? 0 : lastIndex), 0].concat(detachedModelNodes); + modelNode.children.splice.apply(modelNode.children, modelNodesArgs); + adjustIndices(graph, nodeKey); + } + resultKeys = resultKeys.concat(detachedKeys); + } + } else { + console.error('Cut & paste first or last node: pasting can not be provided for the same component or its children, or a multiple selection'); + } + return resultKeys; +} + +export function cutPasteReplace() { + const {selected} = getAllMarkedKeys(); + let resultKeys = []; + if (selected && selected.length === 1 && isCutPasteAvailable(selected[0])) { + const {graph} = graphObject; + const nodeKey = selected[0]; + const node = graph.node(nodeKey); + if (!node) { + console.error('Cut & paste replace: node with key ' + nodeKey + ' was not found.'); + } else if (!node.prop) { + let detachedKeys = getDetachedKeysForCutting(); + if (detachedKeys.length > 0) { + const parentKey = graph.parent(nodeKey); + if (parentKey) { + const nodeIndex = node.index; + traverseGraphBranch(graph, nodeKey, (key => { + graph.removeNode(key); + })); + delete node.modelNode; + let detachedModelNodes = []; + let detachedNode; + detachedKeys.forEach(detachedKey => { + detachedNode = graph.node(detachedKey); + if (detachedNode && detachedNode.modelNode) { + graph.setParent(detachedKey, parentKey); + detachedModelNodes.push(detachedNode.modelNode); + } + }); + let parentNode = graph.node(parentKey); + let {modelNode} = parentNode; + modelNode.children = modelNode.children || []; + let modelNodesArgs = [nodeIndex, 1].concat(detachedModelNodes); + modelNode.children.splice.apply(modelNode.children, modelNodesArgs); + adjustIndices(graph, parentKey); + } + } + resultKeys = resultKeys.concat(detachedKeys); + } else { + resultKeys.push(nodeKey); + } + } else { + console.error('Cut & paste replace: replacing can not be provided for the same component or a multiple selection'); + } + return resultKeys; +} + +//export function cutPasteWrap(nodeKey){ +// if(!isCutPasteAvailable(nodeKey)){ +// throw Error('Node with key ' + nodeKey + ' is not available to cut & paste operation.'); +// } +// const {graph} = graphObject; +// const node = graph.node(nodeKey); +// if(!node){ +// throw Error('Cut & paste wrap: node with key ' + nodeKey + ' was not found.'); +// } +// if(!node.prop){ +// const { forCutting } = getAllMarkedKeys(); +// if(forCutting.length !== 1){ +// throw Error('Cut & paste wrap: wrapping can be applied only for single component'); +// } +// const pastedKeys = cutPasteBeforeOrAfter(nodeKey, false); +// removeForCutting(pastedKeys[0]); +// setForCutting(nodeKey); +// cutPasteFirstOrLast(pastedKeys[0], false); +// removeForCutting(nodeKey); +// return pastedKeys; +// } else { +// return [nodeKey]; +// } +//} + +export function copyPasteBeforeOrAfter(isAfter) { + const {selected} = getAllMarkedKeys(); + const {graph} = graphObject; + let resultKeys = []; + if (selected && selected.length > 0) { + selected.forEach(nodeKey => { + const node = graph.node(nodeKey); + if (!node) { + console.error('Copy & paste before or after node: node with key ' + nodeKey + ' was not found.'); + } else if (!node.prop) { + let detachedKeys = getDetachedKeysForCopying(); + if (detachedKeys.length > 0) { + const parentKey = graph.parent(nodeKey); + if (parentKey) { + let detachedModelNodes = []; + let detachedNode; + detachedKeys.forEach(detachedKey => { + detachedNode = graph.node(detachedKey); + if (detachedNode && detachedNode.modelNode) { + graph.setParent(detachedKey, parentKey); + detachedModelNodes.push(detachedNode.modelNode); + } + }); + let parentNode = graph.node(parentKey); + let {modelNode} = parentNode; + modelNode.children = modelNode.children || []; + let modelNodesArgs = [node.index + (isAfter ? 1 : 0), 0].concat(detachedModelNodes); + modelNode.children.splice.apply(modelNode.children, modelNodesArgs); + adjustIndices(graph, parentKey); + } + } + resultKeys = resultKeys.concat(detachedKeys); + } else { + resultKeys.push(nodeKey); + } + }); + } + return resultKeys; +} + +export function copyPasteFirstOrLast(isFirst) { + const {selected} = getAllMarkedKeys(); + const {graph} = graphObject; + let resultKeys = []; + if (selected && selected.length > 0) { + selected.forEach(nodeKey => { + const node = graph.node(nodeKey); + if (!node) { + console.error('Copy & paste first or last node: node with key ' + nodeKey + ' was not found.'); + } else { + let detachedKeys = getDetachedKeysForCopying(); + if (detachedKeys.length > 0) { + + let detachedModelNodes = []; + let detachedNode; + detachedKeys.forEach(detachedKey => { + detachedNode = graph.node(detachedKey); + if (detachedNode && detachedNode.modelNode) { + graph.setParent(detachedKey, nodeKey); + detachedModelNodes.push(detachedNode.modelNode); + } + }); + let {modelNode} = node; + modelNode.children = modelNode.children || []; + const lastIndex = modelNode.children.length > 0 ? modelNode.children.length : 0; + let modelNodesArgs = [(isFirst ? 0 : lastIndex), 0].concat(detachedModelNodes); + modelNode.children.splice.apply(modelNode.children, modelNodesArgs); + adjustIndices(graph, nodeKey); + } + resultKeys = resultKeys.concat(detachedKeys); + } + }); + } + return resultKeys; +} + +export function copyPasteReplace() { + const {selected, forCopying} = getAllMarkedKeys(); + const {graph} = graphObject; + let resultKeys = []; + if (selected && selected.length > 0) { + selected.forEach(nodeKey => { + if (forCopying.indexOf(nodeKey) < 0) { + const node = graph.node(nodeKey); + if (!node) { + throw Error('Copy & paste replace: node with key ' + nodeKey + ' was not found.'); + } else if (!node.prop) { + let detachedKeys = getDetachedKeysForCopying(); + if (detachedKeys.length > 0) { + const parentKey = graph.parent(nodeKey); + if (parentKey) { + const nodeIndex = node.index; + traverseGraphBranch(graph, nodeKey, (key => { + graph.removeNode(key); + const removedIndex = resultKeys.indexOf(key); + if (removedIndex >= 0) { + resultKeys.splice(removedIndex, 1); + } + })); + delete node.modelNode; + let detachedModelNodes = []; + let detachedNode; + detachedKeys.forEach(detachedKey => { + detachedNode = graph.node(detachedKey); + if (detachedNode && detachedNode.modelNode) { + graph.setParent(detachedKey, parentKey); + detachedModelNodes.push(detachedNode.modelNode); + } + }); + let parentNode = graph.node(parentKey); + let {modelNode} = parentNode; + modelNode.children = modelNode.children || []; + let modelNodesArgs = [nodeIndex, 1].concat(detachedModelNodes); + modelNode.children.splice.apply(modelNode.children, modelNodesArgs); + adjustIndices(graph, parentKey); + } + } + resultKeys = resultKeys.concat(detachedKeys); + } else { + resultKeys.push(nodeKey); + } + } else { + console.warn('Copy & paste replace: replacing can not be provided for the same component.'); + } + }); + } + return resultKeys; +} +// +// export function copyPasteWrap (nodeKey) { +// const {graph} = graphObject +// const node = graph.node(nodeKey) +// if (!node) { +// throw Error('Copy & paste wrap: node with key ' + nodeKey + ' was not found.') +// } +// if (!node.prop) { +// const {forCopying} = getAllMarkedKeys() +// if (forCopying.length !== 1) { +// throw Error('Copy & paste wrap: wrapping can be applied only for single component') +// } +// const pastedKeys = copyPasteBeforeOrAfter(nodeKey, false) +// setForCutting(nodeKey) +// cutPasteFirstOrLast(pastedKeys[0], false) +// removeForCutting(nodeKey) +// return pastedKeys +// } else { +// return [nodeKey] +// } +// } + +export function setBuffer(model) { + const {graph} = graphObject; + if (bufferKey) { + removeBuffer(); + } + const newModel = fulex(model); + bufferKey = mapModel(graph, newModel, 0); + return bufferKey; +} + +export function removeBuffer() { + const {graph} = graphObject; + let childNode; + if (bufferKey) { + traverseGraphBranch(graph, bufferKey, childKey => { + childNode = graph.node(childKey); + if (childNode) { + delete childNode.modelNode; + graph.removeNode(childKey); + } + }); + bufferKey = undefined; + } +} + +export function fromBufferBeforeOrAfter(isAfter, quickKey) { + const {selected} = getAllMarkedKeys(); + const {graph} = graphObject; + let resultKeys = []; + if (selected && selected.length > 0) { + selected.forEach(nodeKey => { + const node = graph.node(nodeKey); + if (!node) { + console.error('From buffer to before or after node: node with key ' + nodeKey + ' was not found.'); + } else if (!node.prop) { + let detachedKey = copyGraphNode(graph, quickKey ? quickKey : bufferKey); + if (detachedKey) { + const parentKey = graph.parent(nodeKey); + if (parentKey) { + let detachedModelNodes = []; + let detachedNode = graph.node(detachedKey); + if (detachedNode && detachedNode.modelNode) { + graph.setParent(detachedKey, parentKey); + detachedModelNodes.push(detachedNode.modelNode); + } + let parentNode = graph.node(parentKey); + let {modelNode} = parentNode; + modelNode.children = modelNode.children || []; + let modelNodesArgs = [node.index + (isAfter ? 1 : 0), 0].concat(detachedModelNodes); + modelNode.children.splice.apply(modelNode.children, modelNodesArgs); + adjustIndices(graph, parentKey); + } + } + resultKeys.push(detachedKey); + } else { + resultKeys.push(nodeKey); + } + }); + } + return resultKeys; +} + +export function quickBeforeOrAfter(model, isAfter) { + const {graph} = graphObject; + let newModel = fulex(model); + let quickKey = mapModel(graph, newModel, 0); + const resultKeys = fromBufferBeforeOrAfter(isAfter, quickKey); + let childNode; + traverseGraphBranch(graph, quickKey, childKey => { + childNode = graph.node(childKey); + if (childNode) { + delete childNode.modelNode; + graph.removeNode(childKey); + } + }); + newModel = undefined; + quickKey = undefined; + return resultKeys; +} + +export function fromBufferFirstOrLast(isFirst, quickKey) { + const {selected} = getAllMarkedKeys(); + const {graph} = graphObject; + let resultKeys = []; + if (selected && selected.length > 0) { + selected.forEach(nodeKey => { + const node = graph.node(nodeKey); + if (!node) { + console.error('From buffer to first or last node: node with key ' + nodeKey + ' was not found.'); + } else { + let detachedKey = copyGraphNode(graph, quickKey ? quickKey : bufferKey); + if (detachedKey) { + + let detachedModelNodes = []; + let detachedNode = graph.node(detachedKey); + if (detachedNode && detachedNode.modelNode) { + graph.setParent(detachedKey, nodeKey); + detachedModelNodes.push(detachedNode.modelNode); + } + let {modelNode} = node; + modelNode.children = modelNode.children || []; + const lastIndex = modelNode.children.length > 0 ? modelNode.children.length : 0; + let modelNodesArgs = [(isFirst ? 0 : lastIndex), 0].concat(detachedModelNodes); + modelNode.children.splice.apply(modelNode.children, modelNodesArgs); + adjustIndices(graph, nodeKey); + } + resultKeys.push(detachedKey); + } + }); + } + return resultKeys; +} + +export function quickFirstOrLast(model, isFirst) { + const {graph} = graphObject; + let newModel = fulex(model); + let quickKey = mapModel(graph, newModel, 0); + const resultKeys = fromBufferFirstOrLast(isFirst, quickKey); + let childNode; + traverseGraphBranch(graph, quickKey, childKey => { + childNode = graph.node(childKey); + if (childNode) { + delete childNode.modelNode; + graph.removeNode(childKey); + } + }); + newModel = undefined; + quickKey = undefined; + return resultKeys; +} + +export function fromBufferReplace(quickKey) { + const {selected} = getAllMarkedKeys(); + const {graph} = graphObject; + let resultKeys = []; + if (selected && selected.length > 0) { + selected.forEach(nodeKey => { + const node = graph.node(nodeKey); + if (!node) { + console.error('From buffer replace: node with key ' + nodeKey + ' was not found.'); + } else if (!node.prop) { + let detachedKey = copyGraphNode(graph, quickKey ? quickKey : bufferKey); + if (detachedKey) { + const parentKey = graph.parent(nodeKey); + if (parentKey) { + const nodeIndex = node.index; + traverseGraphBranch(graph, nodeKey, (key => { + graph.removeNode(key); + })); + delete node.modelNode; + let detachedModelNodes = []; + let detachedNode = graph.node(detachedKey); + if (detachedNode && detachedNode.modelNode) { + graph.setParent(detachedKey, parentKey); + detachedModelNodes.push(detachedNode.modelNode); + } + let parentNode = graph.node(parentKey); + let {modelNode} = parentNode; + modelNode.children = modelNode.children || []; + let modelNodesArgs = [nodeIndex, 1].concat(detachedModelNodes); + modelNode.children.splice.apply(modelNode.children, modelNodesArgs); + adjustIndices(graph, parentKey); + } + } + resultKeys.push(detachedKey); + } else { + resultKeys.push(nodeKey); + } + }); + } + return resultKeys; +} + +export function quickReplace(model) { + const {graph} = graphObject; + let newModel = fulex(model); + let quickKey = mapModel(graph, newModel, 0); + const resultKeys = fromBufferReplace(quickKey); + let childNode; + traverseGraphBranch(graph, quickKey, childKey => { + childNode = graph.node(childKey); + if (childNode) { + delete childNode.modelNode; + graph.removeNode(childKey); + } + }); + newModel = undefined; + quickKey = undefined; + return resultKeys; +} + +//export function fromBufferWrap(nodeKey, quickKey){ +// const {graph} = graphObject; +// const node = graph.node(nodeKey); +// if(!node){ +// throw Error('From buffer wrap: node with key ' + nodeKey + ' was not found.'); +// } +// if(!node.prop){ +// const pastedKeys = fromBufferBeforeOrAfter(nodeKey, false, quickKey); +// setForCutting(nodeKey); +// cutPasteFirstOrLast(pastedKeys[0], false); +// removeForCutting(nodeKey); +// return pastedKeys; +// } else { +// return [nodeKey]; +// } +//} + +//export function quickWrap(model, nodeKey){ +// const {graph} = graphObject; +// let newModel = fulex(model); +// let quickKey = mapModel(graph, newModel, 0); +// const resultKeys = fromBufferWrap(nodeKey, quickKey); +// let childNode; +// traverseGraphBranch(graph, quickKey, childKey => { +// childNode = graph.node(childKey); +// if(childNode){ +// delete childNode.modelNode; +// graph.removeNode(childKey); +// } +// }); +// newModel = undefined; +// quickKey = undefined; +// return resultKeys; +//} + +export function cloneSelected() { + const {graph} = graphObject; + const {selected} = getAllMarkedKeys(); + let pastedKeys = []; + if (selected && selected.length > 0) { + + let clonedMap = new Map(); + let newNodeKey; + selected.forEach(selectedKey => { + const selectedNode = graph.node(selectedKey); + if (selectedNode && !selectedNode.prop) { + newNodeKey = copyGraphNode(graph, selectedKey); + clonedMap.set(selectedKey, newNodeKey); + } + }); + + clonedMap.forEach((newKey, targetKey) => { + const parentKey = graph.parent(targetKey); + let parentNode = graph.node(parentKey); + const selectedNode = graph.node(targetKey); + if (parentNode && selectedNode) { + let detachedModelNodes = []; + let detachedNode = graph.node(newKey); + if (detachedNode && detachedNode.modelNode) { + graph.setParent(newKey, parentKey); + detachedModelNodes.push(detachedNode.modelNode); + let {modelNode} = parentNode; + modelNode.children = modelNode.children || []; + let modelNodesArgs = [selectedNode.index + 1, 0].concat(detachedModelNodes); + modelNode.children.splice.apply(modelNode.children, modelNodesArgs); + adjustIndices(graph, parentKey); + pastedKeys.push(newKey); + } + + } + }); + } + return pastedKeys; +} + +export function moveSelected(isUp) { + const {graph} = graphObject; + const {selected} = getAllMarkedKeys(); + if (selected && selected.length > 0) { + + let parentMap = new Map(); + let parentKey; + let childrenGroup; + let child; + selected.forEach(selectedKey => { + child = graph.node(selectedKey); + if (child) { + parentKey = graph.parent(selectedKey); + if (parentKey) { + childrenGroup = parentMap.get(parentKey); + if (!childrenGroup) { + childrenGroup = []; + parentMap.set(parentKey, childrenGroup); + } + childrenGroup.push(child); + } + } + }); + let parentNode; + parentMap.forEach((group, parentKey) => { + parentNode = graph.node(parentKey); + let {modelNode} = parentNode; + if (modelNode.children && modelNode.children.length !== 1) { + if (isUp) { + group.sort((a, b) => a.index - b.index); + for (let u = 0; u < group.length; u++) { + if (group[u].index <= 0) { + break; + } + modelNode.children.splice(group[u].index, 1); + modelNode.children.splice(group[u].index - 1, 0, group[u].modelNode); + } + } else { + group.sort((a, b) => b.index - a.index); + for (let d = 0; d < group.length; d++) { + if (group[d].index >= modelNode.children.length - 1) { + break; + } + modelNode.children.splice(group[d].index, 1); + modelNode.children.splice(group[d].index + 1, 0, group[d].modelNode); + } + } + adjustIndices(graph, parentKey); + } + }); + } +} + +export function deleteSelected() { + const {graph, pageNodes} = graphObject; + const testFunc = node => node.selected; + let detachedKeys = []; + let resultKeys = []; + pageNodes.forEach(pNode => { + traverseGraphBranch(graph, pNode.pageKey, (key => { + let childNode = graph.node(key); + if (childNode.selected) { + const parentKey = graph.parent(key); + if (parentKey) { + //const parentNode = graph.node(parentKey); + const childKeys = graph.children(parentKey); + if (childKeys && childKeys.length > 1) { + let children = []; + childKeys.forEach(childKey => { + children.push({key: childKey, node: graph.node(childKey)}); + }); + children.sort((a, b) => a.node.index - b.node.index); + if (childNode.index === 0) { + resultKeys.push(children[1].key); + } else { + resultKeys.push(children[childNode.index - 1].key); + } + } else { + resultKeys.push(parentKey); + } + } + } + })); + detachGraphNodes(graph, pNode.pageKey, testFunc, detachedKeys); + }); + let newResultKeys = []; + if (detachedKeys.length > 0) { + resultKeys.forEach(key => { + if (detachedKeys.indexOf(key) < 0) { + newResultKeys.push(key); + } + }); + let childNode; + detachedKeys.forEach(key => { + traverseGraphBranch(graph, key, childKey => { + childNode = graph.node(childKey); + if (childNode) { + delete childNode.modelNode; + graph.removeNode(childKey); + } + }); + }); + } + return newResultKeys; +} + +export function changeModelNodeType(nodeKey, newModel) { + const {graph} = graphObject; + let node = graph.node(nodeKey); + const parentKey = graph.parent(nodeKey); + const detachedModel = fulex(newModel); + if (node && parentKey) { + traverseGraphBranch(graph, nodeKey, childKey => { + if (childKey !== nodeKey) { + let childNode = graph.node(childKey); + if (childNode) { + delete childNode.modelNode; + graph.removeNode(childKey); + } + } + }); + mapModelForNode(graph, detachedModel, node.index, undefined, undefined, nodeKey); + let parentNode = graph.node(parentKey); + let {modelNode} = parentNode; + modelNode.children = modelNode.children || []; + let modelNodesArgs = [node.index, 1].concat([detachedModel]); + modelNode.children.splice.apply(modelNode.children, modelNodesArgs); + adjustIndices(graph, parentKey); + } +} + +export function setForCutting(nodeKey) { + const {graph} = graphObject; + let node = graph.node(nodeKey); + if (node) { + node.isForCutting = true; + traverseGraphBranch(graph, nodeKey, (key => { + let childNode = graph.node(key); + childNode.isForCuttingChild = true; + })); + } +} + +export function removeForCutting(nodeKey) { + const {graph} = graphObject; + let node = graph.node(nodeKey); + if (node) { + node.isForCutting = undefined; + traverseGraphBranch(graph, nodeKey, (key => { + let childNode = graph.node(key); + childNode.isForCuttingChild = undefined; + })); + } +} + +export function setForCopying(nodeKey) { + const {graph} = graphObject; + let node = graph.node(nodeKey); + if (node) { + node.isForCopying = true; + } +} + +export function removeForCopying(nodeKey) { + const {graph} = graphObject; + let node = graph.node(nodeKey); + if (node) { + node.isForCopying = undefined; + } +} + +export function removeClipboardMarks(nodeKey) { + removeForCutting(nodeKey); + removeForCopying(nodeKey); +} + +export function getMarkedKeys(rootKey) { + let selected = []; + let highlighted = []; + let forCutting = []; + let forCopying = []; + const {graph} = graphObject; + traverseGraphBranch(graph, rootKey, (key => { + let childNode = graph.node(key); + if (childNode) { + if (childNode.highlighted) { + highlighted.push(key); + } + if (childNode.selected) { + selected.push(key); + } + if (childNode.isForCutting) { + forCutting.push(key); + } + if (childNode.isForCopying) { + forCopying.push(key); + } + } + })); + //console.log('Get marked keys, root: ' + rootKey + + // ', selected: ' + selected.length + + // ', highlighted: ' + highlighted.length + + // ', forCutting: ' + forCutting.length + + // ', forCopying: ' + forCopying.length); + return {selected, highlighted, forCutting, forCopying}; +} + +export function getAllMarkedKeys() { + const {graph, pageNodes} = graphObject; + let result = undefined; + pageNodes.forEach(pNode => { + if (!result) { + result = getMarkedKeys(pNode.pageKey); + } else { + const {selected, highlighted, forCutting, forCopying} = getMarkedKeys(pNode.pageKey); + result.selected = result.selected.concat(selected); + result.highlighted = result.highlighted.concat(highlighted); + result.forCutting = result.forCutting.concat(forCutting); + result.forCopying = result.forCopying.concat(forCopying); + } + }); + return result; +} diff --git a/src-client/api/model/graphUtils.js b/src-client/api/model/graphUtils.js new file mode 100644 index 00000000..001f6eab --- /dev/null +++ b/src-client/api/model/graphUtils.js @@ -0,0 +1,226 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { uniqueId, forOwn, isObject } from 'lodash'; +import { fulex } from '../utils/utils.js'; + +export function mapModel(srcGraph, rootModelNode, rootIndex, isNew, prop) { + const rootKey = uniqueId(); + srcGraph.setNode(rootKey, { modelNode: rootModelNode, index: rootIndex, prop }); + forOwn(rootModelNode.props, (value, prop) => { + if (isObject(value) && value.type) { + const childKey = mapModel(srcGraph, value, 0, isNew, prop); + srcGraph.setParent(childKey, rootKey); + } + }); + const { children } = rootModelNode; + if (children && children.length > 0) { + for (let i = 0; i < children.length; i++) { + const childKey = mapModel(srcGraph, children[i], i, isNew); + srcGraph.setParent(childKey, rootKey); + } + } + return rootKey; +} + +export function mapModelForNode(srcGraph, rootModelNode, rootIndex, isNew, prop, rootKey) { + srcGraph.setNode(rootKey, { modelNode: rootModelNode, index: rootIndex, prop }); + forOwn(rootModelNode.props, (value, prop) => { + if (isObject(value) && value.type) { + const childKey = mapModel(srcGraph, value, 0, isNew, prop); + srcGraph.setParent(childKey, rootKey); + } + }); + const { children } = rootModelNode; + if (children && children.length > 0) { + for (let i = 0; i < children.length; i++) { + const childKey = mapModel(srcGraph, children[i], i, isNew); + srcGraph.setParent(childKey, rootKey); + } + } + return rootKey; +} + +export function makeNodeWrapper(key, graphNode){ + return { + key: key, + modelNode: graphNode.modelNode, + index: graphNode.index, + prop: graphNode.prop, + selected: graphNode.selected, + highlighted: graphNode.highlighted, + isForCutting: graphNode.isForCutting, + isForCuttingChild: graphNode.isForCuttingChild, + isForCopying: graphNode.isForCopying + }; +} + +export function traverseGraphBranch(srcGraph, rootKey, cbFunc){ + const children = srcGraph.children(rootKey); + if(children && children.length){ + children.forEach(key => { + traverseGraphBranch(srcGraph, key, cbFunc); + }); + } + if(cbFunc){ + cbFunc(rootKey); + } +} + +export function adjustIndices(srcGraph, nodeKey){ + const parentNode = srcGraph.node(nodeKey); + const parentChildren = srcGraph.children(nodeKey); + if(parentNode && parentChildren && parentChildren.length > 0){ + let childNode; + parentChildren.forEach(childKey => { + childNode = srcGraph.node(childKey); + if(childNode){ + if(childNode.prop){ + childNode.index = 0; + } else if(parentNode.modelNode.children && parentNode.modelNode.children.length > 0){ + childNode.index = parentNode.modelNode.children.indexOf(childNode.modelNode); + } else { + childNode.index = 0; + } + } + }); + } +} + +export function detachGraphNode(srcGraph, nodeKey){ + let cutNode = srcGraph.node(nodeKey); + if(!cutNode){ + throw Error('Detach graph node: node key ' + nodeKey + ' was not found.'); + } + const parentNodeKey = srcGraph.parent(nodeKey); + if(parentNodeKey){ + let parentNode = srcGraph.node(parentNodeKey); + if(parentNode && parentNode.modelNode){ + let { modelNode: parentModelNode } = parentNode; + if(cutNode.prop){ + if(!parentModelNode.props[cutNode.prop]){ + throw Error('Detach graph node: model node with key ' + parentNodeKey + ' does not have prop ' + cutNode.prop); + } + parentModelNode.props[cutNode.prop] = undefined; + cutNode.prop = undefined; + } else if(parentModelNode.children && parentModelNode.children.length > 0) { + const childIndex = parentModelNode.children.indexOf(cutNode.modelNode); + if (childIndex < 0) { + throw Error('Detach graph node: child model node with key ' + nodeKey + ' does not belong to parent node with key ' + parentNodeKey); + } + parentModelNode.children.splice(childIndex, 1); + } else { + throw Error('Detach graph node: parent node ' + parentNodeKey + ' does not have children or needed prop.'); + } + adjustIndices(srcGraph, parentNodeKey); + } else { + throw Error('Detach graph node: parent node ' + parentNodeKey + ' is missing or does not have linked modelNode.') + } + srcGraph.setParent(nodeKey, undefined); + } + return nodeKey; +} + +export function detachGraphNodes(srcGraph, rootNodeKey, testFunc, detachedKeys){ + detachedKeys = detachedKeys || []; + const parentNode = srcGraph.node(rootNodeKey); + if(parentNode){ + let childKeys = srcGraph.children(rootNodeKey); + if(testFunc(parentNode)){ + detachedKeys.push(detachGraphNode(srcGraph, rootNodeKey)); + } + if(childKeys && childKeys.length > 0){ + let sortedChildrenNodes = []; + let propChildrenNodes = []; + let childNode; + childKeys.forEach(childKey => { + childNode = srcGraph.node(childKey); + if(childNode){ + if(childNode.prop){ + propChildrenNodes.push({ + key: childKey, + node: childNode + }); + } else { + sortedChildrenNodes.push({ + key: childKey, + node: childNode + }); + } + } + }); + if(sortedChildrenNodes.length > 0){ + sortedChildrenNodes.sort((a, b) => a.node.index - b.node.index); + } + propChildrenNodes.forEach(childNode => { + detachGraphNodes(srcGraph, childNode.key, testFunc, detachedKeys); + }); + sortedChildrenNodes.forEach(childNode => { + detachGraphNodes(srcGraph, childNode.key, testFunc, detachedKeys); + }); + } + } +} + +export function copyGraphNode(srcGraph, nodeKey){ + let copyNode = srcGraph.node(nodeKey); + if(!copyNode){ + throw Error('Copy graph node: node key ' + nodeKey + ' was not found.'); + } + let newModelNode = fulex(copyNode.modelNode); + return mapModel(srcGraph, newModelNode, 0, true); +} + +export function copyGraphNodes(srcGraph, rootNodeKey, testFunc, detachedKeys){ + detachedKeys = detachedKeys || []; + const parentNode = srcGraph.node(rootNodeKey); + if(parentNode){ + let childKeys = srcGraph.children(rootNodeKey); + if(testFunc(parentNode)){ + detachedKeys.push(copyGraphNode(srcGraph, rootNodeKey)); + } + if(childKeys && childKeys.length > 0){ + let sortedChildrenNodes = []; + let propChildrenNodes = []; + let childNode; + childKeys.forEach(childKey => { + childNode = srcGraph.node(childKey); + if(childNode){ + if(childNode.prop){ + propChildrenNodes.push({ + key: childKey, + node: childNode + }); + } else { + sortedChildrenNodes.push({ + key: childKey, + node: childNode + }); + } + } + }); + if(sortedChildrenNodes.length > 0){ + sortedChildrenNodes.sort((a, b) => a.node.index - b.node.index); + } + propChildrenNodes.forEach(childNode => { + copyGraphNodes(srcGraph, childNode.key, testFunc, detachedKeys); + }); + sortedChildrenNodes.forEach(childNode => { + copyGraphNodes(srcGraph, childNode.key, testFunc, detachedKeys); + }); + } + } +} diff --git a/src-client/api/model/previewGraphApi.js b/src-client/api/model/previewGraphApi.js new file mode 100644 index 00000000..473d1856 --- /dev/null +++ b/src-client/api/model/previewGraphApi.js @@ -0,0 +1,82 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Graph } from 'graphlib'; +import { fulex } from '../utils/utils.js'; + +import { mapModel, makeNodeWrapper } from './graphUtils.js'; + +let previewGraph; + +export function initGraph(initialModelsMap){ + if(initialModelsMap){ + previewGraph = new Graph({ compound: true }); + let variantKey; + initialModelsMap.forEach((value, key) => { + const model = fulex(value); + if(model && model.length > 0){ + previewGraph.setNode(key, model); + model.forEach((modelNode, index) => { + variantKey = mapModel(previewGraph, modelNode, index); + previewGraph.setParent(variantKey, key); + }) + } + }); + } +} + +export function getVariantKeys(componentKey){ + const children = previewGraph.children(componentKey); + return children && children.length > 0 ? children : []; +} + +export function getWrappedModelForVariant(variantKey){ + return traverseGraph(variantKey); +} + +export function getModelForVariant(variantKey){ + let node = previewGraph.node(variantKey); + if(node){ + return node.modelNode; + } + return undefined; +} + +function traverseGraph(rootNodeKey, result){ + let rootNode = previewGraph.node(rootNodeKey); + if(rootNode){ + let resultNode = makeNodeWrapper(rootNodeKey, rootNode); + let children = previewGraph.children(rootNodeKey); + if(children && children.length > 0){ + children.forEach(child => { + traverseGraph(child, resultNode); + }); + if(resultNode.children && resultNode.children.length > 0){ + resultNode.children.sort((a, b) => a.index - b.index); + } + } + if(!result){ + result = resultNode; + } else if(rootNode.prop){ + result.props = result.props || {}; + result.props[rootNode.prop] = resultNode; + } else { + result.children = result.children || []; + result.children.push(resultNode); + } + } + return result; +} \ No newline at end of file diff --git a/src-client/api/model/reactRouterApi.js b/src-client/api/model/reactRouterApi.js new file mode 100644 index 00000000..2b2719c2 --- /dev/null +++ b/src-client/api/model/reactRouterApi.js @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const reg = /(\/.+?)(?:#|\?|$)/; + +export function getAvailableRoute (existingRoutes, checkPathname) { + let candidateRootKey = undefined; + if (existingRoutes && existingRoutes.length > 0) { + if (checkPathname === '/' || checkPathname === '/structor-deskpage' || checkPathname === '/structor-deskpage/') { + candidateRootKey = existingRoutes[0]; + } else { + const match = reg.exec(checkPathname); + if (match && match.length > 0) { + candidateRootKey = existingRoutes.find(i => match[1] === i); + } + } + } + if (!candidateRootKey) { + console.error('Path name ' + checkPathname + ' was not found in project model.'); + } + return candidateRootKey; +} diff --git a/src-client/api/model/sandboxGraphApi.js b/src-client/api/model/sandboxGraphApi.js new file mode 100644 index 00000000..4aeb7ff1 --- /dev/null +++ b/src-client/api/model/sandboxGraphApi.js @@ -0,0 +1,79 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Graph } from 'graphlib'; +import { fulex } from '../utils/utils.js'; + +import { mapModel, makeNodeWrapper } from './graphUtils.js'; + +let sandboxGraph; +const rootNodeKey = 'ROOT_KEY'; + +export function initGraph(initialModel){ + if(initialModel && initialModel.length > 0){ + sandboxGraph = new Graph({ compound: true }); + let variantKey; + const model = fulex(initialModel); + sandboxGraph.setNode(rootNodeKey, model); + model.forEach((modelNode, index) => { + variantKey = mapModel(sandboxGraph, modelNode, index); + sandboxGraph.setParent(variantKey, rootNodeKey); + }); + } +} + +export function getVariantKeys(){ + const children = sandboxGraph.children(rootNodeKey); + return children && children.length > 0 ? children : []; +} + +export function getWrappedModelForVariant(variantKey){ + return traverseGraph(variantKey); +} + +export function getModelForVariant(variantKey){ + let node = sandboxGraph.node(variantKey); + if(node){ + return node.modelNode; + } + return undefined; +} + +function traverseGraph(rootNodeKey, result){ + let rootNode = sandboxGraph.node(rootNodeKey); + if(rootNode){ + let resultNode = makeNodeWrapper(rootNodeKey, rootNode); + let children = sandboxGraph.children(rootNodeKey); + if(children && children.length > 0){ + children.forEach(child => { + traverseGraph(child, resultNode); + }); + if(resultNode.children && resultNode.children.length > 0){ + resultNode.children.sort((a, b) => a.index - b.index); + } + } + if(!result){ + result = resultNode; + } else if(rootNode.prop){ + result.props = result.props || {}; + result.props[rootNode.prop] = resultNode; + } else { + result.children = result.children || []; + result.children.push(resultNode); + } + } + return result; +} \ No newline at end of file diff --git a/src/client/src/api/overlays.js b/src-client/api/overlays.js similarity index 89% rename from src/client/src/api/overlays.js rename to src-client/api/overlays.js index 8eb90978..86a76f53 100644 --- a/src/client/src/api/overlays.js +++ b/src-client/api/overlays.js @@ -1,3 +1,19 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as DeskPageActions from '../actions/deskPageActions.js'; export function createComponentOverlay(actions, frameWindow, domNodeId, componentType, options) { diff --git a/src/client/src/api/cookies.js b/src-client/api/utils/cookies.js similarity index 80% rename from src/client/src/api/cookies.js rename to src-client/api/utils/cookies.js index e80c3b0c..95a2aa06 100644 --- a/src/client/src/api/cookies.js +++ b/src-client/api/utils/cookies.js @@ -1,3 +1,19 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use strict'; /*\ diff --git a/src-client/api/utils/printProps.js b/src-client/api/utils/printProps.js new file mode 100644 index 00000000..35e66161 --- /dev/null +++ b/src-client/api/utils/printProps.js @@ -0,0 +1,76 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import _ from 'lodash'; + +const lineTemplate = _.template( +`<%= processProps(props) %>`); + +function processStyle(styleObject){ + var result = ''; + if(styleObject && !_.isEmpty(styleObject)){ + _.forOwn(styleObject, function(value, prop){ + if(_.isString(value) && value.length > 0){ + result += ' ' + prop + ": '" + value + "',"; + } else if(_.isBoolean(value) || _.isNumber(value)){ + result += ' ' + prop + ": " + value + ","; + } + }); + result = result.substr(0, result.length - 1); + } + return result; +} + +function processProps(props){ + + var result = ''; + if(props && !_.isEmpty(props)){ + _.forOwn(props, function(value, prop){ + if(_.isString(value) && value.length > 0){ + result += prop + "=\"" + value + "\" "; + } else if(_.isBoolean(value) || _.isNumber(value)){ + result += prop + "={" + value + "} "; + } else if(_.isArray(value)){ + var arrayString = ''; + _.forEach(value, function(item){ + if(_.isObject(item)){ + arrayString += '{ ' + processStyle(item) + ' },'; + } else { + if(_.isString(item) && item.length > 0){ + arrayString += "\'" + item + "\',"; + } else if(_.isBoolean(item) || _.isNumber(item)){ + arrayString += item + ','; + } + } + }); + result += prop + '={[ ' + arrayString.substr(0, arrayString.length - 1) + ']} '; + } else if(_.isObject(value)){ + if(value['type']){ + result += prop +"={ " + value.type + " } "; + } else { + result += prop + "={{ " + processStyle(value) + " }} "; + } + } + }); + } + return result; +} + + +export function printProps(props){ + return lineTemplate({props, processProps, processStyle}); +} + diff --git a/src-client/api/utils/styleProps.js b/src-client/api/utils/styleProps.js new file mode 100644 index 00000000..36ab64cf --- /dev/null +++ b/src-client/api/utils/styleProps.js @@ -0,0 +1,119 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const prefix = 'style.'; + +let stylePropsList = undefined; + +const names = [ + 'alignContent', + 'alignItems', + 'background', + 'backgroundAttachment', + 'backgroundColor', + 'backgroundImage', + 'backgroundPosition', + 'backgroundRepeat', + 'border', + 'borderBottom', + 'borderBottomColor', + 'borderBottomStyle', + 'borderBottomWidth', + 'borderColor', + 'borderLeft', + 'borderLeftColor', + 'borderLeftStyle', + 'borderLeftWidth', + 'borderRadius', + 'borderRight', + 'borderRightColor', + 'borderRightStyle', + 'borderRightWidth', + 'borderStyle', + 'borderTop', + 'borderTopColor', + 'borderTopStyle', + 'borderTopWidth', + 'borderWidth', + 'bottom', + 'clear', + 'clip', + 'color', + 'cursor', + 'display', + 'filter', + 'flexDirection', + 'flexFlow', + 'flexWrap', + 'font', + 'fontFamily', + 'fontSize', + 'fontVariant', + 'fontWeight', + 'height', + 'justifyContent', + 'left', + 'letterSpacing', + 'lineHeight', + 'listStyle', + 'listStyleImage', + 'listStylePosition', + 'listStyleType', + 'margin', + 'marginBottom', + 'marginLeft', + 'marginRight', + 'marginTop', + 'maxHeight', + 'maxWidth', + 'minHeight', + 'minWidth', + 'overflow', + 'right', + 'padding', + 'paddingBottom', + 'paddingLeft', + 'paddingRight', + 'paddingTop', + 'pageBreakAfter', + 'pageBreakBefore', + 'position', + 'cssFloat', + 'textAlign', + 'textDecoration', + 'textDecorationBlink', + 'textDecorationLineThrough', + 'textDecorationNone', + 'textDecorationOverline', + 'textDecorationUnderline', + 'textIndent', + 'textTransform', + 'top', + 'verticalAlign', + 'visibility', + 'width', + 'zIndex' +]; + +export function getStylePropList(){ + if(!stylePropsList){ + stylePropsList = []; + names.forEach(name => { + stylePropsList.push(prefix + name); + }); + } + return stylePropsList; +} diff --git a/src-client/api/utils/utils.js b/src-client/api/utils/utils.js new file mode 100644 index 00000000..21bd68b2 --- /dev/null +++ b/src-client/api/utils/utils.js @@ -0,0 +1,178 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import _ from 'lodash'; +import cookies from './cookies.js'; + +export function fulfil(obj1, obj2) { + if (_.isArray(obj2)) { + if (!obj1 || obj1 == null) { + obj1 = []; + for (let i = 0; i < obj2.length; i++) { + obj1.push(fulfil(null, obj2[i])); + } + } + } else if (_.isObject(obj2)) { + if (!obj1) { + obj1 = {}; + } + let items = Object.getOwnPropertyNames(obj2); + for (let item = 0; item < items.length; item++) { + obj1[items[item]] = fulfil(obj1[items[item]], obj2[items[item]]); + } + } else { + if (obj1 == undefined) { + obj1 = obj2; + } + } + return obj1; +} + +export function fulex(obj2) { + let obj1 = null; + if (_.isArray(obj2)) { + obj1 = []; + for (let i = 0; i < obj2.length; i++) { + obj1.push(fulex(obj2[i])); + } + } else if (_.isObject(obj2)) { + obj1 = {}; + for (let item in obj2) { + if (obj2.hasOwnProperty(item)) { + obj1[item] = fulex(obj2[item]); + } + } + } else { + obj1 = obj2; + } + return obj1; +} + +export function delex(obj, path) { + let pathArray = path.split('.'); + if (pathArray.length === 1 && obj[path] !== undefined) { + delete obj[path]; + } else { + let tempObj = obj; + pathArray.map(function (step, index) { + if (index === pathArray.length - 1 && tempObj[step] !== undefined) { + delete tempObj[step]; + } else { + tempObj = tempObj[step]; + } + }); + } + return obj; +} + +export function cleanex(obj2) { + let obj1 = null; + let tempObj = null; + if (_.isArray(obj2)) { + obj1 = []; + for (let i = 0; i < obj2.length; i++) { + tempObj = cleanex(obj2[i]); + if (_.isObject(tempObj)) { + if (!_.isEmpty(tempObj)) { + obj1.push(tempObj); + } + } else { + obj1.push(tempObj); + } + } + } else if (_.isObject(obj2)) { + obj1 = {}; + for (let item in obj2) { + if (obj2.hasOwnProperty(item)) { + tempObj = cleanex(obj2[item]); + if (_.isObject(tempObj)) { + if (!_.isEmpty(tempObj)) { + obj1[item] = tempObj; + } + } else { + obj1[item] = tempObj; + } + } + } + } else { + if (_.isObject(obj2)) { + if (!_.isEmpty(obj2)) { + obj1 = obj2; + } + } else { + obj1 = obj2; + } + } + return obj1; +} + +export function isVisible(element) { + let invisibleParent = false; + if ($(element).css("display") === "none") { + invisibleParent = true; + } else { + $(element).parents().each(function (i, el) { + if ($(el).css("display") === "none") { + invisibleParent = true; + return false; + } + return true; + }); + } + return !invisibleParent; +} + +export function trimComponentName(label, width = 50){ + if(label.length > width){ + label = label.substr(0, width) + '...'; + } + return label; +} + +export function cleanPropsUmyId(modelItem) { + if (modelItem.props && modelItem.props['data-umyid']) { + modelItem.props['data-umyid'] = undefined; + delete modelItem.props['data-umyid']; + } + _.forOwn(modelItem.props, (value, prop) => { + if (_.isObject(value) && value.type) { + cleanPropsUmyId(value); + } + }); + if (modelItem.children && modelItem.children.length > 0) { + for (let i = 0; i < modelItem.children.length; i++) { + cleanPropsUmyId(modelItem.children[i]); + } + } +} + +export function saveCookiesObject(key, obj){ + const stringRepresentation = JSON.stringify(obj); + cookies.setItem(key, stringRepresentation, 31536e3, "/"); +} + +export function retrieveCookiesObject(key){ + const stringRepresentation = cookies.getItem(key); + let result = undefined; + if(stringRepresentation){ + try{ + result = JSON.parse(stringRepresentation); + } catch(e){ + console.warn('Cookies has been corrupted.'); + } + } + return result; +} diff --git a/src-client/api/utils/utilsStore.js b/src-client/api/utils/utilsStore.js new file mode 100644 index 00000000..b239dd8d --- /dev/null +++ b/src-client/api/utils/utilsStore.js @@ -0,0 +1,129 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { cloneDeep } from 'lodash'; + +export function findComponentDef (componentTree, componentName, namespace, defaultsIndex = 0) { + let componentDef = undefined; + if (componentName && namespace) { + if (componentTree.modules) { + let module = componentTree.modules[namespace]; + if (module && module.components) { + componentDef = module.components[componentName]; + } else { + throw Error(`Namespace module ${namespace} was not found.`); + } + } + } else if (componentName) { + if (componentTree.htmlComponents) { + componentDef = componentTree.htmlComponents[componentName]; + } + if (!componentDef && componentTree.components) { + componentDef = componentTree.components[componentName]; + } + } + if (componentDef) { + const {defaults} = componentDef; + if (defaults && defaults.length > defaultsIndex) { + return componentDef; + } else { + throw Error(`Model definition for ${componentName} component is missing.`); + } + } else { + if (namespace) { + throw Error(`Component ${componentName} in namespace ${namespace} was not found.`); + } else { + throw Error(`Component ${componentName} was not found.`); + } + } +} + +export function getComponentTupleModel (componentTree, componentNames) { + let tupleModel = undefined; + if (componentNames && componentNames.length > 0) { + let componentName; + let namespace; + let firstBracePos; + let secondBracePos; + let componentDef; + componentNames.forEach(name => { + firstBracePos = name.indexOf('['); + secondBracePos = name.indexOf(']'); + if (firstBracePos > 0 && secondBracePos > 0) { + componentName = name.substr(0, firstBracePos).trim(); + namespace = name.substr(firstBracePos + 1, secondBracePos - firstBracePos - 1); + } else { + componentName = name.trim(); + namespace = undefined; + } + try { + componentDef = findComponentDef(componentTree, componentName, namespace); + const {defaults} = componentDef; + if (tupleModel) { + tupleModel.children = [cloneDeep(defaults[0])]; + } else { + tupleModel = cloneDeep(defaults[0]); + } + } catch (e) { + // do nothing; + console.error('Error in searching the component: ', e); + } + }); + } + return tupleModel; +} + +export function getTemplatePageModel () { + return { + pageName: 'UnnamedPage', + pagePath: '/UnnamedPage', + children: [ + { + type: 'div', + props: { + style: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + padding: '1em', + } + }, + children: [ + { + type: 'div', + children: [ + { + type: 'h3', + props: { + style: { + padding: '1em', + textAlign: 'center' + } + }, + children: [ + { + type: 'span', + text: 'Click on me and start creating a new cool component.' + } + ] + } + ] + } + ] + } + ] + }; +} \ No newline at end of file diff --git a/src-client/assets/app/css/img/screenshot.png b/src-client/assets/app/css/img/screenshot.png new file mode 100644 index 00000000..108e90f7 Binary files /dev/null and b/src-client/assets/app/css/img/screenshot.png differ diff --git a/src/client/css/img/umylogo-final.svg b/src-client/assets/app/css/img/umylogo-final.svg similarity index 90% rename from src/client/css/img/umylogo-final.svg rename to src-client/assets/app/css/img/umylogo-final.svg index 5cc12793..f08f42de 100644 --- a/src/client/css/img/umylogo-final.svg +++ b/src-client/assets/app/css/img/umylogo-final.svg @@ -1,4 +1,20 @@ + + + + .treeview-placeholder-base > .treeview-placeholder-circle { + top: -.6em; + left: -.6em; + width: 1.2em; + height: 1.2em; + border: 1px solid #999; + background-color: rgb(240, 173, 78); +} + +.treeview-placeholder:hover > .treeview-placeholder-base > .treeview-placeholder-bar { + top: -.6em; + left: -.6em; + height: 1.2em; + border: 1px solid #999; + background-color: rgb(240, 173, 78); +} + +.treeview-placeholder:hover > .treeview-placeholder-base > .treeview-placeholder-line { + display: block; +} + +.treeview-placeholder:hover > .treeview-placeholder-base > .treeview-placeholder-icon { + display: block; +} + +.treeview-panel-button { + opacity: 0.7; +} + +.treeview-panel-button:hover { + opacity: 1; +} + +.dimension-box { + position: relative; + border: 1px solid #fff; + border-radius: 4px; + padding: 20px; +} + +.dimension-position { + background-color: #dff0d8; +} + +.dimension-margin { + background-color: #d9edf7; +} + +.dimension-border { + background-color: #fcf8e3; +} + +.dimension-padding { + background-color: #f2dede; +} + +.dimension-center { + background-color: #ffffff; + width: 100%; + height: 40px; +} + +.dimension-btn { + position: absolute; + width: 15px; + height: 15px; + background-color: transparent; + color: #ffffff; + border-radius: 50%; + padding: 2px; + font-size: 9px; + text-align: center; + vertical-align: middle; + font-weight: 100; + cursor: pointer; + z-index: 5; +} + +.dimension-btn:hover { + background-color: #35b3ee; + color: #ffffff; +} + +.dimension-btn-inner { + font-size: 9px; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; +} + +.dimension-border-left { + position: absolute; + top: 0; + left: 0; + width: 20px; + bottom: 0; + background-color: transparent; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +.dimension-border-left:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.dimension-border-top { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 20px; + background-color: transparent; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.dimension-border-top:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.dimension-border-right { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 20px; + background-color: transparent; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.dimension-border-right:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.dimension-border-bottom { + position: absolute; + left: 0; + bottom: 0; + right: 0; + height: 20px; + background-color: transparent; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +.dimension-border-bottom:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.rotate-clockwise { + /* Safari */ + -webkit-transform: rotate(90deg); + /* Firefox */ + -moz-transform: rotate(90deg); + /* IE */ + -ms-transform: rotate(90deg); + /* Opera */ + -o-transform: rotate(90deg); + + transform: rotate(90deg); + display: inline-block; + vertical-align: top; + margin: .2em 0 0 0; +} + +.mouse-center-point { + position: absolute; + top: 0; + left: 0; + width: 1px; + height: 1px; +} diff --git a/src/client/lib/bootstrap/css/cerulean/bootstrap.min.css b/src-client/assets/bootstrap/css/cerulean/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/cerulean/bootstrap.min.css rename to src-client/assets/bootstrap/css/cerulean/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/cerulean/bootswatch.less b/src-client/assets/bootstrap/css/cerulean/bootswatch.less similarity index 100% rename from src/client/lib/bootstrap/css/cerulean/bootswatch.less rename to src-client/assets/bootstrap/css/cerulean/bootswatch.less diff --git a/src/client/lib/bootstrap/css/cerulean/variables.less b/src-client/assets/bootstrap/css/cerulean/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/cerulean/variables.less rename to src-client/assets/bootstrap/css/cerulean/variables.less diff --git a/src/client/lib/bootstrap/css/custom/bootstrap.css b/src-client/assets/bootstrap/css/custom/bootstrap.css similarity index 95% rename from src/client/lib/bootstrap/css/custom/bootstrap.css rename to src-client/assets/bootstrap/css/custom/bootstrap.css index d09f7304..966cb8cc 100755 --- a/src/client/lib/bootstrap/css/custom/bootstrap.css +++ b/src-client/assets/bootstrap/css/custom/bootstrap.css @@ -1,6 +1,6 @@ /* bootstrap default */ /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ -@import url(https://fonts.googleapis.com/css?family=Roboto:400,300,700,500); +@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500); html { font-family: sans-serif; -ms-text-size-adjust: 100%; @@ -884,8 +884,8 @@ body { font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.42857143; - color: #666666; - background-color: #ffffff; + color: #666; + background-color: #f5f5f5; } input, button, @@ -896,12 +896,12 @@ textarea { line-height: inherit; } a { - color: #4183c4; + color: #4183C4; text-decoration: none; } a:hover, a:focus { - color: #4183c4; + color: #4183C4; text-decoration: underline; } a:focus { @@ -930,8 +930,8 @@ img { .img-thumbnail { padding: 4px; line-height: 1.42857143; - background-color: #ffffff; - border: 1px solid #dddddd; + background-color: #f5f5f5; + border: 1px solid #ddd; border-radius: 4px; transition: all 0.2s ease-in-out; display: inline-block; @@ -1132,7 +1132,7 @@ mark, color: #999999; } .text-primary { - color: #2185d0; + color: #2185D0; } a.text-primary:hover { color: #1a69a4; @@ -1163,7 +1163,7 @@ a.text-danger:hover { } .bg-primary { color: #fff; - background-color: #2185d0; + background-color: #2185D0; } a.bg-primary:hover { background-color: #1a69a4; @@ -1328,8 +1328,8 @@ code { kbd { padding: 2px 4px; font-size: 90%; - color: #ffffff; - background-color: #333333; + color: #fff; + background-color: #333; border-radius: 3px; box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); } @@ -1349,7 +1349,7 @@ pre { word-wrap: break-word; color: #333333; background-color: #f5f5f5; - border: 1px solid #cccccc; + border: 1px solid #ccc; border-radius: 4px; } pre code { @@ -2057,11 +2057,11 @@ th { padding: 8px; line-height: 1.42857143; vertical-align: top; - border-top: 1px solid #dddddd; + border-top: 1px solid #ddd; } .table > thead > tr > th { vertical-align: bottom; - border-bottom: 2px solid #dddddd; + border-bottom: 2px solid #ddd; } .table > caption + thead > tr:first-child > th, .table > colgroup + thead > tr:first-child > th, @@ -2072,10 +2072,10 @@ th { border-top: 0; } .table > tbody + tbody { - border-top: 2px solid #dddddd; + border-top: 2px solid #ddd; } .table .table { - background-color: #ffffff; + background-color: #f5f5f5; } .table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, @@ -2086,7 +2086,7 @@ th { padding: 5px; } .table-bordered { - border: 1px solid #dddddd; + border: 1px solid #ddd; } .table-bordered > thead > tr > th, .table-bordered > tbody > tr > th, @@ -2094,7 +2094,7 @@ th { .table-bordered > thead > tr > td, .table-bordered > tbody > tr > td, .table-bordered > tfoot > tr > td { - border: 1px solid #dddddd; + border: 1px solid #ddd; } .table-bordered > thead > tr > th, .table-bordered > thead > tr > td { @@ -2232,7 +2232,7 @@ table th[class*="col-"] { margin-bottom: 15px; overflow-y: hidden; -ms-overflow-style: -ms-autohiding-scrollbar; - border: 1px solid #dddddd; + border: 1px solid #ddd; } .table-responsive > .table { margin-bottom: 0; @@ -2284,7 +2284,7 @@ legend { margin-bottom: 20px; font-size: 21px; line-height: inherit; - color: #666666; + color: #666; border: 0; border-bottom: 1px solid #e5e5e5; } @@ -2326,7 +2326,7 @@ output { padding-top: 9px; font-size: 14px; line-height: 1.42857143; - color: #666666; + color: #666; } .form-control { display: block; @@ -2335,10 +2335,10 @@ output { padding: 8px 12px; font-size: 14px; line-height: 1.42857143; - color: #666666; - background-color: #ffffff; + color: #666; + background-color: #fff; background-image: none; - border: 1px solid #cccccc; + border: 1px solid #ccc; border-radius: 4px; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; @@ -2747,7 +2747,7 @@ select[multiple].form-group-lg .form-control { .btn:hover, .btn:focus, .btn.focus { - color: #333333; + color: #333; text-decoration: none; } .btn:active, @@ -2766,9 +2766,9 @@ fieldset[disabled] .btn { box-shadow: none; } .btn-default { - color: #333333; - background-color: #ffffff; - border-color: #cccccc; + color: #333; + background-color: #fff; + border-color: #ccc; } .btn-default:hover, .btn-default:focus, @@ -2776,7 +2776,7 @@ fieldset[disabled] .btn { .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default { - color: #333333; + color: #333; background-color: #e6e6e6; border-color: #adadad; } @@ -2803,17 +2803,17 @@ fieldset[disabled] .btn-default:active, .btn-default.disabled.active, .btn-default[disabled].active, fieldset[disabled] .btn-default.active { - background-color: #ffffff; - border-color: #cccccc; + background-color: #fff; + border-color: #ccc; } .btn-default .badge { - color: #ffffff; - background-color: #333333; + color: #fff; + background-color: #333; } .btn-primary { - color: #ffffff; - background-color: #2185d0; - border-color: #2185d0; + color: #fff; + background-color: #2185D0; + border-color: #2185D0; } .btn-primary:hover, .btn-primary:focus, @@ -2821,7 +2821,7 @@ fieldset[disabled] .btn-default.active { .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { - color: #ffffff; + color: #fff; background-color: #1a69a4; border-color: #19639b; } @@ -2848,17 +2848,17 @@ fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { - background-color: #2185d0; - border-color: #2185d0; + background-color: #2185D0; + border-color: #2185D0; } .btn-primary .badge { - color: #2185d0; - background-color: #ffffff; + color: #2185D0; + background-color: #fff; } .btn-success { - color: #ffffff; - background-color: #78cb5b; - border-color: #78cb5b; + color: #fff; + background-color: #78CB5B; + border-color: #78CB5B; } .btn-success:hover, .btn-success:focus, @@ -2866,7 +2866,7 @@ fieldset[disabled] .btn-primary.active { .btn-success:active, .btn-success.active, .open > .dropdown-toggle.btn-success { - color: #ffffff; + color: #fff; background-color: #5bb83b; border-color: #57b138; } @@ -2893,17 +2893,17 @@ fieldset[disabled] .btn-success:active, .btn-success.disabled.active, .btn-success[disabled].active, fieldset[disabled] .btn-success.active { - background-color: #78cb5b; - border-color: #78cb5b; + background-color: #78CB5B; + border-color: #78CB5B; } .btn-success .badge { - color: #78cb5b; - background-color: #ffffff; + color: #78CB5B; + background-color: #fff; } .btn-info { - color: #ffffff; - background-color: #4183c4; - border-color: #4183c4; + color: #fff; + background-color: #4183C4; + border-color: #4183C4; } .btn-info:hover, .btn-info:focus, @@ -2911,7 +2911,7 @@ fieldset[disabled] .btn-success.active { .btn-info:active, .btn-info.active, .open > .dropdown-toggle.btn-info { - color: #ffffff; + color: #fff; background-color: #3269a0; border-color: #2f6498; } @@ -2938,17 +2938,17 @@ fieldset[disabled] .btn-info:active, .btn-info.disabled.active, .btn-info[disabled].active, fieldset[disabled] .btn-info.active { - background-color: #4183c4; - border-color: #4183c4; + background-color: #4183C4; + border-color: #4183C4; } .btn-info .badge { - color: #4183c4; - background-color: #ffffff; + color: #4183C4; + background-color: #fff; } .btn-warning { - color: #ffffff; - background-color: #d67c1c; - border-color: #d67c1c; + color: #fff; + background-color: #D67C1C; + border-color: #D67C1C; } .btn-warning:hover, .btn-warning:focus, @@ -2956,7 +2956,7 @@ fieldset[disabled] .btn-info.active { .btn-warning:active, .btn-warning.active, .open > .dropdown-toggle.btn-warning { - color: #ffffff; + color: #fff; background-color: #a96216; border-color: #a05d15; } @@ -2983,15 +2983,15 @@ fieldset[disabled] .btn-warning:active, .btn-warning.disabled.active, .btn-warning[disabled].active, fieldset[disabled] .btn-warning.active { - background-color: #d67c1c; - border-color: #d67c1c; + background-color: #D67C1C; + border-color: #D67C1C; } .btn-warning .badge { - color: #d67c1c; - background-color: #ffffff; + color: #D67C1C; + background-color: #fff; } .btn-danger { - color: #ffffff; + color: #fff; background-color: #ee5f5b; border-color: #ee5f5b; } @@ -3001,7 +3001,7 @@ fieldset[disabled] .btn-warning.active { .btn-danger:active, .btn-danger.active, .open > .dropdown-toggle.btn-danger { - color: #ffffff; + color: #fff; background-color: #e9322d; border-color: #e82924; } @@ -3033,10 +3033,10 @@ fieldset[disabled] .btn-danger.active { } .btn-danger .badge { color: #ee5f5b; - background-color: #ffffff; + background-color: #fff; } .btn-link { - color: #4183c4; + color: #4183C4; font-weight: normal; border-radius: 0; } @@ -3056,7 +3056,7 @@ fieldset[disabled] .btn-link { } .btn-link:hover, .btn-link:focus { - color: #4183c4; + color: #4183C4; text-decoration: underline; background-color: transparent; } @@ -3158,8 +3158,8 @@ tbody.collapse.in { list-style: none; font-size: 14px; text-align: left; - background-color: #ffffff; - border: 1px solid #cccccc; + background-color: #fff; + border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, 0.15); border-radius: 4px; box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); @@ -3187,16 +3187,16 @@ tbody.collapse.in { .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { text-decoration: none; - color: #ffffff; - background-color: #2185d0; + color: #fff; + background-color: #2185D0; } .dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus { - color: #ffffff; + color: #fff; text-decoration: none; outline: 0; - background-color: #2185d0; + background-color: #2185D0; } .dropdown-menu > .disabled > a, .dropdown-menu > .disabled > a:hover, @@ -3517,10 +3517,10 @@ select[multiple].input-group-sm > .input-group-btn > .btn { font-size: 14px; font-weight: normal; line-height: 1; - color: #666666; + color: #666; text-align: center; background-color: #eeeeee; - border: 1px solid #cccccc; + border: 1px solid #ccc; border-radius: 4px; } .input-group-addon.input-sm { @@ -3620,7 +3620,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .nav .open > a:hover, .nav .open > a:focus { background-color: #eeeeee; - border-color: #4183c4; + border-color: #4183C4; } .nav .nav-divider { height: 1px; @@ -3632,7 +3632,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { max-width: none; } .nav-tabs { - border-bottom: 1px solid #dddddd; + border-bottom: 1px solid #ddd; } .nav-tabs > li { float: left; @@ -3645,14 +3645,14 @@ select[multiple].input-group-sm > .input-group-btn > .btn { border-radius: 4px 4px 0 0; } .nav-tabs > li > a:hover { - border-color: #eeeeee #eeeeee #dddddd; + border-color: #eeeeee #eeeeee #ddd; } .nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus { - color: #666666; - background-color: #ffffff; - border: 1px solid #dddddd; + color: #666; + background-color: #f5f5f5; + border: 1px solid #ddd; border-bottom-color: transparent; cursor: default; } @@ -3687,17 +3687,17 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .nav-tabs.nav-justified > .active > a, .nav-tabs.nav-justified > .active > a:hover, .nav-tabs.nav-justified > .active > a:focus { - border: 1px solid #dddddd; + border: 1px solid #ddd; } @media (min-width: 768px) { .nav-tabs.nav-justified > li > a { - border-bottom: 1px solid #dddddd; + border-bottom: 1px solid #ddd; border-radius: 4px 4px 0 0; } .nav-tabs.nav-justified > .active > a, .nav-tabs.nav-justified > .active > a:hover, .nav-tabs.nav-justified > .active > a:focus { - border-bottom-color: #ffffff; + border-bottom-color: #f5f5f5; } } .nav-pills > li { @@ -3712,8 +3712,8 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { - color: #ffffff; - background-color: #2185d0; + color: #fff; + background-color: #2185D0; } .nav-stacked > li { float: none; @@ -3755,17 +3755,17 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > a:hover, .nav-tabs-justified > .active > a:focus { - border: 1px solid #dddddd; + border: 1px solid #ddd; } @media (min-width: 768px) { .nav-tabs-justified > li > a { - border-bottom: 1px solid #dddddd; + border-bottom: 1px solid #ddd; border-radius: 4px 4px 0 0; } .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > a:hover, .nav-tabs-justified > .active > a:focus { - border-bottom-color: #ffffff; + border-bottom-color: #f5f5f5; } } .tab-content > .tab-pane { @@ -4106,38 +4106,38 @@ select[multiple].input-group-sm > .input-group-btn > .btn { } } .navbar-default { - background-color: #eeeeee; + background-color: #eee; border-color: #dddddd; } .navbar-default .navbar-brand { - color: #777777; + color: #777; } .navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus { - color: #4183c4; + color: #4183C4; background-color: transparent; } .navbar-default .navbar-text { - color: #777777; + color: #777; } .navbar-default .navbar-nav > li > a { - color: #777777; + color: #777; } .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus { - color: #4183c4; + color: #4183C4; background-color: transparent; } .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus { - color: #4183c4; + color: #4183C4; background-color: transparent; } .navbar-default .navbar-nav > .disabled > a, .navbar-default .navbar-nav > .disabled > a:hover, .navbar-default .navbar-nav > .disabled > a:focus { - color: #444444; + color: #444; background-color: transparent; } .navbar-default .navbar-toggle { @@ -4148,7 +4148,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { background-color: #dddddd; } .navbar-default .navbar-toggle .icon-bar { - background-color: #cccccc; + background-color: #ccc; } .navbar-default .navbar-collapse, .navbar-default .navbar-form { @@ -4158,82 +4158,82 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus { background-color: transparent; - color: #4183c4; + color: #4183C4; } @media (max-width: 767px) { .navbar-default .navbar-nav .open .dropdown-menu > li > a { - color: #777777; + color: #777; } .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { - color: #4183c4; + color: #4183C4; background-color: transparent; } .navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #4183c4; + color: #4183C4; background-color: transparent; } .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #444444; + color: #444; background-color: transparent; } } .navbar-default .navbar-link { - color: #777777; + color: #777; } .navbar-default .navbar-link:hover { - color: #4183c4; + color: #4183C4; } .navbar-default .btn-link { - color: #777777; + color: #777; } .navbar-default .btn-link:hover, .navbar-default .btn-link:focus { - color: #4183c4; + color: #4183C4; } .navbar-default .btn-link[disabled]:hover, fieldset[disabled] .navbar-default .btn-link:hover, .navbar-default .btn-link[disabled]:focus, fieldset[disabled] .navbar-default .btn-link:focus { - color: #444444; + color: #444; } .navbar-inverse { - background-color: #2185d0; - border-color: #1a69a4; + background-color: #2185D0; + border-color: #2185D0; } .navbar-inverse .navbar-brand { - color: #dddddd; + color: #ddd; } .navbar-inverse .navbar-brand:hover, .navbar-inverse .navbar-brand:focus { - color: #ffffff; + color: #fff; background-color: transparent; } .navbar-inverse .navbar-text { - color: #dddddd; + color: #ddd; } .navbar-inverse .navbar-nav > li > a { - color: #dddddd; + color: #ddd; } .navbar-inverse .navbar-nav > li > a:hover, .navbar-inverse .navbar-nav > li > a:focus { - color: #ffffff; + color: #fff; background-color: transparent; } .navbar-inverse .navbar-nav > .active > a, .navbar-inverse .navbar-nav > .active > a:hover, .navbar-inverse .navbar-nav > .active > a:focus { - color: #ffffff; + color: #fff; background-color: transparent; } .navbar-inverse .navbar-nav > .disabled > a, .navbar-inverse .navbar-nav > .disabled > a:hover, .navbar-inverse .navbar-nav > .disabled > a:focus { - color: #cccccc; + color: #ccc; background-color: transparent; } .navbar-inverse .navbar-toggle { @@ -4244,7 +4244,7 @@ fieldset[disabled] .navbar-default .btn-link:focus { background-color: #1a69a4; } .navbar-inverse .navbar-toggle .icon-bar { - background-color: #ffffff; + background-color: #fff; } .navbar-inverse .navbar-collapse, .navbar-inverse .navbar-form { @@ -4254,54 +4254,54 @@ fieldset[disabled] .navbar-default .btn-link:focus { .navbar-inverse .navbar-nav > .open > a:hover, .navbar-inverse .navbar-nav > .open > a:focus { background-color: transparent; - color: #ffffff; + color: #fff; } @media (max-width: 767px) { .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { - border-color: #1a69a4; + border-color: #2185D0; } .navbar-inverse .navbar-nav .open .dropdown-menu .divider { - background-color: #1a69a4; + background-color: #2185D0; } .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { - color: #dddddd; + color: #ddd; } .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { - color: #ffffff; + color: #fff; background-color: transparent; } .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #ffffff; + color: #fff; background-color: transparent; } .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #cccccc; + color: #ccc; background-color: transparent; } } .navbar-inverse .navbar-link { - color: #dddddd; + color: #ddd; } .navbar-inverse .navbar-link:hover { - color: #ffffff; + color: #fff; } .navbar-inverse .btn-link { - color: #dddddd; + color: #ddd; } .navbar-inverse .btn-link:hover, .navbar-inverse .btn-link:focus { - color: #ffffff; + color: #fff; } .navbar-inverse .btn-link[disabled]:hover, fieldset[disabled] .navbar-inverse .btn-link:hover, .navbar-inverse .btn-link[disabled]:focus, fieldset[disabled] .navbar-inverse .btn-link:focus { - color: #cccccc; + color: #ccc; } .breadcrumb { padding: 8px 15px; @@ -4316,7 +4316,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .breadcrumb > li + li:before { content: "/\00a0"; padding: 0 5px; - color: #cccccc; + color: #ccc; } .breadcrumb > .active { color: #999999; @@ -4337,9 +4337,9 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { padding: 8px 12px; line-height: 1.42857143; text-decoration: none; - color: #4183c4; - background-color: #ffffff; - border: 1px solid #dddddd; + color: #4183C4; + background-color: #fff; + border: 1px solid #ddd; margin-left: -1px; } .pagination > li:first-child > a, @@ -4357,9 +4357,9 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pagination > li > span:hover, .pagination > li > a:focus, .pagination > li > span:focus { - color: #4183c4; + color: #4183C4; background-color: #eeeeee; - border-color: #dddddd; + border-color: #ddd; } .pagination > .active > a, .pagination > .active > span, @@ -4370,7 +4370,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { z-index: 2; color: #999999; background-color: #f5f5f5; - border-color: #dddddd; + border-color: #ddd; cursor: default; } .pagination > .disabled > span, @@ -4380,8 +4380,8 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pagination > .disabled > a:hover, .pagination > .disabled > a:focus { color: #999999; - background-color: #ffffff; - border-color: #dddddd; + background-color: #fff; + border-color: #ddd; cursor: not-allowed; } .pagination-lg > li > a, @@ -4427,8 +4427,8 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pager li > span { display: inline-block; padding: 5px 14px; - background-color: #ffffff; - border: 1px solid #dddddd; + background-color: #fff; + border: 1px solid #ddd; border-radius: 15px; } .pager li > a:hover, @@ -4449,7 +4449,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pager .disabled > a:focus, .pager .disabled > span { color: #999999; - background-color: #ffffff; + background-color: #fff; cursor: not-allowed; } .label { @@ -4458,7 +4458,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { font-size: 75%; font-weight: bold; line-height: 1; - color: #ffffff; + color: #fff; text-align: center; white-space: nowrap; vertical-align: baseline; @@ -4466,7 +4466,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { } a.label:hover, a.label:focus { - color: #ffffff; + color: #fff; text-decoration: none; cursor: pointer; } @@ -4478,35 +4478,35 @@ a.label:focus { top: -1px; } .label-default { - background-color: #ffffff; + background-color: #fff; } .label-default[href]:hover, .label-default[href]:focus { background-color: #e6e6e6; } .label-primary { - background-color: #2185d0; + background-color: #2185D0; } .label-primary[href]:hover, .label-primary[href]:focus { background-color: #1a69a4; } .label-success { - background-color: #78cb5b; + background-color: #78CB5B; } .label-success[href]:hover, .label-success[href]:focus { background-color: #5bb83b; } .label-info { - background-color: #4183c4; + background-color: #4183C4; } .label-info[href]:hover, .label-info[href]:focus { background-color: #3269a0; } .label-warning { - background-color: #d67c1c; + background-color: #D67C1C; } .label-warning[href]:hover, .label-warning[href]:focus { @@ -4525,12 +4525,12 @@ a.label:focus { padding: 3px 7px; font-size: 12px; font-weight: bold; - color: #ffffff; + color: #fff; line-height: 1; vertical-align: baseline; white-space: nowrap; text-align: center; - background-color: #4183c4; + background-color: #4183C4; border-radius: 10px; } .badge:empty { @@ -4546,14 +4546,14 @@ a.label:focus { } a.badge:hover, a.badge:focus { - color: #ffffff; + color: #fff; text-decoration: none; cursor: pointer; } .list-group-item.active > .badge, .nav-pills > .active > a > .badge { - color: #4183c4; - background-color: #ffffff; + color: #4183C4; + background-color: #fff; } .list-group-item > .badge { float: right; @@ -4608,8 +4608,8 @@ a.badge:focus { padding: 4px; margin-bottom: 20px; line-height: 1.42857143; - background-color: #ffffff; - border: 1px solid #dddddd; + background-color: #f5f5f5; + border: 1px solid #ddd; border-radius: 4px; transition: border 0.2s ease-in-out; } @@ -4621,11 +4621,11 @@ a.badge:focus { a.thumbnail:hover, a.thumbnail:focus, a.thumbnail.active { - border-color: #4183c4; + border-color: #4183C4; } .thumbnail .caption { padding: 9px; - color: #666666; + color: #666; } .alert { padding: 15px; @@ -4732,9 +4732,9 @@ a.thumbnail.active { height: 100%; font-size: 12px; line-height: 20px; - color: #ffffff; + color: #fff; text-align: center; - background-color: #2185d0; + background-color: #2185D0; box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); transition: width 0.6s ease; } @@ -4749,19 +4749,19 @@ a.thumbnail.active { animation: progress-bar-stripes 2s linear infinite; } .progress-bar-success { - background-color: #78cb5b; + background-color: #78CB5B; } .progress-striped .progress-bar-success { background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); } .progress-bar-info { - background-color: #4183c4; + background-color: #4183C4; } .progress-striped .progress-bar-info { background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); } .progress-bar-warning { - background-color: #d67c1c; + background-color: #D67C1C; } .progress-striped .progress-bar-warning { background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); @@ -4815,8 +4815,8 @@ a.thumbnail.active { display: block; padding: 10px 15px; margin-bottom: -1px; - background-color: #ffffff; - border: 1px solid #dddddd; + background-color: #fff; + border: 1px solid #ddd; } .list-group-item:first-child { border-top-right-radius: 4px; @@ -4828,15 +4828,15 @@ a.thumbnail.active { border-bottom-left-radius: 4px; } a.list-group-item { - color: #555555; + color: #555; } a.list-group-item .list-group-item-heading { - color: #333333; + color: #333; } a.list-group-item:hover, a.list-group-item:focus { text-decoration: none; - color: #555555; + color: #555; background-color: #f5f5f5; } .list-group-item.disabled, @@ -4860,9 +4860,9 @@ a.list-group-item:focus { .list-group-item.active:hover, .list-group-item.active:focus { z-index: 2; - color: #ffffff; - background-color: #2185d0; - border-color: #2185d0; + color: #fff; + background-color: #2185D0; + border-color: #2185D0; } .list-group-item.active .list-group-item-heading, .list-group-item.active:hover .list-group-item-heading, @@ -4978,7 +4978,7 @@ a.list-group-item-danger.active:focus { } .panel { margin-bottom: 20px; - background-color: #ffffff; + background-color: #fff; border: 1px solid transparent; border-radius: 4px; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); @@ -5007,7 +5007,7 @@ a.list-group-item-danger.active:focus { .panel-footer { padding: 10px 15px; background-color: #f5f5f5; - border-top: 1px solid #dddddd; + border-top: 1px solid #ddd; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } @@ -5117,7 +5117,7 @@ a.list-group-item-danger.active:focus { .panel > .panel-body + .table-responsive, .panel > .table + .panel-body, .panel > .table-responsive + .panel-body { - border-top: 1px solid #dddddd; + border-top: 1px solid #ddd; } .panel > .table > tbody:first-child > tr:first-child th, .panel > .table > tbody:first-child > tr:first-child td { @@ -5194,49 +5194,49 @@ a.list-group-item-danger.active:focus { } .panel-group .panel-heading + .panel-collapse > .panel-body, .panel-group .panel-heading + .panel-collapse > .list-group { - border-top: 1px solid #dddddd; + border-top: 1px solid #ddd; } .panel-group .panel-footer { border-top: 0; } .panel-group .panel-footer + .panel-collapse .panel-body { - border-bottom: 1px solid #dddddd; + border-bottom: 1px solid #ddd; } .panel-default { - border-color: #dddddd; + border-color: #ddd; } .panel-default > .panel-heading { color: #333333; background-color: #f5f5f5; - border-color: #dddddd; + border-color: #ddd; } .panel-default > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #dddddd; + border-top-color: #ddd; } .panel-default > .panel-heading .badge { color: #f5f5f5; background-color: #333333; } .panel-default > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #dddddd; + border-bottom-color: #ddd; } .panel-primary { - border-color: #2185d0; + border-color: #2185D0; } .panel-primary > .panel-heading { - color: #ffffff; - background-color: #2185d0; - border-color: #2185d0; + color: #fff; + background-color: #2185D0; + border-color: #2185D0; } .panel-primary > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #2185d0; + border-top-color: #2185D0; } .panel-primary > .panel-heading .badge { - color: #2185d0; - background-color: #ffffff; + color: #2185D0; + background-color: #fff; } .panel-primary > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #2185d0; + border-bottom-color: #2185D0; } .panel-success { border-color: #d6e9c6; @@ -5362,14 +5362,14 @@ a.list-group-item-danger.active:focus { font-size: 21px; font-weight: bold; line-height: 1; - color: #000000; - text-shadow: 0 1px 0 #ffffff; + color: #000; + text-shadow: 0 1px 0 #fff; opacity: 0.2; filter: alpha(opacity=20); } .close:hover, .close:focus { - color: #000000; + color: #000; text-decoration: none; cursor: pointer; opacity: 0.5; @@ -5402,6 +5402,7 @@ button.close { transform: translate(0, -25%); transition: -webkit-transform 0.3s ease-out; transition: transform 0.3s ease-out; + transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out; } .modal.in .modal-dialog { -webkit-transform: translate(0, 0); @@ -5418,8 +5419,8 @@ button.close { } .modal-content { position: relative; - background-color: #ffffff; - border: 1px solid #999999; + background-color: #fff; + border: 1px solid #999; border: 1px solid rgba(0, 0, 0, 0.2); border-radius: 6px; box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); @@ -5431,7 +5432,7 @@ button.close { top: 0; right: 0; left: 0; - background-color: #000000; + background-color: #000; } .modal-backdrop.fade { opacity: 0; @@ -5531,7 +5532,7 @@ button.close { .tooltip-inner { max-width: 400px; padding: 3px 8px; - color: #ffffff; + color: #fff; text-align: center; text-decoration: none; background-color: rgba(0, 0, 0, 0.9); @@ -5613,9 +5614,9 @@ button.close { font-weight: normal; line-height: 1.42857143; text-align: left; - background-color: #ffffff; + background-color: #fff; background-clip: padding-box; - border: 1px solid #cccccc; + border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, 0.2); border-radius: 6px; box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); @@ -5673,7 +5674,7 @@ button.close { bottom: 1px; margin-left: -10px; border-bottom-width: 0; - border-top-color: #ffffff; + border-top-color: #fff; } .popover.right > .arrow { top: 50%; @@ -5688,7 +5689,7 @@ button.close { left: 1px; bottom: -10px; border-left-width: 0; - border-right-color: #ffffff; + border-right-color: #fff; } .popover.bottom > .arrow { left: 50%; @@ -5703,7 +5704,7 @@ button.close { top: 1px; margin-left: -10px; border-top-width: 0; - border-bottom-color: #ffffff; + border-bottom-color: #fff; } .popover.left > .arrow { top: 50%; @@ -5717,7 +5718,7 @@ button.close { content: " "; right: 1px; border-right-width: 0; - border-left-color: #ffffff; + border-left-color: #fff; bottom: -10px; } .carousel { @@ -5741,6 +5742,7 @@ button.close { .carousel-inner > .item { transition: -webkit-transform 0.6s ease-in-out; transition: transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out; -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-perspective: 1000; @@ -5805,7 +5807,7 @@ button.close { opacity: 0.5; filter: alpha(opacity=50); font-size: 20px; - color: #ffffff; + color: #fff; text-align: center; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); } @@ -5824,7 +5826,7 @@ button.close { .carousel-control:hover, .carousel-control:focus { outline: 0; - color: #ffffff; + color: #fff; text-decoration: none; opacity: 0.9; filter: alpha(opacity=90); @@ -5878,7 +5880,7 @@ button.close { height: 10px; margin: 1px; text-indent: -999px; - border: 1px solid #ffffff; + border: 1px solid #fff; border-radius: 10px; cursor: pointer; background-color: #000 \9; @@ -5888,7 +5890,7 @@ button.close { margin: 0; width: 12px; height: 12px; - background-color: #ffffff; + background-color: #fff; } .carousel-caption { position: absolute; @@ -5898,7 +5900,7 @@ button.close { z-index: 10; padding-top: 20px; padding-bottom: 20px; - color: #ffffff; + color: #fff; text-align: center; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); } @@ -6233,14 +6235,12 @@ button.close { text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3); } .navbar-inverse { - -webkit-filter: none; - filter: none; - border: 1px solid #175b8e; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); + text-shadow: none; + border: 0; } .navbar-inverse .badge { background-color: #fff; - color: #2185d0; + color: #2185D0; } .navbar .badge { text-shadow: none; @@ -6335,7 +6335,3 @@ button.close { .panel-primary .panel-title { color: #fff; } -body { - background-color: #F5F5F5; - font-weight: 300; -} diff --git a/src/client/lib/bootstrap/css/cyborg/bootstrap.min.css b/src-client/assets/bootstrap/css/cyborg/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/cyborg/bootstrap.min.css rename to src-client/assets/bootstrap/css/cyborg/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/cyborg/bootswatch.less b/src-client/assets/bootstrap/css/cyborg/bootswatch.less similarity index 100% rename from src/client/lib/bootstrap/css/cyborg/bootswatch.less rename to src-client/assets/bootstrap/css/cyborg/bootswatch.less diff --git a/src/client/lib/bootstrap/css/cyborg/variables.less b/src-client/assets/bootstrap/css/cyborg/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/cyborg/variables.less rename to src-client/assets/bootstrap/css/cyborg/variables.less diff --git a/src/client/lib/bootstrap/css/darkly/bootstrap.min.css b/src-client/assets/bootstrap/css/darkly/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/darkly/bootstrap.min.css rename to src-client/assets/bootstrap/css/darkly/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/darkly/bootswatch.less b/src-client/assets/bootstrap/css/darkly/bootswatch.less similarity index 100% rename from src/client/lib/bootstrap/css/darkly/bootswatch.less rename to src-client/assets/bootstrap/css/darkly/bootswatch.less diff --git a/src/client/lib/bootstrap/css/darkly/variables.less b/src-client/assets/bootstrap/css/darkly/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/darkly/variables.less rename to src-client/assets/bootstrap/css/darkly/variables.less diff --git a/src/client/lib/bootstrap/css/default/bootstrap.min.css b/src-client/assets/bootstrap/css/default/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/default/bootstrap.min.css rename to src-client/assets/bootstrap/css/default/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/default/less/alerts.less b/src-client/assets/bootstrap/css/default/less/alerts.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/alerts.less rename to src-client/assets/bootstrap/css/default/less/alerts.less diff --git a/src/client/lib/bootstrap/css/default/less/badges.less b/src-client/assets/bootstrap/css/default/less/badges.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/badges.less rename to src-client/assets/bootstrap/css/default/less/badges.less diff --git a/src/client/lib/bootstrap/css/default/less/bootstrap.less b/src-client/assets/bootstrap/css/default/less/bootstrap.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/bootstrap.less rename to src-client/assets/bootstrap/css/default/less/bootstrap.less diff --git a/src/client/lib/bootstrap/css/default/less/breadcrumbs.less b/src-client/assets/bootstrap/css/default/less/breadcrumbs.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/breadcrumbs.less rename to src-client/assets/bootstrap/css/default/less/breadcrumbs.less diff --git a/src/client/lib/bootstrap/css/default/less/button-groups.less b/src-client/assets/bootstrap/css/default/less/button-groups.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/button-groups.less rename to src-client/assets/bootstrap/css/default/less/button-groups.less diff --git a/src/client/lib/bootstrap/css/default/less/buttons.less b/src-client/assets/bootstrap/css/default/less/buttons.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/buttons.less rename to src-client/assets/bootstrap/css/default/less/buttons.less diff --git a/src/client/lib/bootstrap/css/default/less/carousel.less b/src-client/assets/bootstrap/css/default/less/carousel.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/carousel.less rename to src-client/assets/bootstrap/css/default/less/carousel.less diff --git a/src/client/lib/bootstrap/css/default/less/close.less b/src-client/assets/bootstrap/css/default/less/close.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/close.less rename to src-client/assets/bootstrap/css/default/less/close.less diff --git a/src/client/lib/bootstrap/css/default/less/code.less b/src-client/assets/bootstrap/css/default/less/code.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/code.less rename to src-client/assets/bootstrap/css/default/less/code.less diff --git a/src/client/lib/bootstrap/css/default/less/component-animations.less b/src-client/assets/bootstrap/css/default/less/component-animations.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/component-animations.less rename to src-client/assets/bootstrap/css/default/less/component-animations.less diff --git a/src/client/lib/bootstrap/css/default/less/dropdowns.less b/src-client/assets/bootstrap/css/default/less/dropdowns.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/dropdowns.less rename to src-client/assets/bootstrap/css/default/less/dropdowns.less diff --git a/src/client/lib/bootstrap/css/default/less/forms.less b/src-client/assets/bootstrap/css/default/less/forms.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/forms.less rename to src-client/assets/bootstrap/css/default/less/forms.less diff --git a/src/client/lib/bootstrap/css/default/less/glyphicons.less b/src-client/assets/bootstrap/css/default/less/glyphicons.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/glyphicons.less rename to src-client/assets/bootstrap/css/default/less/glyphicons.less diff --git a/src/client/lib/bootstrap/css/default/less/grid.less b/src-client/assets/bootstrap/css/default/less/grid.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/grid.less rename to src-client/assets/bootstrap/css/default/less/grid.less diff --git a/src/client/lib/bootstrap/css/default/less/input-groups.less b/src-client/assets/bootstrap/css/default/less/input-groups.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/input-groups.less rename to src-client/assets/bootstrap/css/default/less/input-groups.less diff --git a/src/client/lib/bootstrap/css/default/less/jumbotron.less b/src-client/assets/bootstrap/css/default/less/jumbotron.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/jumbotron.less rename to src-client/assets/bootstrap/css/default/less/jumbotron.less diff --git a/src/client/lib/bootstrap/css/default/less/labels.less b/src-client/assets/bootstrap/css/default/less/labels.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/labels.less rename to src-client/assets/bootstrap/css/default/less/labels.less diff --git a/src/client/lib/bootstrap/css/default/less/list-group.less b/src-client/assets/bootstrap/css/default/less/list-group.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/list-group.less rename to src-client/assets/bootstrap/css/default/less/list-group.less diff --git a/src/client/lib/bootstrap/css/default/less/media.less b/src-client/assets/bootstrap/css/default/less/media.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/media.less rename to src-client/assets/bootstrap/css/default/less/media.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins.less b/src-client/assets/bootstrap/css/default/less/mixins.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins.less rename to src-client/assets/bootstrap/css/default/less/mixins.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/alerts.less b/src-client/assets/bootstrap/css/default/less/mixins/alerts.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/alerts.less rename to src-client/assets/bootstrap/css/default/less/mixins/alerts.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/background-variant.less b/src-client/assets/bootstrap/css/default/less/mixins/background-variant.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/background-variant.less rename to src-client/assets/bootstrap/css/default/less/mixins/background-variant.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/border-radius.less b/src-client/assets/bootstrap/css/default/less/mixins/border-radius.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/border-radius.less rename to src-client/assets/bootstrap/css/default/less/mixins/border-radius.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/buttons.less b/src-client/assets/bootstrap/css/default/less/mixins/buttons.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/buttons.less rename to src-client/assets/bootstrap/css/default/less/mixins/buttons.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/center-block.less b/src-client/assets/bootstrap/css/default/less/mixins/center-block.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/center-block.less rename to src-client/assets/bootstrap/css/default/less/mixins/center-block.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/clearfix.less b/src-client/assets/bootstrap/css/default/less/mixins/clearfix.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/clearfix.less rename to src-client/assets/bootstrap/css/default/less/mixins/clearfix.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/forms.less b/src-client/assets/bootstrap/css/default/less/mixins/forms.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/forms.less rename to src-client/assets/bootstrap/css/default/less/mixins/forms.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/gradients.less b/src-client/assets/bootstrap/css/default/less/mixins/gradients.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/gradients.less rename to src-client/assets/bootstrap/css/default/less/mixins/gradients.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/grid-framework.less b/src-client/assets/bootstrap/css/default/less/mixins/grid-framework.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/grid-framework.less rename to src-client/assets/bootstrap/css/default/less/mixins/grid-framework.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/grid.less b/src-client/assets/bootstrap/css/default/less/mixins/grid.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/grid.less rename to src-client/assets/bootstrap/css/default/less/mixins/grid.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/hide-text.less b/src-client/assets/bootstrap/css/default/less/mixins/hide-text.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/hide-text.less rename to src-client/assets/bootstrap/css/default/less/mixins/hide-text.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/image.less b/src-client/assets/bootstrap/css/default/less/mixins/image.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/image.less rename to src-client/assets/bootstrap/css/default/less/mixins/image.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/labels.less b/src-client/assets/bootstrap/css/default/less/mixins/labels.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/labels.less rename to src-client/assets/bootstrap/css/default/less/mixins/labels.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/list-group.less b/src-client/assets/bootstrap/css/default/less/mixins/list-group.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/list-group.less rename to src-client/assets/bootstrap/css/default/less/mixins/list-group.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/nav-divider.less b/src-client/assets/bootstrap/css/default/less/mixins/nav-divider.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/nav-divider.less rename to src-client/assets/bootstrap/css/default/less/mixins/nav-divider.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/nav-vertical-align.less b/src-client/assets/bootstrap/css/default/less/mixins/nav-vertical-align.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/nav-vertical-align.less rename to src-client/assets/bootstrap/css/default/less/mixins/nav-vertical-align.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/opacity.less b/src-client/assets/bootstrap/css/default/less/mixins/opacity.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/opacity.less rename to src-client/assets/bootstrap/css/default/less/mixins/opacity.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/pagination.less b/src-client/assets/bootstrap/css/default/less/mixins/pagination.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/pagination.less rename to src-client/assets/bootstrap/css/default/less/mixins/pagination.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/panels.less b/src-client/assets/bootstrap/css/default/less/mixins/panels.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/panels.less rename to src-client/assets/bootstrap/css/default/less/mixins/panels.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/progress-bar.less b/src-client/assets/bootstrap/css/default/less/mixins/progress-bar.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/progress-bar.less rename to src-client/assets/bootstrap/css/default/less/mixins/progress-bar.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/reset-filter.less b/src-client/assets/bootstrap/css/default/less/mixins/reset-filter.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/reset-filter.less rename to src-client/assets/bootstrap/css/default/less/mixins/reset-filter.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/resize.less b/src-client/assets/bootstrap/css/default/less/mixins/resize.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/resize.less rename to src-client/assets/bootstrap/css/default/less/mixins/resize.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/responsive-visibility.less b/src-client/assets/bootstrap/css/default/less/mixins/responsive-visibility.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/responsive-visibility.less rename to src-client/assets/bootstrap/css/default/less/mixins/responsive-visibility.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/size.less b/src-client/assets/bootstrap/css/default/less/mixins/size.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/size.less rename to src-client/assets/bootstrap/css/default/less/mixins/size.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/tab-focus.less b/src-client/assets/bootstrap/css/default/less/mixins/tab-focus.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/tab-focus.less rename to src-client/assets/bootstrap/css/default/less/mixins/tab-focus.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/table-row.less b/src-client/assets/bootstrap/css/default/less/mixins/table-row.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/table-row.less rename to src-client/assets/bootstrap/css/default/less/mixins/table-row.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/text-emphasis.less b/src-client/assets/bootstrap/css/default/less/mixins/text-emphasis.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/text-emphasis.less rename to src-client/assets/bootstrap/css/default/less/mixins/text-emphasis.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/text-overflow.less b/src-client/assets/bootstrap/css/default/less/mixins/text-overflow.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/text-overflow.less rename to src-client/assets/bootstrap/css/default/less/mixins/text-overflow.less diff --git a/src/client/lib/bootstrap/css/default/less/mixins/vendor-prefixes.less b/src-client/assets/bootstrap/css/default/less/mixins/vendor-prefixes.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/mixins/vendor-prefixes.less rename to src-client/assets/bootstrap/css/default/less/mixins/vendor-prefixes.less diff --git a/src/client/lib/bootstrap/css/default/less/modals.less b/src-client/assets/bootstrap/css/default/less/modals.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/modals.less rename to src-client/assets/bootstrap/css/default/less/modals.less diff --git a/src/client/lib/bootstrap/css/default/less/navbar.less b/src-client/assets/bootstrap/css/default/less/navbar.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/navbar.less rename to src-client/assets/bootstrap/css/default/less/navbar.less diff --git a/src/client/lib/bootstrap/css/default/less/navs.less b/src-client/assets/bootstrap/css/default/less/navs.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/navs.less rename to src-client/assets/bootstrap/css/default/less/navs.less diff --git a/src/client/lib/bootstrap/css/default/less/normalize.less b/src-client/assets/bootstrap/css/default/less/normalize.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/normalize.less rename to src-client/assets/bootstrap/css/default/less/normalize.less diff --git a/src/client/lib/bootstrap/css/default/less/pager.less b/src-client/assets/bootstrap/css/default/less/pager.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/pager.less rename to src-client/assets/bootstrap/css/default/less/pager.less diff --git a/src/client/lib/bootstrap/css/default/less/pagination.less b/src-client/assets/bootstrap/css/default/less/pagination.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/pagination.less rename to src-client/assets/bootstrap/css/default/less/pagination.less diff --git a/src/client/lib/bootstrap/css/default/less/panels.less b/src-client/assets/bootstrap/css/default/less/panels.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/panels.less rename to src-client/assets/bootstrap/css/default/less/panels.less diff --git a/src/client/lib/bootstrap/css/default/less/popovers.less b/src-client/assets/bootstrap/css/default/less/popovers.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/popovers.less rename to src-client/assets/bootstrap/css/default/less/popovers.less diff --git a/src/client/lib/bootstrap/css/default/less/print.less b/src-client/assets/bootstrap/css/default/less/print.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/print.less rename to src-client/assets/bootstrap/css/default/less/print.less diff --git a/src/client/lib/bootstrap/css/default/less/progress-bars.less b/src-client/assets/bootstrap/css/default/less/progress-bars.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/progress-bars.less rename to src-client/assets/bootstrap/css/default/less/progress-bars.less diff --git a/src/client/lib/bootstrap/css/default/less/responsive-embed.less b/src-client/assets/bootstrap/css/default/less/responsive-embed.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/responsive-embed.less rename to src-client/assets/bootstrap/css/default/less/responsive-embed.less diff --git a/src/client/lib/bootstrap/css/default/less/responsive-utilities.less b/src-client/assets/bootstrap/css/default/less/responsive-utilities.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/responsive-utilities.less rename to src-client/assets/bootstrap/css/default/less/responsive-utilities.less diff --git a/src/client/lib/bootstrap/css/default/less/scaffolding.less b/src-client/assets/bootstrap/css/default/less/scaffolding.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/scaffolding.less rename to src-client/assets/bootstrap/css/default/less/scaffolding.less diff --git a/src/client/lib/bootstrap/css/default/less/tables.less b/src-client/assets/bootstrap/css/default/less/tables.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/tables.less rename to src-client/assets/bootstrap/css/default/less/tables.less diff --git a/src/client/lib/bootstrap/css/default/less/theme.less b/src-client/assets/bootstrap/css/default/less/theme.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/theme.less rename to src-client/assets/bootstrap/css/default/less/theme.less diff --git a/src/client/lib/bootstrap/css/default/less/thumbnails.less b/src-client/assets/bootstrap/css/default/less/thumbnails.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/thumbnails.less rename to src-client/assets/bootstrap/css/default/less/thumbnails.less diff --git a/src/client/lib/bootstrap/css/default/less/tooltip.less b/src-client/assets/bootstrap/css/default/less/tooltip.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/tooltip.less rename to src-client/assets/bootstrap/css/default/less/tooltip.less diff --git a/src/client/lib/bootstrap/css/default/less/type.less b/src-client/assets/bootstrap/css/default/less/type.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/type.less rename to src-client/assets/bootstrap/css/default/less/type.less diff --git a/src/client/lib/bootstrap/css/default/less/utilities.less b/src-client/assets/bootstrap/css/default/less/utilities.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/utilities.less rename to src-client/assets/bootstrap/css/default/less/utilities.less diff --git a/src/client/lib/bootstrap/css/default/less/variables.less b/src-client/assets/bootstrap/css/default/less/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/variables.less rename to src-client/assets/bootstrap/css/default/less/variables.less diff --git a/src/client/lib/bootstrap/css/default/less/wells.less b/src-client/assets/bootstrap/css/default/less/wells.less similarity index 100% rename from src/client/lib/bootstrap/css/default/less/wells.less rename to src-client/assets/bootstrap/css/default/less/wells.less diff --git a/src/client/lib/bootstrap/css/default/variables.less b/src-client/assets/bootstrap/css/default/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/default/variables.less rename to src-client/assets/bootstrap/css/default/variables.less diff --git a/src/client/lib/bootstrap/css/flatly/bootstrap.min.css b/src-client/assets/bootstrap/css/flatly/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/flatly/bootstrap.min.css rename to src-client/assets/bootstrap/css/flatly/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/flatly/bootswatch.less b/src-client/assets/bootstrap/css/flatly/bootswatch.less similarity index 100% rename from src/client/lib/bootstrap/css/flatly/bootswatch.less rename to src-client/assets/bootstrap/css/flatly/bootswatch.less diff --git a/src/client/lib/bootstrap/css/flatly/variables.less b/src-client/assets/bootstrap/css/flatly/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/flatly/variables.less rename to src-client/assets/bootstrap/css/flatly/variables.less diff --git a/src/client/lib/bootstrap/css/fonts/glyphicons-halflings-regular.eot b/src-client/assets/bootstrap/css/fonts/glyphicons-halflings-regular.eot similarity index 100% rename from src/client/lib/bootstrap/css/fonts/glyphicons-halflings-regular.eot rename to src-client/assets/bootstrap/css/fonts/glyphicons-halflings-regular.eot diff --git a/src/client/lib/bootstrap/css/fonts/glyphicons-halflings-regular.svg b/src-client/assets/bootstrap/css/fonts/glyphicons-halflings-regular.svg similarity index 100% rename from src/client/lib/bootstrap/css/fonts/glyphicons-halflings-regular.svg rename to src-client/assets/bootstrap/css/fonts/glyphicons-halflings-regular.svg diff --git a/src/client/lib/bootstrap/css/fonts/glyphicons-halflings-regular.ttf b/src-client/assets/bootstrap/css/fonts/glyphicons-halflings-regular.ttf similarity index 100% rename from src/client/lib/bootstrap/css/fonts/glyphicons-halflings-regular.ttf rename to src-client/assets/bootstrap/css/fonts/glyphicons-halflings-regular.ttf diff --git a/src/client/lib/bootstrap/css/fonts/glyphicons-halflings-regular.woff b/src-client/assets/bootstrap/css/fonts/glyphicons-halflings-regular.woff similarity index 100% rename from src/client/lib/bootstrap/css/fonts/glyphicons-halflings-regular.woff rename to src-client/assets/bootstrap/css/fonts/glyphicons-halflings-regular.woff diff --git a/src/client/lib/bootstrap/css/journal/bootstrap.min.css b/src-client/assets/bootstrap/css/journal/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/journal/bootstrap.min.css rename to src-client/assets/bootstrap/css/journal/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/journal/bootswatch.less b/src-client/assets/bootstrap/css/journal/bootswatch.less similarity index 100% rename from src/client/lib/bootstrap/css/journal/bootswatch.less rename to src-client/assets/bootstrap/css/journal/bootswatch.less diff --git a/src/client/lib/bootstrap/css/journal/variables.less b/src-client/assets/bootstrap/css/journal/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/journal/variables.less rename to src-client/assets/bootstrap/css/journal/variables.less diff --git a/src/client/lib/bootstrap/css/lumen/bootstrap.min.css b/src-client/assets/bootstrap/css/lumen/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/lumen/bootstrap.min.css rename to src-client/assets/bootstrap/css/lumen/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/lumen/bootswatch.less b/src-client/assets/bootstrap/css/lumen/bootswatch.less similarity index 100% rename from src/client/lib/bootstrap/css/lumen/bootswatch.less rename to src-client/assets/bootstrap/css/lumen/bootswatch.less diff --git a/src/client/lib/bootstrap/css/lumen/variables.less b/src-client/assets/bootstrap/css/lumen/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/lumen/variables.less rename to src-client/assets/bootstrap/css/lumen/variables.less diff --git a/src/client/lib/bootstrap/css/paper/bootstrap.min.css b/src-client/assets/bootstrap/css/paper/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/paper/bootstrap.min.css rename to src-client/assets/bootstrap/css/paper/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/paper/bootswatch.less b/src-client/assets/bootstrap/css/paper/bootswatch.less similarity index 100% rename from src/client/lib/bootstrap/css/paper/bootswatch.less rename to src-client/assets/bootstrap/css/paper/bootswatch.less diff --git a/src/client/lib/bootstrap/css/paper/variables.less b/src-client/assets/bootstrap/css/paper/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/paper/variables.less rename to src-client/assets/bootstrap/css/paper/variables.less diff --git a/src/client/lib/bootstrap/css/readable/bootstrap.min.css b/src-client/assets/bootstrap/css/readable/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/readable/bootstrap.min.css rename to src-client/assets/bootstrap/css/readable/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/readable/bootswatch.less b/src-client/assets/bootstrap/css/readable/bootswatch.less similarity index 100% rename from src/client/lib/bootstrap/css/readable/bootswatch.less rename to src-client/assets/bootstrap/css/readable/bootswatch.less diff --git a/src/client/lib/bootstrap/css/readable/variables.less b/src-client/assets/bootstrap/css/readable/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/readable/variables.less rename to src-client/assets/bootstrap/css/readable/variables.less diff --git a/src/client/lib/bootstrap/css/sandstone/bootstrap.min.css b/src-client/assets/bootstrap/css/sandstone/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/sandstone/bootstrap.min.css rename to src-client/assets/bootstrap/css/sandstone/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/sandstone/bootswatch.less b/src-client/assets/bootstrap/css/sandstone/bootswatch.less similarity index 100% rename from src/client/lib/bootstrap/css/sandstone/bootswatch.less rename to src-client/assets/bootstrap/css/sandstone/bootswatch.less diff --git a/src/client/lib/bootstrap/css/sandstone/variables.less b/src-client/assets/bootstrap/css/sandstone/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/sandstone/variables.less rename to src-client/assets/bootstrap/css/sandstone/variables.less diff --git a/src/client/lib/bootstrap/css/simplex/bootstrap.min.css b/src-client/assets/bootstrap/css/simplex/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/simplex/bootstrap.min.css rename to src-client/assets/bootstrap/css/simplex/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/simplex/bootswatch.less b/src-client/assets/bootstrap/css/simplex/bootswatch.less similarity index 100% rename from src/client/lib/bootstrap/css/simplex/bootswatch.less rename to src-client/assets/bootstrap/css/simplex/bootswatch.less diff --git a/src/client/lib/bootstrap/css/simplex/variables.less b/src-client/assets/bootstrap/css/simplex/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/simplex/variables.less rename to src-client/assets/bootstrap/css/simplex/variables.less diff --git a/src/client/lib/bootstrap/css/slate/bootstrap.min.css b/src-client/assets/bootstrap/css/slate/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/slate/bootstrap.min.css rename to src-client/assets/bootstrap/css/slate/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/slate/bootswatch.less b/src-client/assets/bootstrap/css/slate/bootswatch.less similarity index 100% rename from src/client/lib/bootstrap/css/slate/bootswatch.less rename to src-client/assets/bootstrap/css/slate/bootswatch.less diff --git a/src/client/lib/bootstrap/css/slate/variables.less b/src-client/assets/bootstrap/css/slate/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/slate/variables.less rename to src-client/assets/bootstrap/css/slate/variables.less diff --git a/src/client/lib/bootstrap/css/spacelab/bootstrap.min.css b/src-client/assets/bootstrap/css/spacelab/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/spacelab/bootstrap.min.css rename to src-client/assets/bootstrap/css/spacelab/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/spacelab/bootswatch.less b/src-client/assets/bootstrap/css/spacelab/bootswatch.less similarity index 95% rename from src/client/lib/bootstrap/css/spacelab/bootswatch.less rename to src-client/assets/bootstrap/css/spacelab/bootswatch.less index 85a697f4..76e0712a 100644 --- a/src/client/lib/bootstrap/css/spacelab/bootswatch.less +++ b/src-client/assets/bootstrap/css/spacelab/bootswatch.less @@ -18,8 +18,10 @@ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3); &-inverse { - .btn-shadow(@navbar-inverse-bg); - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); + //.btn-shadow(@navbar-inverse-bg); + //text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); + text-shadow: none; + border: 0; .badge { background-color: #fff; diff --git a/src/client/lib/bootstrap/css/spacelab/variables.less b/src-client/assets/bootstrap/css/spacelab/variables.less similarity index 99% rename from src/client/lib/bootstrap/css/spacelab/variables.less rename to src-client/assets/bootstrap/css/spacelab/variables.less index 951c318b..a8620f2b 100644 --- a/src/client/lib/bootstrap/css/spacelab/variables.less +++ b/src-client/assets/bootstrap/css/spacelab/variables.less @@ -32,7 +32,7 @@ //## Settings for some of the most global styles. //** Background color for ``. -@body-bg: #fff; +@body-bg: #f5f5f5; //** Global text color on ``. @text-color: @gray; @@ -392,7 +392,7 @@ // Reset inverted navbar basics @navbar-inverse-color: #ddd; @navbar-inverse-bg: @brand-primary; -@navbar-inverse-border: darken(@navbar-inverse-bg, 10%); +@navbar-inverse-border: @brand-primary; // Inverted navbar links @navbar-inverse-link-color: #ddd; diff --git a/src/client/lib/bootstrap/css/superhero/bootstrap.min.css b/src-client/assets/bootstrap/css/superhero/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/superhero/bootstrap.min.css rename to src-client/assets/bootstrap/css/superhero/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/superhero/bootswatch.less b/src-client/assets/bootstrap/css/superhero/bootswatch.less similarity index 100% rename from src/client/lib/bootstrap/css/superhero/bootswatch.less rename to src-client/assets/bootstrap/css/superhero/bootswatch.less diff --git a/src/client/lib/bootstrap/css/superhero/variables.less b/src-client/assets/bootstrap/css/superhero/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/superhero/variables.less rename to src-client/assets/bootstrap/css/superhero/variables.less diff --git a/src/client/lib/bootstrap/css/united/bootstrap.min.css b/src-client/assets/bootstrap/css/united/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/united/bootstrap.min.css rename to src-client/assets/bootstrap/css/united/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/united/bootswatch.less b/src-client/assets/bootstrap/css/united/bootswatch.less similarity index 100% rename from src/client/lib/bootstrap/css/united/bootswatch.less rename to src-client/assets/bootstrap/css/united/bootswatch.less diff --git a/src/client/lib/bootstrap/css/united/variables.less b/src-client/assets/bootstrap/css/united/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/united/variables.less rename to src-client/assets/bootstrap/css/united/variables.less diff --git a/src/client/lib/bootstrap/css/yeti/bootstrap.min.css b/src-client/assets/bootstrap/css/yeti/bootstrap.min.css similarity index 100% rename from src/client/lib/bootstrap/css/yeti/bootstrap.min.css rename to src-client/assets/bootstrap/css/yeti/bootstrap.min.css diff --git a/src/client/lib/bootstrap/css/yeti/bootswatch.less b/src-client/assets/bootstrap/css/yeti/bootswatch.less similarity index 100% rename from src/client/lib/bootstrap/css/yeti/bootswatch.less rename to src-client/assets/bootstrap/css/yeti/bootswatch.less diff --git a/src/client/lib/bootstrap/css/yeti/variables.less b/src-client/assets/bootstrap/css/yeti/variables.less similarity index 100% rename from src/client/lib/bootstrap/css/yeti/variables.less rename to src-client/assets/bootstrap/css/yeti/variables.less diff --git a/src/client/lib/bootstrap/js/bootstrap.js b/src-client/assets/bootstrap/js/bootstrap.js similarity index 100% rename from src/client/lib/bootstrap/js/bootstrap.js rename to src-client/assets/bootstrap/js/bootstrap.js diff --git a/src/client/lib/bootstrap/less/bootstrap.less b/src-client/assets/bootstrap/less/bootstrap.less similarity index 72% rename from src/client/lib/bootstrap/less/bootstrap.less rename to src-client/assets/bootstrap/less/bootstrap.less index c6fba1f7..2585a974 100755 --- a/src/client/lib/bootstrap/less/bootstrap.less +++ b/src-client/assets/bootstrap/less/bootstrap.less @@ -7,14 +7,10 @@ //@import "../css/slate/variables.less"; //@import "../css/slate/bootswatch.less"; -@import url(https://fonts.googleapis.com/css?family=Roboto:400,300,700,500); +@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500); +//@import url(https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300); //Your custom app styles //.example-page { // text-align: center; // padding-top: 200px; //} - -body { - background-color: #F5F5F5; - font-weight: 300; -} diff --git a/src/client/lib/font-awesome/css/font-awesome.css b/src-client/assets/font-awesome/css/font-awesome.css similarity index 100% rename from src/client/lib/font-awesome/css/font-awesome.css rename to src-client/assets/font-awesome/css/font-awesome.css diff --git a/src/client/lib/font-awesome/fonts/FontAwesome.otf b/src-client/assets/font-awesome/fonts/FontAwesome.otf similarity index 100% rename from src/client/lib/font-awesome/fonts/FontAwesome.otf rename to src-client/assets/font-awesome/fonts/FontAwesome.otf diff --git a/src/client/lib/font-awesome/fonts/fontawesome-webfont.eot b/src-client/assets/font-awesome/fonts/fontawesome-webfont.eot similarity index 100% rename from src/client/lib/font-awesome/fonts/fontawesome-webfont.eot rename to src-client/assets/font-awesome/fonts/fontawesome-webfont.eot diff --git a/src/client/lib/font-awesome/fonts/fontawesome-webfont.svg b/src-client/assets/font-awesome/fonts/fontawesome-webfont.svg similarity index 100% rename from src/client/lib/font-awesome/fonts/fontawesome-webfont.svg rename to src-client/assets/font-awesome/fonts/fontawesome-webfont.svg diff --git a/src/client/lib/font-awesome/fonts/fontawesome-webfont.ttf b/src-client/assets/font-awesome/fonts/fontawesome-webfont.ttf similarity index 100% rename from src/client/lib/font-awesome/fonts/fontawesome-webfont.ttf rename to src-client/assets/font-awesome/fonts/fontawesome-webfont.ttf diff --git a/src/client/lib/font-awesome/fonts/fontawesome-webfont.woff b/src-client/assets/font-awesome/fonts/fontawesome-webfont.woff similarity index 100% rename from src/client/lib/font-awesome/fonts/fontawesome-webfont.woff rename to src-client/assets/font-awesome/fonts/fontawesome-webfont.woff diff --git a/src/client/lib/font-awesome/fonts/fontawesome-webfont.woff2 b/src-client/assets/font-awesome/fonts/fontawesome-webfont.woff2 similarity index 100% rename from src/client/lib/font-awesome/fonts/fontawesome-webfont.woff2 rename to src-client/assets/font-awesome/fonts/fontawesome-webfont.woff2 diff --git a/src-client/assets/umy-font/fonts/umy-font.eot b/src-client/assets/umy-font/fonts/umy-font.eot new file mode 100644 index 00000000..954798d9 Binary files /dev/null and b/src-client/assets/umy-font/fonts/umy-font.eot differ diff --git a/src-client/assets/umy-font/fonts/umy-font.svg b/src-client/assets/umy-font/fonts/umy-font.svg new file mode 100644 index 00000000..29feacad --- /dev/null +++ b/src-client/assets/umy-font/fonts/umy-font.svg @@ -0,0 +1,18 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src-client/assets/umy-font/fonts/umy-font.ttf b/src-client/assets/umy-font/fonts/umy-font.ttf new file mode 100644 index 00000000..040481e9 Binary files /dev/null and b/src-client/assets/umy-font/fonts/umy-font.ttf differ diff --git a/src-client/assets/umy-font/fonts/umy-font.woff b/src-client/assets/umy-font/fonts/umy-font.woff new file mode 100644 index 00000000..a67b3598 Binary files /dev/null and b/src-client/assets/umy-font/fonts/umy-font.woff differ diff --git a/src-client/assets/umy-font/umy-font.css b/src-client/assets/umy-font/umy-font.css new file mode 100644 index 00000000..79f05404 --- /dev/null +++ b/src-client/assets/umy-font/umy-font.css @@ -0,0 +1,67 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@font-face { + font-family: 'umy-font'; + src: url('fonts/umy-font.eot?wg8r5d'); + src: url('fonts/umy-font.eot?wg8r5d#iefix') format('embedded-opentype'), + url('fonts/umy-font.ttf?wg8r5d') format('truetype'), + url('fonts/umy-font.woff?wg8r5d') format('woff'), + url('fonts/umy-font.svg?wg8r5d#umy-font') format('svg'); + font-weight: normal; + font-style: normal; +} + +[class^="umy-icon-"], [class*=" umy-icon-"] { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'umy-font' !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.umy-icon-arrow-plus-down:before { + content: "\e900"; +} +.umy-icon-arrow-plus-up:before { + content: "\e901"; +} +.umy-icon-replace:before { + content: "\e908"; +} +.umy-icon-edit:before { + content: "\e905"; +} +.umy-icon-copy:before { + content: "\e906"; +} +.umy-icon-duplicate:before { + content: "\e907"; +} +.umy-icon-delete:before { + content: "\e909"; +} +.umy-icon-cut:before { + content: "\e902"; +} + diff --git a/src-client/components/AceEditor.js b/src-client/components/AceEditor.js new file mode 100644 index 00000000..02b6bb9c --- /dev/null +++ b/src-client/components/AceEditor.js @@ -0,0 +1,117 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +class AceEditor extends Component { + + constructor (props) { + super(props); + this.checkEditor = this.checkEditor.bind(this); + this.getSourceCode = this.getSourceCode.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleFocus = this.handleFocus.bind(this); + this.handleBlur = this.handleBlur.bind(this); + } + + checkEditor (sourceCode) { + if (!this.editor) { + // + let domNode = this.refs.editorElement; + this.editor = ace.edit(domNode); + this.editor.getSession().setMode(this.props.mode); + this.editor.getSession().setTabSize(4); + if (this.props.isReadOnly) { + this.editor.setReadOnly(true); + } + this.editor.$blockScrolling = Infinity; + this.editor.getSession().on('change', this.handleChange); + this.editor.on('blur', this.handleBlur); + this.editor.on('focus', this.handleFocus); + this.isFocused = false; + } + if (sourceCode) { + this.editor.getSession().setValue(sourceCode); + } + //this.editor.focus(); + //this.editor.navigateFileEnd(); + } + + getSourceCode () { + if (this.editor) { + return this.editor.getSession().getValue(); + } + return null; + } + + handleChange (e) { + if (this.isFocused && this.props.onChangeText) { + this.props.onChangeText(this.editor.getSession().getValue()); + } + } + + handleFocus () { + this.isFocused = true; + } + + handleBlur () { + this.isFocused = false; + } + + componentDidMount () { + this.checkEditor(this.props.sourceCode); + } + + componentDidUpdate () { + this.checkEditor(this.props.sourceCode); + } + + shouldComponentUpdate (nextProps, nextState) { + return !this.isFocused; + } + + componentWillUnmount () { + if (this.editor) { + this.editor.destroy(); + this.editor = null; + } + } + + render () { + return ( +
+ ); + } + +} + +AceEditor.propTypes = { + mode: PropTypes.string.isRequired, + onChangeText: PropTypes.func, + isReadOnly: PropTypes.bool, + sourceCode: PropTypes.string +}; + +AceEditor.defaultProps = { + sourceCode: '', + mode: 'ace/mode/javascript', + onChangeText: null, + isReadOnly: false +}; + +export default AceEditor; + diff --git a/src-client/components/CheckboxStateful.js b/src-client/components/CheckboxStateful.js new file mode 100644 index 00000000..3f8cc286 --- /dev/null +++ b/src-client/components/CheckboxStateful.js @@ -0,0 +1,65 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +class CheckboxStateful extends Component { + + constructor (props, content) { + super(props, content); + this.state = { + value: this.props.value + }; + this.handleOnChange = this.handleOnChange.bind(this); + } + + componentWillReceiveProps (nextProps) { + this.setState({ + value: nextProps.value + }); + } + + handleOnChange () { + this.setState({ + value: this.refs.inputElement.checked + }); + } + + getValue () { + return this.state.value; + } + + render () { + const {value} = this.state; + return ( + + ); + } +} +CheckboxStateful.defaultProps = { + value: false +}; +CheckboxStateful.propTypes = { + value: PropTypes.bool +}; + +export default CheckboxStateful; diff --git a/src-client/components/CollapsiblePlusOptionInput.js b/src-client/components/CollapsiblePlusOptionInput.js new file mode 100644 index 00000000..3f09744b --- /dev/null +++ b/src-client/components/CollapsiblePlusOptionInput.js @@ -0,0 +1,124 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import { Collapse, Panel } from 'react-bootstrap'; + +class CollapsiblePlusOptionInput extends Component { + + constructor (props) { + super(props); + this.state = {}; + this.handleToggle = this.handleToggle.bind(this); + this.handleCommit = this.handleCommit.bind(this); + this.handleOnKeyDown = this.handleOnKeyDown.bind(this); + } + + handleToggle (e) { + e.preventDefault(); + e.stopPropagation(); + if (this.props.onToggle) { + this.props.onToggle(); + } + this.setState({open: !this.state.open}); + } + + handleCommit (e) { + e.preventDefault(); + e.stopPropagation(); + this.handleToggle(e); + if (this.props.onCommit) { + this.props.onCommit({ + path: this.refs.inputPath.value, + value: this.refs.inputValue.value + }); + } + } + + handleOnKeyDown (e) { + if (e.keyCode == 27) { + this.handleToggle(e); + } + } + + render () { + + let options = []; + const {optionNames} = this.props; + if (optionNames && optionNames.length > 0) { + optionNames.forEach((optionName, index) => { + options.push( + + ); + }); + } + + return ( +
+
+ +
+ + {this.refs.inputPath.focus();}}> +
+
+ + + + + + + + + {options} + +
+
+
+
+ ); + } + +} + +CollapsiblePlusOptionInput.defaultProps = { + onToggle: null +}; + +export default CollapsiblePlusOptionInput; + + diff --git a/src-client/components/ContentEditable.js b/src-client/components/ContentEditable.js new file mode 100644 index 00000000..f57b4e23 --- /dev/null +++ b/src-client/components/ContentEditable.js @@ -0,0 +1,107 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +class ContentEditable extends Component { + + constructor (props) { + super(props); + this.emitChange = this.emitChange.bind(this); + this.handleOnBlur = this.handleOnBlur.bind(this); + this.handleOnKeyDown = this.handleOnKeyDown.bind(this); + } + + emitChange () { + + const html = this.refs.editableElement.innerHTML; + if (this.props.onChange && html !== this.lastHtml) { + this.props.onChange({ + target: { + value: html + } + }); + } + this.lastHtml = html; + + } + + handleOnBlur () { + + if (this.props.onBlur) { + this.props.onBlur({ + target: { + value: this.lastHtml + } + }); + } + + } + + handleOnKeyDown (e) { + + if (e.keyCode == 13 || e.keyCode == 27) { + this.handleOnBlur(); + } + + } + + shouldComponentUpdate (nextProps) { + return nextProps.html !== this.refs.editableElement.innerHTML; + } + + componentDidUpdate () { + var node = this.refs.editableElement; + if (this.props.html !== node.innerHTML) { + node.innerHTML = this.props.html; + this.lastHtml = this.props.html; + } + } + + componentDidMount () { + $(this.refs.editableElement).focus(); + this.lastHtml = this.props.html; + } + + render () { + return ( +
+
+ ); + } +} + +ContentEditable.defaultProps = { + onBlur: undefined, + onChange: undefined, + html: '' +}; +ContentEditable.propTypes = { + onBlur: PropTypes.func, + onChange: PropTypes.func, + html: PropTypes.string +}; + +export default ContentEditable; + diff --git a/src-client/components/DimensionBorder.js b/src-client/components/DimensionBorder.js new file mode 100644 index 00000000..faae0b80 --- /dev/null +++ b/src-client/components/DimensionBorder.js @@ -0,0 +1,102 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +class DimensionBorder extends Component { + + constructor (props) { + super(props); + this.handleDecrease = this.handleDecrease.bind(this); + this.handleIncrease = this.handleIncrease.bind(this); + } + + handleIncrease (e) { + e.stopPropagation(); + e.preventDefault(); + const {onIncrease, type} = this.props; + if (onIncrease) { + onIncrease(type); + } + } + + handleDecrease (e) { + e.stopPropagation(); + e.preventDefault(); + const {onDecrease, type} = this.props; + if (onDecrease) { + onDecrease(type); + } + } + + render () { + const {type, buttonDistance} = this.props; + const distance = buttonDistance + 'px'; + let className; + let increaseButtonStyle = {}; + let decreaseButtonStyle = {}; + if (type === 'left') { + className = 'dimension-border-left'; + increaseButtonStyle = {top: distance, left: '2px'}; + decreaseButtonStyle = {bottom: distance, left: '2px'}; + } else if (type === 'right') { + className = 'dimension-border-right'; + increaseButtonStyle = {top: distance, right: '2px'}; + decreaseButtonStyle = {bottom: distance, right: '2px'}; + } else if (type === 'top') { + className = 'dimension-border-top'; + increaseButtonStyle = {left: distance, top: '2px'}; + decreaseButtonStyle = {right: distance, top: '2px'}; + } else if (type === 'bottom') { + className = 'dimension-border-bottom'; + increaseButtonStyle = {left: distance, bottom: '2px'}; + decreaseButtonStyle = {right: distance, bottom: '2px'}; + } + return ( +
+
+
+ +
+
+
+
+ +
+
+
+ ); + } +} + +DimensionBorder.propTypes = { + type: PropTypes.string.isRequired, + buttonDistance: PropTypes.number.isRequired, + onDecrease: PropTypes.func, + onIncrease: PropTypes.func, +}; + +DimensionBorder.defaultProps = { + type: 'left', + buttonDistance: 30, +}; + +export default DimensionBorder; diff --git a/src-client/components/DimensionBox.js b/src-client/components/DimensionBox.js new file mode 100644 index 00000000..d85dd534 --- /dev/null +++ b/src-client/components/DimensionBox.js @@ -0,0 +1,104 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import DimensionBorder from './DimensionBorder.js'; + +class DimensionBox extends Component { + + constructor (props) { + super(props); + this.handleDecrease = this.handleDecrease.bind(this); + this.handleIncrease = this.handleIncrease.bind(this); + } + + handleDecrease (border) { + const {onDecrease, type} = this.props; + if (onDecrease) { + onDecrease(type, border); + } + } + + handleIncrease (border) { + const {onIncrease, type} = this.props; + if (onIncrease) { + onIncrease(type, border); + } + } + + render () { + const {type} = this.props; + let buttonDistance = 20; + let className = 'dimension-box'; + if (type === 'padding') { + className += ' dimension-padding'; + buttonDistance = 20; + } else if (type === 'border') { + className += ' dimension-border'; + buttonDistance = 30; + } else if (type === 'margin') { + className += ' dimension-margin'; + buttonDistance = 30; + } else if (type === 'position') { + className += ' dimension-position'; + buttonDistance = 30; + } + return ( +
+ + + + + {type === 'padding' ? +
+ : this.props.children + } +
+ ); + } +} + +DimensionBox.propTypes = { + type: PropTypes.string.isRequired, + onDecrease: PropTypes.func, + onIncrease: PropTypes.func, +}; + +DimensionBox.defaultProps = { + type: 'padding', + onDecrease: (dimension, border) => console.log(`Decrease ${dimension} on ${border}`), + onIncrease: (dimension, border) => console.log(`Increase ${dimension} on ${border}`), +}; + +export default DimensionBox; diff --git a/src-client/components/DimensionContainer.js b/src-client/components/DimensionContainer.js new file mode 100644 index 00000000..da9a20c5 --- /dev/null +++ b/src-client/components/DimensionContainer.js @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import DimensionBox from './DimensionBox.js'; + +class DimensionBoxContainer extends Component { + + render () { + return ( +
+
+ + + + + + + +
+
+ +
+
+ ); + } +} + +export default DimensionBoxContainer; diff --git a/src-client/components/DirPathInput.js b/src-client/components/DirPathInput.js new file mode 100644 index 00000000..b588f78a --- /dev/null +++ b/src-client/components/DirPathInput.js @@ -0,0 +1,131 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +class DirPathInput extends Component { + + constructor (props) { + super(props); + this.state = { + dirPathValue: this.props.dirPath || '' + }; + this.handleClearValue = this.handleClearValue.bind(this); + this.handleChangeValue = this.handleChangeValue.bind(this); + this.handleOnKeyDown = this.handleOnKeyDown.bind(this); + } + + componentWillReceiveProps (nextProps) { + this.setState({dirPathValue: nextProps.dirPath}); + } + + getValue () { + return this.state.dirPathValue; + } + + focus () { + this.inputElement.focus(); + } + + handleClearValue (e) { + e.stopPropagation(); + e.preventDefault(); + this.setState({ + dirPathValue: '' + }); + } + + handleChangeValue (e) { + this.setState({ + dirPathValue: this.inputElement.value + }); + } + + handleOnKeyDown (e) { + if (e.keyCode == 13) { + const {onEnterKey} = this.props; + if (onEnterKey) { + onEnterKey(); + } + } + } + + render () { + const {label, isAutoComplete, options} = this.props; + const {dirPathValue} = this.state; + let dirsDataOptions = []; + if (options && options.length > 0) { + options.forEach((name, index) => { + dirsDataOptions.push( + + ); + }); + } + return ( +
+ +
+ + Path: + + this.inputElement = me} + value={dirPathValue || ''} + type="text" + className="form-control" + placeholder="Enter directory path" + onChange={this.handleChangeValue} + list="dirPathInputOptions" + autoComplete={isAutoComplete} + onKeyDown={this.handleOnKeyDown} + /> + {isAutoComplete && dirsDataOptions.length > 0 && + + {dirsDataOptions} + + } + + + +
+
+ ); + } + +} + +DirPathInput.propTypes = { + dirPath: PropTypes.string, + label: PropTypes.string, + isAutoComplete: PropTypes.bool, + options: PropTypes.array, + onEnterKey: PropTypes.func, +}; + +DirPathInput.defaultProps = { + dirPath: null, + label: 'Proxy URL:', + isAutoComplete: false, + options: [], + onEnterKey: null, +}; + +export default DirPathInput; diff --git a/src-client/components/GeneratorKeyTitleView.js b/src-client/components/GeneratorKeyTitleView.js new file mode 100644 index 00000000..c5f6f25f --- /dev/null +++ b/src-client/components/GeneratorKeyTitleView.js @@ -0,0 +1,58 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +class GeneratorKeyTitleView extends Component { + + constructor (props, content) { + super(props, content); + } + + render () { + const {generatorKey} = this.props; + if (generatorKey && generatorKey.length > 0 && generatorKey.indexOf('.') > 0) { + let parts = generatorKey.split('.'); + let partsItems = []; + for (let i = 0; i < parts.length; i++) { + if (i != parts.length - 1) { + partsItems.push( + {parts[i]} + ); + partsItems.push( + {'/'} + ); + } else { + partsItems.push( + {parts[i]} + ); + } + } + return {partsItems}; + } else { + return {generatorKey}; + } + } +} +GeneratorKeyTitleView.defaultProps = { + generatorKey: '' +}; +GeneratorKeyTitleView.propTypes = { + generatorKey: PropTypes.string +}; + +export default GeneratorKeyTitleView; diff --git a/src-client/components/InputComponentAutocomplete.js b/src-client/components/InputComponentAutocomplete.js new file mode 100644 index 00000000..e85e20c1 --- /dev/null +++ b/src-client/components/InputComponentAutocomplete.js @@ -0,0 +1,416 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { debounce } from 'lodash'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +const HtmlComponents = [ + 'input', + 'label', + 'fieldset', + 'button', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'span', + 'strong', + 'small', + 'em', + 'i', + 'div', + 'p', + 'path', + 'blockquote', + 'hr', + 'a', + 'select', + 'option', + 'img', + 'form', + 'table', + 'thead', + 'tbody', + 'tr', + 'td', + 'th', + 'ol', + 'ul', + 'li', + 'dl', + 'dt', + 'dd', + 'pre', + 'code', + 'header', + 'main', + 'article', + 'nav', + 'aside', + 'footer' +]; + +const formGroupStyle = { + position: 'relative' +}; +const popupStyle = { + position: 'absolute', + display: 'block', + top: '2em', +}; +const popoverListStyle = { + height: '5.5em', + width: '15em', + overflowX: 'auto', +}; +const textProbeStyle = { + padding: 0, + margin: 0, + position: 'absolute', + color: 'transparent', + zIndex: -1 +}; +const listItemSelectedStyle = { + margin: 0, + padding: '2px 4px', + borderRadius: '3px', + overflow: 'hidden', +}; +const listItemStyle = { + margin: 0, + padding: '2px 4px', + overflow: 'hidden', +}; + +const getCaretPosition = (input) => { + return input.selectionDirection == 'backward' ? input.selectionStart : input.selectionEnd; +}; + +const getCaretPixelPosition = ($textProbe, textValue) => { + $textProbe.text(textValue); + return $textProbe.outerWidth(); +}; + +class InputComponentAutocomplete extends Component { + + constructor (props) { + super(props); + this.handleKeyUp = this.handleKeyUp.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleTextChange = this.handleTextChange.bind(this); + this.handleClickListItem = this.handleClickListItem.bind(this); + this.handleClickInput = this.handleClickInput.bind(this); + this.selectListItem = this.selectListItem.bind(this); + this.getText = this.getText.bind(this); + this.state = { + showPopup: false, + popupValues: [], + text: '', + textPartIndex: -1, + caretPosition: 0, + caretPixelPosition: -1, + selectedItemIndex: -1, + }; + } + + componentWillMount () { + this.delayedScrollToSelected = debounce((e) => { + const list = this.popoverList; + const element = this.selectedListItem; + const scrollTop = list.scrollTop; + const offsetTop = element.offsetTop; + const delta = offsetTop - scrollTop; + if (delta < 0) { + list.scrollTop = scrollTop - 24; + } else if (delta > 48) { + list.scrollTop = scrollTop + 24; + } + }, 10); + } + + componentWillUnmount () { + this.delayedScrollToSelected.cancel; + } + + componentDidMount () { + this.input.focus(); + } + + handleKeyUp (e) { + if (e.which === 37 || e.which === 39) { + // arrow left and right + this.handleTextChange(e); + } + } + + handleKeyDown (e) { + if (e.which === 40 || e.which === 38) { + e.stopPropagation(); + e.preventDefault(); + // arrow down and up + const {showPopup, selectedItemIndex, popupValues} = this.state; + if (showPopup) { + if (selectedItemIndex < 0) { + this.setState({selectedItemIndex: 0}); + } else { + if (e.which === 40) { + if (selectedItemIndex < popupValues.length - 1) { + this.setState({selectedItemIndex: selectedItemIndex + 1}); + this.delayedScrollToSelected(e); + } + } else if (e.which === 38) { + if (selectedItemIndex > 0) { + this.setState({selectedItemIndex: selectedItemIndex - 1}); + this.delayedScrollToSelected(e); + } + } + } + } + } else if (e.which === 13) { + e.stopPropagation(); + e.preventDefault(); + const {showPopup, selectedItemIndex, textPartIndex, text} = this.state; + if (showPopup && selectedItemIndex >= 0 && textPartIndex >= 0) { + this.selectListItem(selectedItemIndex); + } else { + const {onSubmit} = this.props; + if (onSubmit) { + onSubmit(text); + } + } + } else if (e.which === 27) { + e.stopPropagation(); + e.preventDefault(); + const {showPopup} = this.state; + if (showPopup) { + this.setState({ + showPopup: false, + popupValues: [], + textPartIndex: -1, + caretPosition: 0, + caretPixelPosition: -1, + selectedItemIndex: -1, + }); + } else { + const {onCancel} = this.props; + if (onCancel) { + onCancel(); + } + } + } + } + + handleTextChange (e) { + let text = e.currentTarget.value; + if (text && text.length > 0) { + let values = []; + const caretPosition = getCaretPosition(this.input); + const textParts = text.split('.'); + let textPart; + let textPartLengthCumulative = 0; + let checkPart; + let checkPartIndex = -1; + let caretPixelPosition = -1; + textParts.forEach((p, partIndex) => { + if (p && p.length > 0) { + if (caretPosition > textPartLengthCumulative + && (textPartLengthCumulative + p.length) >= caretPosition) { + checkPart = text.substr(textPartLengthCumulative, caretPosition - textPartLengthCumulative); + textPart = p; + checkPartIndex = partIndex; + } + textPartLengthCumulative += p.length + 1; + } + }); + if (checkPart && checkPart.length > 0) { + caretPixelPosition = getCaretPixelPosition(this.$textProbe, text.substr(0, caretPosition)); + const checkText = checkPart.toUpperCase(); + values = this.props.componentNames.filter(i => i.toUpperCase().indexOf(checkText) >= 0); + values.sort((a, b) => { + const eq = a.toUpperCase().indexOf(checkText) - b.toUpperCase().indexOf(checkText); + if (eq === 0) { + return a.length - b.length; + } else { + return eq; + } + }); + if (values.length === 1 && checkText === values[0].toUpperCase()) { + values = []; + } + } + this.setState({ + showPopup: values.length > 0, + popupValues: values, + text: text, + textPartIndex: checkPartIndex, + caretPosition: caretPosition, + caretPixelPosition: caretPixelPosition, + selectedItemIndex: values.length > 0 ? 0 : -1, + }); + } else { + this.setState({ + showPopup: false, + popupValues: [], + text: text, + textPartIndex: -1, + caretPosition: 0, + caretPixelPosition: -1, + selectedItemIndex: -1, + }); + } + } + + handleClickListItem (e) { + if (e) { + e.stopPropagation(); + e.preventDefault(); + } + this.selectListItem(e.currentTarget.dataset.index); + this.input.focus(); + } + + handleClickInput (e) { + this.handleTextChange(e); + } + + selectListItem (index) { + const {popupValues, textPartIndex, text} = this.state; + let textParts = text.split('.'); + if (textPartIndex < textParts.length) { + textParts[textPartIndex] = popupValues[index]; + } + this.setState({ + text: textParts.join('.'), + showPopup: false, + popupValues: [], + textPartIndex: -1, + caretPosition: 0, + caretPixelPosition: -1, + selectedItemIndex: -1, + }); + } + + getText () { + return this.state.text; + } + + render () { + const {showPopup, popupValues, text, caretPixelPosition, selectedItemIndex} = this.state; + let popupStylePositioned = undefined; + if (showPopup) { + popupStylePositioned = Object.assign({}, popupStyle, { + left: 'calc(' + caretPixelPosition + 'px - 7.7em)', + }); + } + return ( +
+ +
+ { + this.input = me; + }} + className="form-control" + onChange={this.handleTextChange} + onKeyUp={this.handleKeyUp} + onKeyDown={this.handleKeyDown} + onClick={this.handleClickInput} + value={text}/> + { + this.$textProbe = $(me); + }}> + Probe + +
+
+

Type the name of single component.

+

Or you may type the construction of nested components: +  component1.component2.componentN +

+

Press enter to submit.

+
+
+ {showPopup && +
+
+
+
this.popoverList = me} + style={popoverListStyle}> + {popupValues.map((item, index) => { + if (selectedItemIndex >= 0 && index === selectedItemIndex) { + return ( +

this.selectedListItem = me} + className="bg-primary" + style={listItemSelectedStyle}> + + {item} + +

+ ); + } else { + return ( +

+ + {item} + +

+ ); + } + })} +
+
+
+ } +
+
+ ); + } +} + +InputComponentAutocomplete.propTypes = { + componentNames: PropTypes.array.isRequired, + onSubmit: PropTypes.func, + onCancel: PropTypes.func, +}; + +InputComponentAutocomplete.defaultProps = { + componentNames: HtmlComponents, + onSubmit: (text) => console.log('Submitting text: ', text), + onCancel: () => console.log('Cancelling'), +}; + +export default InputComponentAutocomplete; diff --git a/src-client/components/InputTextStateful.js b/src-client/components/InputTextStateful.js new file mode 100644 index 00000000..bd7862e7 --- /dev/null +++ b/src-client/components/InputTextStateful.js @@ -0,0 +1,97 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +class InputTextStateful extends Component { + + constructor (props, content) { + super(props, content); + this.state = { + value: this.props.value || this.props.defaultValue || '' + }; + this.handleOnChange = this.handleOnChange.bind(this); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.value) { + this.setState({ + value: nextProps.value + }); + } + } + + handleOnChange () { + this.setState({ + value: this.inputElement.value + }); + } + + getValue () { + return this.state.value; + } + + focus () { + this.inputElement.focus(); + } + + validate (value) { + const {validateFunc} = this.props; + if (validateFunc) { + return validateFunc(value) ? 'has-success' : 'has-error'; + } + } + + render () { + const {value} = this.state; + return ( +
+ this.inputElement = me} + className="form-control" + value={ value } + disabled={this.props.disabled} + list={this.props.list} + autoComplete={this.props.autoComplete} + placeholder={this.props.placeholder} + onChange={ this.handleOnChange } + /> +
+ ); + } +} +InputTextStateful.defaultProps = { + defaultValue: undefined, + value: undefined, + validateFunc: undefined, + type: 'text', + disabled: false, +}; +InputTextStateful.propTypes = { + defaultValue: PropTypes.string, + value: PropTypes.string, + validateFunc: PropTypes.func, + type: PropTypes.string, + list: PropTypes.string, + autoComplete: PropTypes.string, + placeholder: PropTypes.string, + disabled: PropTypes.bool, +}; + +export default InputTextStateful; diff --git a/src-client/components/MainFrame.js b/src-client/components/MainFrame.js new file mode 100644 index 00000000..835915e1 --- /dev/null +++ b/src-client/components/MainFrame.js @@ -0,0 +1,41 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; + +import AppContainer from 'modules/app/containers/AppContainer'; +import AppSpinner from 'modules/app/containers/AppSpinner'; +import AppMessage from 'modules/app/containers/AppMessage'; + +class MainFrame extends Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
+ + + +
+ ); + } +} + +export default MainFrame; + diff --git a/src-client/components/MetaOptionInput.js b/src-client/components/MetaOptionInput.js new file mode 100644 index 00000000..c8d53951 --- /dev/null +++ b/src-client/components/MetaOptionInput.js @@ -0,0 +1,204 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isObject, isString, isNumber, isBoolean, get, set, debounce, find } from 'lodash'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +const getTypeOfProperty = (propertyValue) => { + if (isString(propertyValue)) { + return 'text'; + } else if (isNumber(propertyValue)) { + return 'number'; + } else if (isBoolean(propertyValue)) { + return 'checkbox'; + } +}; + +class MetaOptionInput extends Component { + + constructor (props) { + + super(props); + this.state = { + value: this.props.value, + propertyType: getTypeOfProperty(this.props.value), + }; + this.handleChangeInputValue = this.handleChangeInputValue.bind(this); + this.handleChangeCheckboxValue = this.handleChangeCheckboxValue.bind(this); + this.handleChangeOptionValue = this.handleChangeOptionValue.bind(this); + } + + componentWillMount () { + this.delayedChangeInputValue = debounce((value) => { + if (this.props.onChangeValue) { + this.props.onChangeValue(value); + } + }, 500); + } + + componentWillUnmount () { + this.delayedChangeInputValue.cancel; + } + + handleChangeInputValue (e) { + let value = null; + const {propertyType} = this.state; + const {inputElement} = this.refs; + if (propertyType) { + if (propertyType === 'text') { + value = inputElement.value; + } else if (propertyType === 'number') { + value = parseFloat(inputElement.value); + } + } + this.setState({ + value: value + }); + e.persist(); + this.delayedChangeInputValue(value); + } + + handleChangeOptionValue (e) { + e.stopPropagation(); + e.preventDefault(); + let value = null; + const {propertyType} = this.state; + const {onChangeValue} = this.props; + const newValue = e.currentTarget.dataset.val; + if (propertyType) { + if (propertyType === 'text') { + value = newValue; + } else if (propertyType === 'number') { + value = parseFloat(newValue); + } + } + this.setState({ + value: value + }); + if (onChangeValue) { + onChangeValue(value); + } + } + + handleChangeCheckboxValue (e) { + let value = this.refs.inputElement.checked; + const {onChangeValue} = this.props; + this.setState({ + value: value + }); + if (onChangeValue) { + onChangeValue(value); + } + } + + render () { + const {value, propertyType} = this.state; + const {help: {description, options: helpOptions}} = this.props; + let element = null; + if (helpOptions && helpOptions.length > 0) { + let valueLabel = helpOptions[0].label; + let selectedOptionIndex = helpOptions.findIndex(i => i.value === value); + if (selectedOptionIndex >= 0) { + valueLabel = helpOptions[selectedOptionIndex].label; + } + element = ( +
+
+ +
+
+ +
    + {helpOptions.map((u, index) => { + return ( +
  • + + {u.label} + +
  • + ); + })} +
+
+
+ ); + } else if (propertyType === 'checkbox') { + element = ( +
+ +
+ ); + + } else if (propertyType === 'text' || propertyType === 'number') { + element = ( +
+
+ +
+ +
+ ); + } + return ( +
+ {element} +
+ ); + } + +} + +MetaOptionInput.propTypes = { + value: PropTypes.any, + help: PropTypes.any, + onChangeValue: PropTypes.func.isRequired, +}; + +MetaOptionInput.defaultProps = { + value: null, + help: {}, + onChangeValue: (value) => { + console.log(JSON.stringify(value)); + }, +}; + +export default MetaOptionInput; \ No newline at end of file diff --git a/src-client/components/MetaOptionsContainer.js b/src-client/components/MetaOptionsContainer.js new file mode 100644 index 00000000..32e160ee --- /dev/null +++ b/src-client/components/MetaOptionsContainer.js @@ -0,0 +1,88 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { forOwn, isObject, isNumber, isString, isBoolean } from 'lodash'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import MetaOptionsInput from './MetaOptionInput'; + +class MetaOptionsContainer extends Component { + + constructor (props) { + super(props); + this.state = { + optionsObject: Object.assign({}, this.props.optionsObject) + }; + } + + getOptionsObject () { + const {optionsObject} = this.state; + const {optionsObject: originalObject} = this.props; + let resultObject = {}; + forOwn(optionsObject, (value, prop) => { + if (isString(value)) { + resultObject[prop] = value && value.length > 0 ? value : originalObject[prop]; + } else if (isNumber(value)) { + resultObject[prop] = value && isFinite(value) ? value : originalObject[prop]; + } else if (isBoolean(value)) { + resultObject[prop] = value !== undefined ? value : originalObject[prop]; + } + }); + return resultObject; + } + + handleChangeValue = (prop) => (value) => { + let newObject = Object.assign({}, this.state.optionsObject); + newObject[prop] = value; + this.setState({optionsObject: newObject}); + }; + + render () { + const {optionsObject} = this.state; + const {optionsHelpObject} = this.props; + let optionsList = []; + forOwn(optionsObject, (value, prop) => { + if (!isObject(value)) { + optionsList.push( + + ); + } + }); + return ( +
+
+ {optionsList} +
+
+ ); + } +} + +MetaOptionsContainer.propTypes = { + optionsObject: PropTypes.any.isRequired, + optionsHelpObject: PropTypes.any, +}; + +MetaOptionsContainer.defaultProps = { + optionObject: null, + optionsHelpObject: {}, +}; + +export default MetaOptionsContainer; diff --git a/src-client/components/MouseMenuOverlay.js b/src-client/components/MouseMenuOverlay.js new file mode 100644 index 00000000..6e860b68 --- /dev/null +++ b/src-client/components/MouseMenuOverlay.js @@ -0,0 +1,132 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import { MenuItem } from 'react-bootstrap'; + +const MENU_WIDTH = 150; +const MENU_HEIGHT = 200; + +class MouseMenuOverlay extends Component { + + constructor (props) { + super(props); + this.handleCopy = this.handleCopy.bind(this); + this.handleCut = this.handleCut.bind(this); + this.handleReplace = this.handleReplace.bind(this); + this.handleBefore = this.handleBefore.bind(this); + this.handleFirst = this.handleFirst.bind(this); + this.handleAfter = this.handleAfter.bind(this); + this.handleDelete = this.handleDelete.bind(this); + } + + shouldComponentUpdate (nextProps, nextState) { + const {pointX, pointY, showMenu} = this.props; + return nextProps.pointX !== pointX || nextProps.pointY !== pointY || nextProps.showMenu !== showMenu; + } + + handleCopy (e) { + e.stopPropagation(); + e.preventDefault(); + const {itemKey, onMenuItemClick} = this.props; + onMenuItemClick('copy', itemKey); + } + + handleCut (e) { + e.stopPropagation(); + e.preventDefault(); + const {itemKey, onMenuItemClick} = this.props; + onMenuItemClick('cut', itemKey); + } + + handleReplace (e) { + e.stopPropagation(); + e.preventDefault(); + const {itemKey, onMenuItemClick} = this.props; + onMenuItemClick('replace', itemKey); + } + + handleBefore (e) { + e.stopPropagation(); + e.preventDefault(); + const {itemKey, onMenuItemClick} = this.props; + onMenuItemClick('before', itemKey); + } + + handleFirst (e) { + e.stopPropagation(); + e.preventDefault(); + const {itemKey, onMenuItemClick} = this.props; + onMenuItemClick('first', itemKey); + } + + handleAfter (e) { + e.stopPropagation(); + e.preventDefault(); + const {itemKey, onMenuItemClick} = this.props; + onMenuItemClick('after', itemKey); + } + + handleDelete (e) { + e.stopPropagation(); + e.preventDefault(); + const {itemKey, onMenuItemClick} = this.props; + onMenuItemClick('delete', itemKey); + } + + render () { + const {showMenu, pointX, pointY, panel} = this.props; + if (!showMenu) { + return null; + } + const $panel = $(panel); + const x = pointX - $panel.offset().left + panel.scrollLeft; + const y = pointY - $panel.offset().top + panel.scrollTop; + const clientWidth = panel.clientWidth; + const clientHeight = panel.clientHeight; + const widthDelta = clientWidth - x; + const heightDelta = clientHeight - y; + let top = y; + let left = x; + if (widthDelta < MENU_WIDTH) { + left -= MENU_WIDTH; + } + if (y - panel.scrollTop > MENU_HEIGHT &&heightDelta < MENU_HEIGHT) { + top -= MENU_HEIGHT; + } + return ( +
+
    + Copy + Cut + + Replace + + Add Before + Insert First + Add After + + Delete +
+
+ ); + } +} + +export default MouseMenuOverlay; diff --git a/src-client/components/NamespaceCard.js b/src-client/components/NamespaceCard.js new file mode 100644 index 00000000..21a2cfeb --- /dev/null +++ b/src-client/components/NamespaceCard.js @@ -0,0 +1,198 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import defaultScreenshotSrc from 'assets/app/css/img/screenshot.png'; + +const titleStyle = { + margin: '0px', + position: 'relative', + padding: '3px', + backgroundColor: '#f5f5f5', + borderRadius: '3px' +}; + +const containerStyle = { + display: 'flex', + flexDirection: 'row', + flexFlow: 'nowrap', + alignItems: 'center', + width: '600px', +}; +const screenshotSectionStyle = { + flexGrow: 0, + minWidth: '200px', + width: '200px', + padding: '0.5em', +}; +const mainSectionStyle = { + marginLeft: '1.5em', + flexGrow: 2, + padding: '0.5em 0.5em 0.5em 0', + height: '200px', + overflow: 'auto', +}; +const installBtnSectionStyle = { + flexGrow: 0, + minWidth: '200px', + width: '200px', + padding: '0.5em', + alignItems: 'center', +}; +const bottomToolbarStyle = { + flexGrow: 2, + display: 'flex', + flexDirection: 'row', + padding: '0.5em 0.5em 0.5em 0', + alignItems: 'center', + justifyContent: 'flex-end', +}; + +class NamespaceCard extends Component { + + constructor (props) { + super(props); + this.handleInstallClick = this.handleInstallClick.bind(this); + } + + handleInstallClick (e) { + e.stopPropagation(); + e.preventDefault(); + const tarballUrl = e.currentTarget.dataset.url; + const {onInstallClick} = this.props; + if (onInstallClick) { + onInstallClick(tarballUrl); + } + } + + render () { + const {style, repoData, currentVersion} = this.props; + const { + gitHubRepo, + gitHubOwner, + description, + screenshotUrl, + releases, + structorVersion + } = repoData; + const repoUrl = `https://github.com/${gitHubOwner}/${gitHubRepo}`; + return ( +
+
+
+
+ {releases && releases.length > 0 && +
+
+ + + +
+
+ } + {gitHubRepo} +
+
+
+ +
+
+

+ {description} +

+
+
+
+
+ + Structor version: + + + {structorVersion} + +
+ +
+
+
+
+ ); + } +} + +export default NamespaceCard; diff --git a/src-client/components/OptionInput.js b/src-client/components/OptionInput.js new file mode 100644 index 00000000..0519beda --- /dev/null +++ b/src-client/components/OptionInput.js @@ -0,0 +1,213 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isObject, isString, isNumber, isBoolean, get, set, debounce } from 'lodash'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +class OptionInput extends Component { + + constructor (props) { + + super(props); + + let valueType = 'text'; + if (props.valueObject && isObject(props.valueObject)) { + var value = get(props.valueObject, props.path); + valueType = this.getTypeOfProperty(value); + } + let label = props.path.replace('.', ' / '); + this.state = { + valueObject: props.valueObject, + label: label, + propertyType: valueType + }; + + this.handleChangeInputValue = this.handleChangeInputValue.bind(this); + this.handleChangeCheckboxValue = this.handleChangeCheckboxValue.bind(this); + this.handleDelete = this.handleDelete.bind(this); + this.handleFocus = this.handleFocus.bind(this); + } + + componentWillReceiveProps (nextProps) { + const {valueObject, path} = nextProps; + let valueType = 'text'; + let value; + if (valueObject && isObject(valueObject)) { + value = get(valueObject, path); + valueType = this.getTypeOfProperty(value); + } + let label = path.replace('.', ' / '); + this.setState({ + valueObject: valueObject, + label: label, + propertyType: valueType + }); + } + + componentWillMount () { + this.delayedChangeInputValue = debounce((e) => { + if (this.props.onChangeValue) { + this.props.onChangeValue(this.state.valueObject); + } + }, 1000); + } + + componentWillUnmount () { + this.delayedChangeInputValue.cancel; + } + + getValueFromObject () { + return get(this.state.valueObject, this.props.path); + } + + handleChangeInputValue (e) { + let value = null; + const {propertyType} = this.state; + if (propertyType) { + if (propertyType === 'text') { + value = this.inputElement.value; + } else if (propertyType === 'checkbox') { + value = this.inputElement.checked; + } else if (propertyType === 'number') { + value = parseFloat(this.inputElement.value); + } + } + let valueObject = set({}, this.props.path, value); + this.setState({ + valueObject: valueObject + }); + e.persist(); + this.delayedChangeInputValue(e); + } + + handleChangeCheckboxValue (e) { + let value = null; + const {propertyType} = this.state; + const {path, onChangeValue} = this.props; + if (propertyType) { + if (propertyType === 'checkbox') { + value = this.inputElement.checked; + } + } + let valueObject = set({}, path, value); + this.setState({ + valueObject: valueObject + }); + if (onChangeValue) { + onChangeValue(valueObject); + } + } + + handleDelete (e) { + if (this.props.onDeleteValue) { + this.props.onDeleteValue(this.props.path); + } + } + + handleFocus () { + if (this.props.onSetFocus) { + this.props.onSetFocus(this.props.path); + } + } + + getTypeOfProperty (propertyValue) { + if (isString(propertyValue)) { + return 'text'; + } else if (isNumber(propertyValue)) { + return 'number'; + } else if (isBoolean(propertyValue)) { + return 'checkbox'; + } + } + + render () { + const {propertyType, label} = this.state; + let element = null; + let style = { + height: '1.55em', paddingTop: '2px', paddingBottom: '2px' + }; + if (propertyType === 'checkbox') { + style.width = '2em'; + element = ( +
+
+ +
+
+ this.inputElement = me} + type={propertyType} + checked={this.getValueFromObject()} + onFocus={this.handleFocus} + style={style} + onChange={this.handleChangeCheckboxValue} + /> +
+
+ ); + + } else if (propertyType === 'text' || propertyType === 'number') { + element = ( +
+
+ +
+
+ this.inputElement = me} + type={propertyType} + className="form-control" + value={this.getValueFromObject()} + style={style} + onFocus={this.handleFocus} + onChange={this.handleChangeInputValue} + /> +
+
+ ); + } + return ( +
+

+ {label} +

+ {element} +
+ ); + } + +} + +OptionInput.propTypes = { + valueObject: PropTypes.any, + label: PropTypes.string +}; + +OptionInput.defaultProps = { + valueObject: null, + label: 'Option:' +}; + +export default OptionInput; \ No newline at end of file diff --git a/src-client/components/PageComponentForm.js b/src-client/components/PageComponentForm.js new file mode 100644 index 00000000..061ae9de --- /dev/null +++ b/src-client/components/PageComponentForm.js @@ -0,0 +1,97 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { uniqueId } from 'lodash'; +import { Panel } from 'react-bootstrap'; + +class PageComponentForm extends Component { + + constructor (props) { + super(props); + this.state = { + pagePath: this.props.pagePath, + pageTitle: this.props.pageTitle + }; + this.validatePagePath = this.validatePagePath.bind(this); + this.handlePagePathChange = this.handlePagePathChange.bind(this); + } + + getOptions () { + return { + pageName: uniqueId('Page'), + pagePath: this.state.pagePath, + pageTitle: this.state.pageTitle, + makeIndexRoute: this.refs.pageIndexCheckbox.checked + }; + } + + validatePagePath () { + const {pagePath} = this.state; + if (pagePath && pagePath.length > 0 && pagePath.charAt(0) === '/') { + return 'has-success'; + } else { + return 'has-error'; + } + } + + handlePagePathChange () { + this.setState({ + pagePath: this.refs.pagePathInput.value + }); + } + + render () { + return ( + + + + + + +
+ +
+ + +
+
+ +
+
+
+ ); + } +} + +export default PageComponentForm; diff --git a/src-client/components/PageTreeViewItem.js b/src-client/components/PageTreeViewItem.js new file mode 100644 index 00000000..e3a57aa8 --- /dev/null +++ b/src-client/components/PageTreeViewItem.js @@ -0,0 +1,222 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { print } from 'api'; + +const linkStyle = {outline: 'none', color: '#2185D0'}; +const propsStyle = {margin: '0 0 0 0.5em', fontWeight: '200', cursor: 'pointer'}; +const namespaceStyle = {margin: '0 0 0 0.3em'}; +const topTagWrapperStyle = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +}; +const bottomTagWrapperStyle = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +}; + +class PageTreeViewItem extends Component { + + constructor (props) { + super(props); + } + + handleClick = (e) => { + e.preventDefault(); + e.stopPropagation(); + }; + + handleMouseDownStartTag = (e) => { + e.preventDefault(); + e.stopPropagation(); + const {onSelect, isSelected, itemKey, onShowMouseMenu, onHideMouseMenu} = this.props; + if (e.button === 0) { + if (onSelect) { + onSelect(itemKey, e.metaKey || e.ctrlKey); + } + onHideMouseMenu(); + } else if (e.button === 2) { + if (onSelect && !isSelected) { + onSelect(itemKey, e.metaKey || e.ctrlKey); + } + onShowMouseMenu(e, itemKey); + } else { + onHideMouseMenu(); + } + }; + + handleMouseDownEndTag = (e) => { + e.preventDefault(); + e.stopPropagation(); + const {onSelect, itemKey, onHideMouseMenu} = this.props; + if (e.button === 0) { + if (onSelect) { + onSelect(itemKey, e.metaKey || e.ctrlKey); + } + onHideMouseMenu(); + } else { + onHideMouseMenu(); + } + }; + + render () { + + let content = null; + + const { + isSelected, + isForCutting, + isForCopying, + itemKey, + children, + type, + namespace, + modelProps, + beforeNamePlaceholder + } = this.props; + const {onMouseEnter, onMouseLeave} = this.props; + + let className; + if (isSelected) { + className = 'umy-treeview-list-item-selected'; + } else if (isForCopying) { + className = 'umy-treeview-list-item-for-copying'; + } else if (isForCutting) { + className = 'umy-treeview-list-item-for-cutting'; + } else { + className = 'umy-treeview-list-item'; + } + + const label = (
{type}
); + const namespaceLabel = namespace ? ( +
{'[' + namespace + ']'}
) + : null; + let props = print.printProps(modelProps); + if (children && children.length > 0) { + content = ( +
  • +
    +
    {'<'}
    + {beforeNamePlaceholder} + +
    + {label} + {namespaceLabel} +
    +
    + { props && +
    + {props} +
    + } +
    {'>'}
    +
    + {children} +
    +
    {' + {beforeNamePlaceholder} + + {label} + +
    {'>'}
    +
    +
  • + ); + } else { + content = ( +
  • +
    +
    {'<'}
    + {beforeNamePlaceholder} + +
    + {label} + {namespaceLabel} +
    +
    + { props && +
    + {props} +
    + } +
    {' />'}
    +
    +
  • + ); + } + + return content; + } +} + +PageTreeViewItem.defaultProps = { + itemKey: undefined, + isSelected: false, + onSelect: undefined, + type: undefined, + namespace: undefined +}; +PageTreeViewItem.propTypes = { + itemKey: PropTypes.string.isRequired, + inSelected: PropTypes.bool, + onSelect: PropTypes.func, + type: PropTypes.string.isRequired, + namespace: PropTypes.string +}; + +export default PageTreeViewItem; + diff --git a/src-client/components/PageTreeViewItemText.js b/src-client/components/PageTreeViewItemText.js new file mode 100644 index 00000000..1793e9db --- /dev/null +++ b/src-client/components/PageTreeViewItemText.js @@ -0,0 +1,122 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ContentEditable from './ContentEditable.js'; + +class PageTreeViewItemText extends Component { + + constructor (props) { + super(props); + this.state = { + textValue: this.props.textValue, + isEditable: false + }; + this.handleTextClick = this.handleTextClick.bind(this); + this.handleBlur = this.handleBlur.bind(this); + this.handleChange = this.handleChange.bind(this); + } + + handleTextClick (e) { + e.preventDefault(); + e.stopPropagation(); + if (this.props.onSelect) { + this.props.onSelect(this.props.itemKey); + } + this.setState({ + isEditable: true + }); + } + + handleBlur (e) { + this.props.onChangeText(e.target.value, this.props.itemKey); + this.setState({ + isEditable: false + }); + } + + handleChange (e) { + this.setState({ + textValue: e.target.value + }); + } + + componentDidMount () { + if (this.state.isEditable === true) { + $(this.refs.textItemElement).focus(); + } + } + + componentWillReceiveProps (nextProps) { + this.setState({ + textValue: nextProps.textValue + }); + } + + render () { + let content = null; + if (this.state.isEditable === true) { + content = ( +
    + {this.props.textValue} + +
    + ); + } else { + content = ( +
    + {this.props.textValue ? this.props.textValue : empty} +
    + ); + } + return ( +
    + {content} +
    + ); + } + +} + +PageTreeViewItemText.defaultProps = { + itemKey: undefined, + textValue: '', + onChangeText: undefined, + onSelect: undefined +}; +PageTreeViewItemText.propTypes = { + itemKey: PropTypes.string.isRequired, + textValue: PropTypes.string, + onChangeText: PropTypes.func.isRequired, + onSelect: PropTypes.func +}; + +export default PageTreeViewItemText; + + diff --git a/src-client/components/PageTreeViewPlaceholder.js b/src-client/components/PageTreeViewPlaceholder.js new file mode 100644 index 00000000..93cfcc2e --- /dev/null +++ b/src-client/components/PageTreeViewPlaceholder.js @@ -0,0 +1,69 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, {Component} from 'react'; + +const lineStyle = { + height: '0.8em', +}; + +const holderStyle = { + width: '1em', + height: '1em', + marginLeft: '2em', + marginTop: '-.1em', +}; + +class PageTreeViewPlaceholder extends Component { + + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + + handleClick(e) { + e.stopPropagation(); + e.preventDefault(); + const {onClick, itemKey} = this.props; + if(onClick){ + onClick(itemKey); + } + }; + + render() { + const {iconClassName, itemKey, title, onMouseEnter, onMouseLeave} = this.props; + const icon = iconClassName ? iconClassName : 'fa fa-plus'; + return ( +
  • +
    +
    + +
    +
    +
    +
  • + ); + } +} + +export default PageTreeViewPlaceholder; \ No newline at end of file diff --git a/src-client/components/PlaceholderCircle.js b/src-client/components/PlaceholderCircle.js new file mode 100644 index 00000000..252354e2 --- /dev/null +++ b/src-client/components/PlaceholderCircle.js @@ -0,0 +1,61 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, {Component} from 'react'; + +const holderStyle = { + width: '1em', + height: '1em', +}; + +class PlaceholderCircle extends Component { + + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + + handleClick(e) { + e.stopPropagation(); + e.preventDefault(); + const {onClick, itemKey} = this.props; + if(onClick){ + onClick(itemKey); + } + }; + + render() { + const {iconClassName, title, itemKey, onMouseEnter, onMouseLeave} = this.props; + const icon = iconClassName ? iconClassName : 'fa fa-plus'; + return ( +
    +
    + +
    +
    +
    + ); + } +} + +export default PlaceholderCircle; \ No newline at end of file diff --git a/src-client/components/ProxyInput.js b/src-client/components/ProxyInput.js new file mode 100644 index 00000000..e9a628bd --- /dev/null +++ b/src-client/components/ProxyInput.js @@ -0,0 +1,90 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +class ProxyInput extends Component { + + constructor (props) { + super(props); + this.state = { + urlValue: this.props.urlValue || '' + }; + this.handleClearUrlValue = this.handleClearUrlValue.bind(this); + this.handleChangeUrlValue = this.handleChangeUrlValue.bind(this); + } + + getUrlValue () { + return this.inputElement.value; + } + + handleClearUrlValue (e) { + e.stopPropagation(); + e.preventDefault(); + this.setState({ + urlValue: '' + }); + } + + handleChangeUrlValue (e) { + this.setState({ + urlValue: this.inputElement.value + }); + } + + render () { + return ( +
    + +
    + + URL: + + this.inputElement = me} + value={this.state.urlValue} + type="text" + className="form-control" + placeholder="Enter value" + onChange={this.handleChangeUrlValue} + > + + + + +
    +
    + ); + } + +} + +ProxyInput.propTypes = { + urlValue: PropTypes.string, + label: PropTypes.string +}; + +ProxyInput.defaultProps = { + urlValue: null, + label: 'Proxy URL:' +}; + +export default ProxyInput; diff --git a/src-client/components/StyleNumberInput.js b/src-client/components/StyleNumberInput.js new file mode 100644 index 00000000..76bd2cab --- /dev/null +++ b/src-client/components/StyleNumberInput.js @@ -0,0 +1,244 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isObject, isString, get, set, debounce, startCase } from 'lodash'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +const getValueOfProperty = (propertyValue) => { + if (propertyValue) { + let checkValue = parseFloat(propertyValue); + if (isFinite(checkValue)) { + return checkValue; + } + } + return 0; +}; + +const getStateObject = (valueObject, path) => { + let value; + if (valueObject && isObject(valueObject)) { + value = get(valueObject, path); + } + let label = path.split('.'); + if (label && label.length > 1) { + label = startCase(label[1]); + } else { + label = path.replace('.', ' / '); + } + return { + valueObject: valueObject, + label: label, + value: getValueOfProperty(value), + }; +}; + +const rowContainerStyle = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', + position: 'relative', + width: '100%', + marginTop: '3px', +}; + +const inputStyle = { + height: '1.55em', paddingTop: '2px', paddingBottom: '2px' +}; + +const buttonStyle = { + width: '2em', +}; + +const doubleButtonStyle = { + width: '4em', +}; + +const checkBoxStyle = {margin: 0}; +const labelStyle = {margin: '0px 0px 0px 3px'}; + +class StyleNumberInput extends Component { + + constructor (props) { + + super(props); + this.state = getStateObject(props.valueObject, props.path); + this.handleChangeInputValue = this.handleChangeInputValue.bind(this); + this.handleToggleOption = this.handleToggleOption.bind(this); + this.handleToggleFavorite = this.handleToggleFavorite.bind(this); + this.changeValueByMouse = this.changeValueByMouse.bind(this); + this.changeValue = this.changeValue.bind(this); + } + + componentWillReceiveProps (nextProps) { + const {valueObject, path} = nextProps; + this.setState(getStateObject(valueObject, path)); + } + + componentDidMount () { + this.delayedChangeInputValue = debounce(valueObject => { + if (this.props.onChangeValue) { + this.props.onChangeValue(valueObject); + } + }, 1000); + } + + componentWillUnmount () { + this.delayedChangeInputValue.cancel; + } + + handleChangeInputValue (e) { + const value = e.currentTarget.value; + this.setState({value}); + this.changeValue(value, true); + } + + handleToggleOption (e) { + const {path, onSet} = this.props; + const checked = e.currentTarget.checked; + onSet(path, checked); + } + + handleToggleFavorite (e) { + e.stopPropagation(); + e.preventDefault(); + const {path, onToggleFavorite} = this.props; + onToggleFavorite(path); + } + + handleMouseDown = (direction) => (e) => { + e.stopPropagation(); + e.preventDefault(); + this.setState({isMouseDown: true}); + this.changeValueByMouse(direction); + }; + + handleMouseUp = (e) => { + e.stopPropagation(); + e.preventDefault(); + this.setState({isMouseDown: false}); + }; + + changeValueByMouse (direction) { + setTimeout(() => { + const {value} = this.state; + let newValue = direction === 'up' ? (value + 1) : (value - 1); + this.setState({value: newValue}); + this.changeValue(newValue); + if (this.state.isMouseDown) { + this.changeValueByMouse(direction); + } + }, 200); + }; + + changeValue (newValue, delay = false) { + const value = getValueOfProperty(newValue); + let valueObject = set({}, this.props.path, value); + if (delay) { + this.delayedChangeInputValue(valueObject); + } else { + this.props.onChangeValue(valueObject); + } + } + + render () { + const {isSet, isFavorite} = this.props; + const {label, units, value} = this.state; + return ( +
    +
    + +
    +

    + {isSet ? {label} : {label}} +

    +
    +
    + +
    +
    + {isSet && +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    +
    + } +
    + ); + } +} + +StyleNumberInput.propTypes = { + valueObject: PropTypes.any.isRequired, + path: PropTypes.string.isRequired, + isSet: PropTypes.bool.isRequired, + onSet: PropTypes.func.isRequired, + onChangeValue: PropTypes.func.isRequired, +}; + +StyleNumberInput.defaultProps = { + valueObject: null, + path: 'style.width', + isSet: false, + onSet: (path, checked) => { + console.log(path, checked); + }, + onChangeValue: (valueObject) => { + console.log(JSON.stringify(valueObject)); + }, +}; + +export default StyleNumberInput; \ No newline at end of file diff --git a/src-client/components/StyleOptionSelect.js b/src-client/components/StyleOptionSelect.js new file mode 100644 index 00000000..7d3124b9 --- /dev/null +++ b/src-client/components/StyleOptionSelect.js @@ -0,0 +1,169 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isObject, get, set, startCase } from 'lodash'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +const getStateObject = (valueObject, path) => { + let value; + if (valueObject && isObject(valueObject)) { + value = get(valueObject, path); + } + let label = path.split('.'); + if (label && label.length > 1) { + label = startCase(label[1]); + } else { + label = path.replace('.', ' / '); + } + return { + valueObject: valueObject, + label: label, + value: value, + }; +}; + +const rowContainerStyle = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', + position: 'relative', + width: '100%', + marginTop: '3px', +}; + +const checkBoxStyle = {margin: 0}; +const labelStyle = {margin: '0px 0px 0px 3px'}; + +class StyleOptionSelect extends Component { + + constructor (props) { + super(props); + this.state = getStateObject(props.valueObject, props.path); + this.handleToggleOption = this.handleToggleOption.bind(this); + this.handleToggleFavorite = this.handleToggleFavorite.bind(this); + this.handleChangeValue = this.handleChangeValue.bind(this); + } + + componentWillReceiveProps (nextProps) { + const {valueObject, path} = nextProps; + this.setState(getStateObject(valueObject, path)); + } + + handleToggleOption (e) { + const {path, onSet} = this.props; + const checked = e.currentTarget.checked; + onSet(path, checked); + } + + handleToggleFavorite (e) { + e.stopPropagation(); + e.preventDefault(); + const {path, onToggleFavorite} = this.props; + onToggleFavorite(path); + } + + handleChangeValue (e) { + const newValue = e.currentTarget.dataset.val; + let valueObject = set({}, this.props.path, newValue); + this.props.onChangeValue(valueObject); + } + + render () { + const {isSet, valueList, isFavorite} = this.props; + const {label, value} = this.state; + return ( +
    +
    + +
    +

    + {isSet ? {label} : {label}} +

    +
    +
    + +
    +
    + {isSet && +
    +
    + +
      + {valueList.map((u, index) => { + return ( +
    • + + {u} + +
    • + ); + })} +
    +
    +
    + } +
    + ); + } +} + +StyleOptionSelect.propTypes = { + valueObject: PropTypes.any.isRequired, + path: PropTypes.string.isRequired, + valueList: PropTypes.array.isRequired, + isSet: PropTypes.bool.isRequired, + onSet: PropTypes.func.isRequired, + onChangeValue: PropTypes.func.isRequired, +}; + +StyleOptionSelect.defaultProps = { + valueObject: null, + path: 'style.width', + valueList: ['option1', 'option2', 'option3'], + isSet: false, + onSet: (path, checked) => { + console.log(path, checked); + }, + onChangeValue: (valueObject) => { + console.log(JSON.stringify(valueObject)); + }, +}; + +export default StyleOptionSelect; \ No newline at end of file diff --git a/src-client/components/StyleSizeInput.js b/src-client/components/StyleSizeInput.js new file mode 100644 index 00000000..6ab06d08 --- /dev/null +++ b/src-client/components/StyleSizeInput.js @@ -0,0 +1,305 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isObject, isString, get, set, debounce, startCase } from 'lodash'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +const unitList = ['px', 'em', '%', 'ex', 'ch', 'rem', 'vw', 'vh', 'cm', 'mm', 'in', 'pt', 'pc']; + +const getUnitsOfProperty = (propertyValue) => { + if (propertyValue) { + let checkString; + if (isString(propertyValue)) { + checkString = propertyValue; + } else { + checkString = '' + propertyValue; + } + checkString = checkString.toLowerCase(); + let found = unitList.find(u => checkString.indexOf(u) > 0); + if (found) { + return found; + } else { + return 'px'; + } + } + return 'px'; +}; + +const getValueOfProperty = (propertyValue) => { + if (propertyValue) { + let checkValue = parseFloat(propertyValue); + if (isFinite(checkValue)) { + return checkValue; + } + } + return 0; +}; + +const getStateObject = (valueObject, path) => { + let value; + if (valueObject && isObject(valueObject)) { + value = get(valueObject, path); + } + let label = path.split('.'); + if (label && label.length > 1) { + label = startCase(label[1]); + } else { + label = path.replace('.', ' / '); + } + return { + valueObject: valueObject, + label: label, + value: getValueOfProperty(value), + units: getUnitsOfProperty(value), + }; +}; + +const rowContainerStyle = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', + position: 'relative', + width: '100%', + marginTop: '3px', +}; + +const inputStyle = { + height: '1.55em', paddingTop: '2px', paddingBottom: '2px' +}; + +const buttonStyle = { + width: '2em', +}; + +const doubleButtonStyle = { + width: '4em', +}; + +const checkBoxStyle = {margin: 0}; +const labelStyle = {margin: '0px 0px 0px 3px'}; + +class StyleSizeInput extends Component { + + constructor (props) { + + super(props); + + this.state = getStateObject(props.valueObject, props.path); + + this.handleChangeInputValue = this.handleChangeInputValue.bind(this); + this.handleToggleOption = this.handleToggleOption.bind(this); + this.handleToggleFavorite = this.handleToggleFavorite.bind(this); + this.handleChangeUnits = this.handleChangeUnits.bind(this); + this.changeValueByMouse = this.changeValueByMouse.bind(this); + this.changeValue = this.changeValue.bind(this); + } + + componentWillReceiveProps (nextProps) { + const {valueObject, path} = nextProps; + this.setState(getStateObject(valueObject, path)); + } + + componentDidMount () { + this.delayedChangeInputValue = debounce(valueObject => { + if (this.props.onChangeValue) { + this.props.onChangeValue(valueObject); + } + }, 1000); + } + + componentWillUnmount () { + this.delayedChangeInputValue.cancel; + } + + handleChangeInputValue (e) { + const value = e.currentTarget.value; + this.setState({value}); + this.changeValue(value, this.state.units, true); + } + + handleToggleOption (e) { + const {path, onSet} = this.props; + const checked = e.currentTarget.checked; + onSet(path, checked); + } + + handleToggleFavorite (e) { + e.stopPropagation(); + e.preventDefault(); + const {path, onToggleFavorite} = this.props; + onToggleFavorite(path); + } + + handleChangeUnits (e) { + const newUnits = e.currentTarget.dataset.units; + this.setState({ + units: newUnits, + }); + this.changeValue(this.state.value, newUnits); + } + + handleMouseDown = (direction) => (e) => { + e.stopPropagation(); + e.preventDefault(); + this.setState({isMouseDown: true}); + this.changeValueByMouse(direction); + }; + + handleMouseUp = (e) => { + e.stopPropagation(); + e.preventDefault(); + this.setState({isMouseDown: false}); + }; + + changeValueByMouse (direction) { + setTimeout(() => { + const {value, units} = this.state; + let newValue = direction === 'up' ? (value + 1) : (value - 1); + this.setState({value: newValue}); + this.changeValue(newValue, units); + if (this.state.isMouseDown) { + this.changeValueByMouse(direction); + } + }, 200); + }; + + changeValue (newValue, newUnits, delay = false) { + const value = getValueOfProperty(newValue); + let valueObject = set({}, this.props.path, '' + value + newUnits); + if (delay) { + this.delayedChangeInputValue(valueObject); + } else { + this.props.onChangeValue(valueObject); + } + } + + render () { + const {isSet, isFavorite} = this.props; + const {label, units, value} = this.state; + return ( +
    +
    + +
    +

    + {isSet ? {label} : {label}} +

    +
    +
    + +
    +
    + {isSet && +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + +
    + +
      + {unitList.map((u, index) => { + return ( +
    • + + {u} + +
    • + ); + })} +
    +
    +
    +
    +
    +
    + } +
    + ); + } +} + +StyleSizeInput.propTypes = { + valueObject: PropTypes.any.isRequired, + path: PropTypes.string.isRequired, + isSet: PropTypes.bool.isRequired, + onSet: PropTypes.func.isRequired, + onChangeValue: PropTypes.func.isRequired, +}; + +StyleSizeInput.defaultProps = { + valueObject: null, + path: 'style.width', + isSet: false, + onSet: (path, checked) => { + console.log(path, checked); + }, + onChangeValue: (valueObject) => { + console.log(JSON.stringify(valueObject)); + }, +}; + +export default StyleSizeInput; \ No newline at end of file diff --git a/src-client/components/StyleSwatchesPicker.js b/src-client/components/StyleSwatchesPicker.js new file mode 100644 index 00000000..760bcc26 --- /dev/null +++ b/src-client/components/StyleSwatchesPicker.js @@ -0,0 +1,237 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isString, isObject, get, set, startCase } from 'lodash'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { SketchPicker } from 'react-color'; + +const getStateObject = (valueObject, path) => { + let value; + if (valueObject && isObject(valueObject)) { + value = get(valueObject, path); + } + let label = path.split('.'); + if (label && label.length > 1) { + label = startCase(label[1]); + } else { + label = path.replace('.', ' / '); + } + return { + valueObject: valueObject, + label: label, + value: value, + }; +}; + +const popover = { + position: 'absolute', + left: '-1em', + top: '0em', + height: '100px', + zIndex: 5, +}; + +const container = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + position: 'relative', + marginTop: '6px', + +}; + +const closeButton = { + position: 'absolute', + fontSize: '10px', + fontWeight: 200, + top: '-.7em', + left: '-.7em', + width: '2em', + height: '2em', + border: '1px solid #cdcdcd', + borderRadius: '50%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', + backgroundColor: '#fff', + zIndex: 6, +}; + +const rowContainerStyle = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', + position: 'relative', + width: '100%', + marginTop: '3px', +}; + +const checkBoxStyle = {margin: 0}; +const labelStyle = {margin: '0px 0px 0px 3px'}; + +class StyleSwatchesPicker extends Component { + + constructor (props) { + super(props); + this.state = getStateObject(props.valueObject, props.path); + // this.state = { + // colorValueString: this.props.colorValueString || '#fff', + // displayColorPicker: false + // }; + this.handleToggleOption = this.handleToggleOption.bind(this); + this.handleToggleFavorite = this.handleToggleFavorite.bind(this); + this.handleChangeComplete = this.handleChangeComplete.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleClick = this.handleClick.bind(this); + }; + + componentWillReceiveProps (nextProps) { + const {valueObject, path} = nextProps; + this.setState(getStateObject(valueObject, path)); + } + + handleClick (e) { + e.stopPropagation(); + e.preventDefault(); + this.setState({ + displayColorPicker: !this.state.displayColorPicker + }); + }; + + handleClose (e) { + e.stopPropagation(); + e.preventDefault(); + this.setState({ + displayColorPicker: false + }); + }; + + handleToggleOption (e) { + const {path, onSet} = this.props; + const checked = e.currentTarget.checked; + onSet(path, checked); + } + + handleToggleFavorite (e) { + e.stopPropagation(); + e.preventDefault(); + const {path, onToggleFavorite} = this.props; + onToggleFavorite(path); + } + + handleChangeComplete (color) { + let colorValueString; + const {hex, rgb} = color; + if (rgb) { + if (rgb.a === 1) { + colorValueString = hex; + } else { + colorValueString = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`; + } + } + let valueObject = set({}, this.props.path, colorValueString); + this.props.onChangeValue(valueObject); + }; + + render () { + const {isSet, isFavorite} = this.props; + const {displayColorPicker, label, value} = this.state; + return ( +
    +
    + +
    +

    + {isSet ? {label} : {label}} +

    +
    +
    + +
    +
    + {isSet && +
    +
    + {displayColorPicker && +
    +
    +
    + +
    +
    + +
    + } +
    + {value} +
    +
    + } +
    + ); + } +} + +StyleSwatchesPicker.propTypes = { + valueObject: PropTypes.any.isRequired, + path: PropTypes.string.isRequired, + isSet: PropTypes.bool.isRequired, + onSet: PropTypes.func.isRequired, + onChangeValue: PropTypes.func.isRequired, +}; + +StyleSwatchesPicker.defaultProps = { + valueObject: null, + path: 'style.color', + isSet: false, + onSet: (path, checked) => { + console.log(path, checked); + }, + onChangeValue: (valueObject) => { + console.log(JSON.stringify(valueObject)); + }, +}; + +export default StyleSwatchesPicker; diff --git a/src-client/components/index.js b/src-client/components/index.js new file mode 100644 index 00000000..6d444bbd --- /dev/null +++ b/src-client/components/index.js @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export CheckboxStateful from './CheckboxStateful'; +export ContentEditable from './ContentEditable'; +export GeneratorKeyTitleView from './GeneratorKeyTitleView'; +export InputTextStateful from './InputTextStateful'; +export MainFrame from './MainFrame'; +export AceEditor from './AceEditor'; +export CollapsiblePlusOptionInput from './CollapsiblePlusOptionInput'; +export DimensionBorder from './DimensionBorder'; +export DimensionBox from './DimensionBox'; +export DimensionContainer from './DimensionContainer'; +export DirPathInput from './DirPathInput'; +export InputComponentAutocomplete from './InputComponentAutocomplete'; +export MetaOptionInput from './MetaOptionInput'; +export MetaOptionsContainer from './MetaOptionsContainer'; +export MouseOverOverlay from './MouseMenuOverlay'; +export NamespaceCard from './NamespaceCard'; +export OptionInput from './OptionInput'; +export PageComponentForm from './PageComponentForm'; +export PageTreeViewItem from './PageTreeViewItem'; +export PageTreeViewItemText from './PageTreeViewItemText'; +export PageTreeViewPlaceholder from './PageTreeViewPlaceholder'; +export PlaceholderCircle from './PlaceholderCircle'; +export ProxyInput from './ProxyInput'; +export StyleNumberInput from './StyleNumberInput'; +export StyleOptionSelect from './StyleOptionSelect'; +export StyleSizeInput from './StyleSizeInput'; +export StyleSwatchesPicker from './StyleSwatchesPicker'; diff --git a/src-client/main.js b/src-client/main.js new file mode 100644 index 00000000..8662f7b0 --- /dev/null +++ b/src-client/main.js @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'babel-polyfill'; + +import 'assets/bootstrap/css/custom/bootstrap.css'; +import 'assets/bootstrap/js/bootstrap.js'; +import 'assets/font-awesome/css/font-awesome.css'; +import 'assets/umy-font/umy-font.css'; +import 'assets/app/css/umyproto.deskpage.css'; + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { createStore, applyMiddleware } from 'redux' +import createSagaMiddleware from 'redux-saga' +import { Provider } from 'react-redux'; +import thunk from 'redux-thunk'; + +import reducer from 'redux/reducer.js'; +import mainSaga from 'sagas/saga.js'; +import myMiddleware from 'redux/middleware.js'; +import { handleCompilerMessage } from 'modules/app/containers/AppContainer/actions.js'; + +import {MainFrame} from 'components/index'; + +const sagaMiddleware = createSagaMiddleware(); +const store = createStore(reducer, applyMiddleware(myMiddleware, sagaMiddleware, thunk)); +sagaMiddleware.run(mainSaga); +const { protocol, hostname, port } = window.location; +const socket = io.connect(protocol + '//' + hostname + ':' + port); +socket.on( 'invitation', message => console.log(message) ); +socket.on( 'compiler.message', stats => { + // console.log('compiler.message: ' + JSON.stringify(stats)); + store.dispatch(handleCompilerMessage(stats)); +}); + +//window.onbeforeunload = function(e) { +// store.dispatch(saveModel()); +//}; + +ReactDOM.render( + + + , + document.getElementById('content') +); diff --git a/src-client/modules/app/containers/AppContainer/actions.js b/src-client/modules/app/containers/AppContainer/actions.js new file mode 100644 index 00000000..7c9b6c97 --- /dev/null +++ b/src-client/modules/app/containers/AppContainer/actions.js @@ -0,0 +1,101 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; +import { coockiesApi } from 'api'; +import { failed } from 'modules/app/containers/AppMessage/actions'; +import { + setReloadPageRequest, + executeReloadPageRequest, + setEditModeOn, + setLivePreviewModeOn +} from 'modules/workspace/containers/DeskPage/actions'; +import { updateDeskLayout } from 'modules/workspace/containers/Desk/actions'; +import { loadComponents } from 'modules/workspace/containers/LibraryPanel/actions'; +import { updateOptionsPanelState } from 'modules/workspace/containers/ComponentOptionsPanel/actions'; +import { setShowButtons } from 'modules/workspace/containers/BlueprintControls/actions'; + +export const GET_PROJECT_STATUS = 'AppContainer/GET_PROJECT_STATUS'; +export const SET_PROJECT_INFO = 'AppContainer/SET_PROJECT_INFO'; +export const SET_PROJECT_PROXY_URL = 'AppContainer/SET_PROJECT_PROXY_URL'; +export const COMPILER_START = 'AppContainer/COMPILER_START'; +export const COMPILER_DONE = 'AppContainer/COMPILER_DONE'; +export const COMPILER_TIMEOUT = 'AppContainer/COMPILER_TIMEOUT'; + +export const SHOW_DESK = 'AppContainer/SHOW_DESK'; +export const SHOW_GENERATOR = 'AppContainer/SHOW_GENERATOR'; +export const HIDE_GENERATOR = 'AppContainer/HIDE_GENERATOR'; +export const SHOW_INSTALLER = 'AppContainer/SHOW_INSTALLER'; +export const HIDE_INSTALLER = 'AppContainer/HIDE_INSTALLER'; +export const SHOW_EXTRACTOR = 'AppContainer/SHOW_EXTRACTOR'; +export const HIDE_EXTRACTOR = 'AppContainer/HIDE_EXTRACTOR'; + +export const getProjectStatus = () => ({type: GET_PROJECT_STATUS}); +export const setProjectInfo = (info) => ({type: SET_PROJECT_INFO, payload: info}); +export const setProjectProxyURL = (proxyURL) => ({type: SET_PROJECT_PROXY_URL, payload: proxyURL}); +export const compilerStart = () => ({type: COMPILER_START}); +export const compilerDone = () => ({type: COMPILER_DONE}); +export const compilerTimeout = () => ({type: COMPILER_TIMEOUT}); + +export const showDesk = () => ({type: SHOW_DESK}); +export const showGenerator = () => ({type: SHOW_GENERATOR}); +export const hideGenerator = () => ({type: HIDE_GENERATOR}); +export const showInstaller = () => ({type: SHOW_INSTALLER}); +export const hideInstaller = () => ({type: HIDE_INSTALLER}); +export const showExtractor = () => ({type: SHOW_EXTRACTOR}); +export const hideExtractor = () => ({type: HIDE_EXTRACTOR}); + +export const handleCompilerMessage = (message) => (dispatch, getState) => { + if (message.status === 'start') { + dispatch(compilerStart()); + } else if (message.status === 'done') { + if (message.errors && message.errors.length > 0) { + message.errors.forEach(error => { + dispatch(failed(error.message ? error.message : error)); + }); + dispatch(setReloadPageRequest()); + } else { + dispatch(loadComponents()); + dispatch(executeReloadPageRequest()); + } + dispatch(compilerDone()); + } +}; + +export const setupDesk = () => (dispatch, getState) => { + const deskSettings = coockiesApi.getDeskSettings(); + if (deskSettings) { + const {isEditModeOn, layout, optionsPanel, showBlueprintButtons} = deskSettings; + if (isEditModeOn || isEditModeOn === undefined) { + dispatch(setEditModeOn()); + } else { + dispatch(setLivePreviewModeOn()); + } + if (layout) { + dispatch(updateDeskLayout(layout)); + } + if (optionsPanel) { + dispatch(updateOptionsPanelState(optionsPanel)); + } + if (showBlueprintButtons !== undefined) { + dispatch(setShowButtons(showBlueprintButtons)); + } + } +}; + +export const containerActions = (dispatch) => bindActionCreators({ + getProjectStatus +}, dispatch); \ No newline at end of file diff --git a/src-client/modules/app/containers/AppContainer/index.js b/src-client/modules/app/containers/AppContainer/index.js new file mode 100644 index 00000000..355a1c77 --- /dev/null +++ b/src-client/modules/app/containers/AppContainer/index.js @@ -0,0 +1,111 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +import { + Desk, + PageOptionsModal, + ComponentOptionsModal, + QuickAppendModal, + SaveDefaultModelModal, + PageExportModal +} from 'modules/workspace'; +import { Generator } from 'modules/generator'; +import { Installer, SelectDirectoryModal } from 'modules/installer'; +import { Extractor } from 'modules/extractor'; +import ProxySetupModal from 'modules/app/containers/ProxySetupModal'; +import ConfirmationModal from 'modules/app/containers/ConfirmationModal'; +import InformationModal from 'modules/app/containers/InformationModal'; + +class Container extends Component { + + constructor (props) { + super(props); + } + + componentDidMount () { + this.props.getProjectStatus(); + } + + render () { + + const {componentModel: {packageConfig, workspaceMode}} = this.props; + + let content = null; + if (workspaceMode === 'desk') { + content = ( +
    +
    + + + + + + + + + +
    +
    + ); + } else if (workspaceMode === 'generator') { + content = ( +
    + + + +
    + ); + } else if (workspaceMode === 'installer') { + content = ( +
    + + + + +
    + ); + } else if (workspaceMode === 'extractor') { + content = ( +
    + + + +
    + ); + } else { + content = ( +
    +
    +
    +
    +
    +
    +
    + ); + } + + return content; + } +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/app/containers/AppContainer/reducer.js b/src-client/modules/app/containers/AppContainer/reducer.js new file mode 100644 index 00000000..4f383b8c --- /dev/null +++ b/src-client/modules/app/containers/AppContainer/reducer.js @@ -0,0 +1,79 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + packageConfig: {}, + proxyURL: undefined, + workspaceMode: undefined, + paths: {}, +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + if(type === actions.SET_PROJECT_INFO){ + return Object.assign({}, state, { + paths: payload.project.paths, + packageConfig: payload.server.packageConf + }); + } + + if(type === actions.SET_PROJECT_PROXY_URL){ + return Object.assign({}, state, { + proxyURL: payload + }); + } + + if(type === actions.SHOW_DESK){ + return Object.assign({}, state, { + workspaceMode: 'desk' + }); + } + + if(type === actions.SHOW_GENERATOR){ + return Object.assign({}, state, { + workspaceMode: 'generator' + }); + } + + if(type === actions.SHOW_INSTALLER){ + return Object.assign({}, state, { + workspaceMode: 'installer' + }); + } + + if(type === actions.SHOW_EXTRACTOR){ + return Object.assign({}, state, { + workspaceMode: 'extractor' + }); + } + + if(type === actions.HIDE_GENERATOR + || type === actions.HIDE_INSTALLER + || type === actions.HIDE_EXTRACTOR){ + return Object.assign({}, state, { + workspaceMode: 'desk' + }); + } + + return state; + +} + + diff --git a/src-client/modules/app/containers/AppContainer/sagas.js b/src-client/modules/app/containers/AppContainer/sagas.js new file mode 100644 index 00000000..9e3480bc --- /dev/null +++ b/src-client/modules/app/containers/AppContainer/sagas.js @@ -0,0 +1,83 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SagaCancellationException } from 'redux-saga'; +import { fork, take, call, put, race, cancel } from 'redux-saga/effects'; +import * as actions from './actions.js'; +import * as spinnerActions from 'modules/app/containers/AppSpinner/actions'; +import * as messageActions from 'modules/app/containers/AppMessage/actions'; +import * as deskPageActions from 'modules/workspace/containers/DeskPage/actions'; +import * as libraryPanelActions from 'modules/workspace/containers/LibraryPanel/actions'; +import { serverApi } from 'api'; + +const delay = ms => new Promise(resolve => setTimeout(() => resolve('timed out'), ms)); + +function* delayForCompiler(){ + try{ + yield call(delay, 300000); + yield put(messageActions.timeout('The source code compiling is timed out. Look at console output or reload the browser page.')); + yield put(deskPageActions.setReloadPageRequest()); + yield put(actions.compilerTimeout()); + } catch(e) { + if (e instanceof SagaCancellationException) { + // do nothing + } + } +} + +function* waitForCompiler(){ + while(true){ + yield take(actions.COMPILER_START); + yield put(spinnerActions.started('Compiling the source code')); + const delayTask = yield fork(delayForCompiler); + yield take([actions.COMPILER_DONE, actions.COMPILER_TIMEOUT]); + yield cancel(delayTask); + yield put(spinnerActions.done('Compiling the source code')); + } +} + +function* getProjectStatus(){ + while(true){ + yield take(actions.GET_PROJECT_STATUS); + yield put(spinnerActions.started('Loading project')); + try{ + // yield call(signInByToken); + const status = yield call(serverApi.getProjectStatus); + if(status === 'ready-to-go'){ + const model = yield call(serverApi.getProjectModel); + yield put(libraryPanelActions.loadComponents()); + yield put(deskPageActions.loadModel(model || {})); + + const projectInfo = yield call(serverApi.getProjectInfo); + yield put(actions.setProjectInfo(projectInfo)); + + yield put(actions.showDesk()); + yield put(actions.setupDesk()); + } else { + yield put(messageActions.failed('Loading error of the current project')); + } + } catch(error) { + yield put(messageActions.failed('Obtaining project status error. ' + (error.message ? error.message : error))); + } + yield put(spinnerActions.done('Loading project')); + } +} + +// main saga +export default function* mainSaga() { + yield fork(getProjectStatus); + yield fork(waitForCompiler) +}; diff --git a/src-client/modules/app/containers/AppContainer/selectors.js b/src-client/modules/app/containers/AppContainer/selectors.js new file mode 100644 index 00000000..86596400 --- /dev/null +++ b/src-client/modules/app/containers/AppContainer/selectors.js @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createStructuredSelector } from 'reselect'; + +export const userAccountSelector = state => state.appContainer.userAccount; +export const projectPathsSelector = state => state.appContainer.paths; +export const versionSelector = state => state.appContainer.packageConfig.version; + +export const modelSelector = createStructuredSelector({ + componentModel: state => state.appContainer +}); + diff --git a/src-client/modules/app/containers/AppMessage/actions.js b/src-client/modules/app/containers/AppMessage/actions.js new file mode 100644 index 00000000..892aaf21 --- /dev/null +++ b/src-client/modules/app/containers/AppMessage/actions.js @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; +import {showModal} from 'modules/app/containers/InformationModal/actions'; + +let messageCounter = 0; + +export const SUCCESS = "AppMessage/SUCCESS"; +export const FAILED = "AppMessage/FAILED"; +export const TIMEOUT = "AppMessage/TIMEOUT"; + +export const CLOSE = "AppMessage/CLOSE"; + +export const success = (text, timeout = 3000) => ({ type: SUCCESS, payload: {text, timeout, timestamp: Date.now() + ++messageCounter} }); +export const failed = (text) => ({ type: FAILED, payload: {text, timestamp: Date.now() + ++messageCounter} }); +export const timeout = (text) => ({ type: TIMEOUT, payload: {text, timestamp: Date.now() + ++messageCounter} }); +export const close = (key) => ({ type: CLOSE, payload: key }); + +export const containerActions = (dispatch) => bindActionCreators({ + success, failed, timeout, close, showModal +}, dispatch); \ No newline at end of file diff --git a/src-client/modules/app/containers/AppMessage/index.js b/src-client/modules/app/containers/AppMessage/index.js new file mode 100644 index 00000000..ed841998 --- /dev/null +++ b/src-client/modules/app/containers/AppMessage/index.js @@ -0,0 +1,157 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import marked from 'marked'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions, FAILED, TIMEOUT } from './actions.js'; + +const style = { + position: 'relative', + color: '#ffffff', + borderRadius: '4px', + border: '1px solid #ffffff', + verticalAlign: 'middle', + textAlign: 'left', + padding: '1.5em', + marginBottom: '0.5em', + fontSize: '12px' +}; +const buttonStyle = { + position: 'absolute', + top: '0px', + right: '0px', + width: '1em', + height: '1em', + color: '#fff', + borderRadius: '50%', + verticalAlign: 'middle', + textAlign: 'center', + cursor: 'pointer', + fontSize: '18px' +}; + +const overlayStyle = { + position: 'fixed', + width: '30em', + padding: '.5em 1.5em .5em .5em', + right: '0px', + top: '0px', + zIndex: '9999' +}; + +class Container extends Component { + + constructor (props) { + super(props); + } + + componentDidMount () { + this._mountNode = document.createElement('div'); + this._mountNode.style['z-index'] = '9999'; + document.body.appendChild(this._mountNode); + ReactDOM.render(this._overlay, this._mountNode); + } + + componentWillUnmount () { + ReactDOM.unmountComponentAtNode(this._mountNode); + this._mountNode = null; + } + + componentDidUpdate () { + if (this._mountNode) { + ReactDOM.render(this._overlay, this._mountNode); + } + } + + handleShowDetails = (text) => (e) => { + e.preventDefault(); + e.stopPropagation(); + this.props.showModal(text); + }; + + render () { + const {componentModel: {messages}, close} = this.props; + if (messages.size > 0) { + let messagesItems = []; + messages.forEach((item, key) => { + let messageStyle = Object.assign({}, style); + if (item.type === FAILED || item.type === TIMEOUT) { + messageStyle.backgroundColor = '#C90008'; + } else { + messageStyle.backgroundColor = '#5cb85c'; + } + let messageText = item.text; + let textNeedCut = messageText && messageText.length > 300; + if (textNeedCut) { + messageText = messageText.substr(0, 300) + '...'; + } + messagesItems.push( +
    + {messageText && +
    + } + { textNeedCut ?

    + + [Read more] + +

    + : null + } + {close(key);}} + /> +
    + ); + }); + + this._overlay = ( +
    +
    + {messagesItems} +
    + +
    + ); + } else { + this._overlay = ( + + ); + } + return ( + + ); + } + +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/app/containers/AppMessage/reducer.js b/src-client/modules/app/containers/AppMessage/reducer.js new file mode 100644 index 00000000..ffa7c2c5 --- /dev/null +++ b/src-client/modules/app/containers/AppMessage/reducer.js @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + messages: new Map() +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + if( type === actions.SUCCESS || type === actions.FAILED || type === actions.TIMEOUT ) { + state = Object.assign({}, state); + state.messages.set(payload.timestamp, { + text: payload.text, + type: type + }); + return state; + } + if(type === actions.CLOSE) { + state = Object.assign({}, state); + state.messages.delete(payload); + return state; + } + + return state; +} + diff --git a/src-client/modules/app/containers/AppMessage/sagas.js b/src-client/modules/app/containers/AppMessage/sagas.js new file mode 100644 index 00000000..41ed2225 --- /dev/null +++ b/src-client/modules/app/containers/AppMessage/sagas.js @@ -0,0 +1,33 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fork, take, call, put } from 'redux-saga/effects'; +import * as actions from './actions.js'; + +const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); + +function* closeSuccessMessage(key, timeout){ + yield call(delay, timeout); + yield put(actions.close(key)); +} + +// main saga +export default function* mainSaga() { + while(true){ + const { payload } = yield take(actions.SUCCESS); + yield fork(closeSuccessMessage, payload.timestamp, payload.timeout); + } +}; diff --git a/src-client/modules/app/containers/AppMessage/selectors.js b/src-client/modules/app/containers/AppMessage/selectors.js new file mode 100644 index 00000000..26ae9713 --- /dev/null +++ b/src-client/modules/app/containers/AppMessage/selectors.js @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createStructuredSelector } from 'reselect'; + +export const modelSelector = createStructuredSelector({ + componentModel: state => state.appMessage +}); + diff --git a/src-client/modules/app/containers/AppSpinner/actions.js b/src-client/modules/app/containers/AppSpinner/actions.js new file mode 100644 index 00000000..4f2b3357 --- /dev/null +++ b/src-client/modules/app/containers/AppSpinner/actions.js @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; + +export const STARTED = "AppSpinner/STARTED"; +export const DONE = "AppSpinner/DONE"; + +export const started = (payload) => ({ type: STARTED, payload }); +export const done = (payload) => ({ type: DONE, payload }); + +export const containerActions = (dispatch) => bindActionCreators({ + started, done +}, dispatch); \ No newline at end of file diff --git a/src-client/modules/app/containers/AppSpinner/index.js b/src-client/modules/app/containers/AppSpinner/index.js new file mode 100644 index 00000000..cf4bc259 --- /dev/null +++ b/src-client/modules/app/containers/AppSpinner/index.js @@ -0,0 +1,88 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +class Container extends Component { + + constructor (props) { + super(props); + } + + componentDidMount () { + this._mountNode = document.createElement('div'); + this._mountNode.style['z-index'] = '9999'; + document.body.appendChild(this._mountNode); + ReactDOM.render(this._overlay, this._mountNode); + } + + componentWillUnmount () { + ReactDOM.unmountComponentAtNode(this._mountNode); + this._mountNode = null; + } + + componentDidUpdate () { + if (this._mountNode) { + ReactDOM.render(this._overlay, this._mountNode); + } + } + + render () { + const {componentModel: {tasks}} = this.props; + const tasksCount = tasks.size; + if (tasksCount > 0) { + + const itemStyle = { + backgroundColor: 'rgba(0, 0, 0, 0.6)', + color: '#ffffff', + borderRadius: '5px', + padding: '0.7em' + }; + let tasksList = []; + tasks.forEach((value, key) => { + tasksList.push( +
    + + {key} +
    + ); + }); + this._overlay = ( +
    +
    +
    + {tasksList} +
    +
    +
    + ); + } else { + this._overlay = ( + + ); + } + return ( + + ); + } + +} + +export default connect(modelSelector, containerActions)(Container); diff --git a/src-client/modules/app/containers/AppSpinner/reducer.js b/src-client/modules/app/containers/AppSpinner/reducer.js new file mode 100644 index 00000000..68537e3c --- /dev/null +++ b/src-client/modules/app/containers/AppSpinner/reducer.js @@ -0,0 +1,60 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + tasks: new Map() +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + if(type === actions.STARTED) { + let status = state.tasks.get(payload); + if(status){ + status.stage = 'started'; + status.counter += 1; + } else { + status = { + stage: 'started', + counter: 1 + } + } + state = Object.assign({}, state); + state.tasks.set(payload, status); + return state; + } + if(type === actions.DONE) { + let status = state.tasks.get(payload); + if(status) { + state = Object.assign({}, state); + if(status.counter > 1){ + status.counter -= 1; + state.tasks.set(payload, status); + } else { + state.tasks.delete(payload); + } + } else { + console.warn('AppSpinner: some process tried to call DONE action for ' + payload + ' which was not started before.'); + } + return state; + } + + return state; +} + diff --git a/src-client/modules/app/containers/AppSpinner/sagas.js b/src-client/modules/app/containers/AppSpinner/sagas.js new file mode 100644 index 00000000..5231030b --- /dev/null +++ b/src-client/modules/app/containers/AppSpinner/sagas.js @@ -0,0 +1,19 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// main saga +export default function* mainSaga() { +}; diff --git a/src-client/modules/app/containers/AppSpinner/selectors.js b/src-client/modules/app/containers/AppSpinner/selectors.js new file mode 100644 index 00000000..204e2860 --- /dev/null +++ b/src-client/modules/app/containers/AppSpinner/selectors.js @@ -0,0 +1,20 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createStructuredSelector } from 'reselect'; + +export const modelSelector = createStructuredSelector({ + componentModel: state => state.appSpinner +}); diff --git a/src-client/modules/app/containers/ConfirmationModal/actions.js b/src-client/modules/app/containers/ConfirmationModal/actions.js new file mode 100644 index 00000000..62f29615 --- /dev/null +++ b/src-client/modules/app/containers/ConfirmationModal/actions.js @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; + +export const HIDE_MODAL = "ConfirmationModal/HIDE_MODAL"; +export const SHOW_MODAL = "ConfirmationModal/SHOW_MODAL"; + +export const hideModal = () => ({type: HIDE_MODAL}); +export const showModal = (message, accept, cancel) => ({type: SHOW_MODAL, payload: {message, accept, cancel}}); + +export const containerActions = (dispatch) => bindActionCreators({ + hideModal +}, dispatch); diff --git a/src-client/modules/app/containers/ConfirmationModal/index.js b/src-client/modules/app/containers/ConfirmationModal/index.js new file mode 100644 index 00000000..29a4498b --- /dev/null +++ b/src-client/modules/app/containers/ConfirmationModal/index.js @@ -0,0 +1,94 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import marked from 'marked'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +import { Modal, Button } from 'react-bootstrap'; + +class Container extends Component { + + constructor (props) { + super(props); + this.handleCancel = this.handleCancel.bind(this); + this.handleAccept = this.handleAccept.bind(this); + } + + handleCancel (e) { + if (e) { + e.stopPropagation(); + e.preventDefault(); + } + const {componentModel: {cancel}, hideModal} = this.props; + if (cancel) { + cancel(); + } + hideModal(); + } + + handleAccept (e) { + if (e) { + e.stopPropagation(); + e.preventDefault(); + } + const {componentModel: {accept}, hideModal} = this.props; + if (accept) { + accept(); + } + hideModal(); + } + + render () { + const {componentModel: {show, message}} = this.props; + return ( + + + Confirm + + +
    + {message && +
    + } +
    + + + + + + + ); + } + +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/app/containers/ConfirmationModal/reducer.js b/src-client/modules/app/containers/ConfirmationModal/reducer.js new file mode 100644 index 00000000..ad8727d8 --- /dev/null +++ b/src-client/modules/app/containers/ConfirmationModal/reducer.js @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + show: false, + message: null, + accept: null, + cancel: null, +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + if(type === actions.HIDE_MODAL){ + return Object.assign({}, state, { + show: false, + message: null, + accept: null, + cancel: null + }); + } + + if(type === actions.SHOW_MODAL){ + return Object.assign({}, state, { + show: true, + message: payload.message, + accept: payload.accept, + cancel: payload.cancel, + }); + } + + return state; +} + diff --git a/src-client/modules/app/containers/ConfirmationModal/sagas.js b/src-client/modules/app/containers/ConfirmationModal/sagas.js new file mode 100644 index 00000000..5231030b --- /dev/null +++ b/src-client/modules/app/containers/ConfirmationModal/sagas.js @@ -0,0 +1,19 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// main saga +export default function* mainSaga() { +}; diff --git a/src-client/modules/app/containers/ConfirmationModal/selectors.js b/src-client/modules/app/containers/ConfirmationModal/selectors.js new file mode 100644 index 00000000..f692e17b --- /dev/null +++ b/src-client/modules/app/containers/ConfirmationModal/selectors.js @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; + +export const modelSelector = createStructuredSelector({ + componentModel: state => state.confirmationModal, +}); + diff --git a/src-client/modules/app/containers/InformationModal/actions.js b/src-client/modules/app/containers/InformationModal/actions.js new file mode 100644 index 00000000..a6a4008c --- /dev/null +++ b/src-client/modules/app/containers/InformationModal/actions.js @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; + +export const HIDE_MODAL = "InformationModal/HIDE_MODAL"; +export const SHOW_MODAL = "InformationModal/SHOW_MODAL"; + +export const hideModal = () => ({type: HIDE_MODAL}); +export const showModal = (message, close) => ({type: SHOW_MODAL, payload: {message, close}}); + +export const containerActions = (dispatch) => bindActionCreators({ + hideModal +}, dispatch); diff --git a/src-client/modules/app/containers/InformationModal/index.js b/src-client/modules/app/containers/InformationModal/index.js new file mode 100644 index 00000000..1e08d783 --- /dev/null +++ b/src-client/modules/app/containers/InformationModal/index.js @@ -0,0 +1,81 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import marked from 'marked'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +import { Modal, Button } from 'react-bootstrap'; +import { ProxyInput } from 'components'; + +class Container extends Component { + + constructor (props) { + super(props); + this.handleClose = this.handleClose.bind(this); + } + + handleClose (e) { + if (e) { + e.stopPropagation(); + e.preventDefault(); + } + const {componentModel: {close}, hideModal} = this.props; + if (close) { + close(); + } + hideModal(); + } + + render () { + const {componentModel: {show, message}} = this.props; + return ( + + + Info + + +
    + {message && +
    + } +
    +
    + + + +
    + ); + } + +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/app/containers/InformationModal/reducer.js b/src-client/modules/app/containers/InformationModal/reducer.js new file mode 100644 index 00000000..497f8197 --- /dev/null +++ b/src-client/modules/app/containers/InformationModal/reducer.js @@ -0,0 +1,47 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + show: false, + message: null, + close: null, +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + if(type === actions.HIDE_MODAL){ + return Object.assign({}, state, { + show: false, + message: null, + close: null + }); + } + + if(type === actions.SHOW_MODAL){ + return Object.assign({}, state, { + show: true, + message: payload.message, + close: payload.close, + }); + } + + return state; +} + diff --git a/src-client/modules/app/containers/InformationModal/sagas.js b/src-client/modules/app/containers/InformationModal/sagas.js new file mode 100644 index 00000000..5231030b --- /dev/null +++ b/src-client/modules/app/containers/InformationModal/sagas.js @@ -0,0 +1,19 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// main saga +export default function* mainSaga() { +}; diff --git a/src-client/modules/app/containers/InformationModal/selectors.js b/src-client/modules/app/containers/InformationModal/selectors.js new file mode 100644 index 00000000..5e7f619b --- /dev/null +++ b/src-client/modules/app/containers/InformationModal/selectors.js @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; + +export const modelSelector = createStructuredSelector({ + componentModel: state => state.informationModal, +}); + diff --git a/src-client/modules/app/containers/ProxySetupModal/actions.js b/src-client/modules/app/containers/ProxySetupModal/actions.js new file mode 100644 index 00000000..1b762f0d --- /dev/null +++ b/src-client/modules/app/containers/ProxySetupModal/actions.js @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; + +export const HIDE_MODAL = "ProxySetupModal/HIDE_MODAL"; +export const SHOW_MODAL = "ProxySetupModal/SHOW_MODAL"; +export const SAVE = "ProxySetupModal/SAVE"; + +export const hideModal = () => ({type: HIDE_MODAL}); +export const showModal = () => ({type: SHOW_MODAL}); +export const save = (proxyURL) => ({type: SAVE, payload: proxyURL}); + +export const containerActions = (dispatch) => bindActionCreators({ + hideModal, save +}, dispatch); diff --git a/src-client/modules/app/containers/ProxySetupModal/index.js b/src-client/modules/app/containers/ProxySetupModal/index.js new file mode 100644 index 00000000..10eb358f --- /dev/null +++ b/src-client/modules/app/containers/ProxySetupModal/index.js @@ -0,0 +1,85 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +import { Modal, Button } from 'react-bootstrap'; +import { ProxyInput } from 'components'; + +class Container extends Component { + + constructor(props) { + super(props); + this.handleClose = this.handleClose.bind(this); + this.handleSave = this.handleSave.bind(this); + } + + handleClose(e){ + e.stopPropagation(); + e.preventDefault(); + this.props.hideModal(); + } + + handleSave(e){ + e.stopPropagation(); + e.preventDefault(); + const { save } = this.props; + save(this.urlInputElement.getUrlValue() || null); + } + + render() { + const { componentModel, appContainerModel: {proxyURL}, hideModal } = this.props; + return ( + + + + Proxy URL settings + + +
    + this.urlInputElement = me} + urlValue={proxyURL} + /> +
    +
    + + + + +
    + ); + } + +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/app/containers/ProxySetupModal/reducer.js b/src-client/modules/app/containers/ProxySetupModal/reducer.js new file mode 100644 index 00000000..330937b1 --- /dev/null +++ b/src-client/modules/app/containers/ProxySetupModal/reducer.js @@ -0,0 +1,41 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + show: false +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + if(type === actions.HIDE_MODAL){ + return Object.assign({}, state, { + show: false + }); + } + + if(type === actions.SHOW_MODAL){ + return Object.assign({}, state, { + show: true + }); + } + + return state; +} + diff --git a/src-client/modules/app/containers/ProxySetupModal/sagas.js b/src-client/modules/app/containers/ProxySetupModal/sagas.js new file mode 100644 index 00000000..df7d06ef --- /dev/null +++ b/src-client/modules/app/containers/ProxySetupModal/sagas.js @@ -0,0 +1,44 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fork, take, call, put, race } from 'redux-saga/effects'; +import * as actions from './actions.js'; +import * as spinnerActions from 'modules/app/containers/AppSpinner/actions'; +import * as messageActions from 'modules/app/containers/AppMessage/actions'; +import { setProjectProxyURL } from 'modules/app/containers/AppContainer/actions'; +import { serverApi } from 'api'; + +const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); + +function* saveProxyURL(){ + while(true){ + const {payload} = yield take(actions.SAVE); + yield put(spinnerActions.started('Saving proxy URL')); + try { + yield call(serverApi.setProxyURL, payload); + yield put(setProjectProxyURL(payload)); + yield put(actions.hideModal()); + } catch(error) { + yield put(messageActions.failed('Proxy saving error. ' + (error.message ? error.message : error))); + } + yield put(spinnerActions.done('Saving proxy URL')); + } +} + +// main saga +export default function* mainSaga() { + yield fork(saveProxyURL); +}; diff --git a/src-client/modules/app/containers/ProxySetupModal/selectors.js b/src-client/modules/app/containers/ProxySetupModal/selectors.js new file mode 100644 index 00000000..ad4d1aae --- /dev/null +++ b/src-client/modules/app/containers/ProxySetupModal/selectors.js @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; + +export const modelSelector = createStructuredSelector({ + componentModel: state => state.proxySetupModal, + appContainerModel: state => state.appContainer +}); + diff --git a/src-client/modules/app/index.js b/src-client/modules/app/index.js new file mode 100644 index 00000000..5a6cec22 --- /dev/null +++ b/src-client/modules/app/index.js @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +export AppContainer from 'modules/app/containers/AppContainer'; +export AppMessage from 'modules/app/containers/AppMessage'; +export AppSpinner from 'modules/app/containers/AppSpinner'; +export ProxySetupModal from 'modules/app/containers/ProxySetupModal'; +export SignInModal from 'modules/app/containers/SignInModal'; diff --git a/src-client/modules/extractor/containers/ExtractList/actions.js b/src-client/modules/extractor/containers/ExtractList/actions.js new file mode 100644 index 00000000..438121b3 --- /dev/null +++ b/src-client/modules/extractor/containers/ExtractList/actions.js @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; +import { extract } from 'modules/extractor/containers/Extractor/actions'; + +export const containerActions = (dispatch) => bindActionCreators({ + extract +}, dispatch); diff --git a/src-client/modules/extractor/containers/ExtractList/index.js b/src-client/modules/extractor/containers/ExtractList/index.js new file mode 100644 index 00000000..5664b624 --- /dev/null +++ b/src-client/modules/extractor/containers/ExtractList/index.js @@ -0,0 +1,159 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { includes, findIndex, difference } from 'lodash'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +import { Grid, Row, Col } from 'react-bootstrap'; +import { DirPathInput } from 'components'; + +const labelContainerStyle = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + padding: '0.5em', +}; + +const firstCellStyle = { + whiteSpace: 'wrap', + flexGrow: 2, +}; + +const secondCellStyle = { + marginLeft: '0.5em', + flexGrow: 0, +}; + +class Container extends Component { + + constructor (props) { + super(props); + this.handleExtract = this.handleExtract.bind(this); + } + + handleExtract (e) { + e.stopPropagation(); + e.preventDefault(); + const {dependentNamespaces, dependencies, selectedPages, extract} = this.props; + extract(dependentNamespaces, dependencies, selectedPages, this.dirPathInput.getValue()); + } + + render () { + const {dependentNamespaces, dependencies, selectedNamespaces, recentDirPaths} = this.props; + let addedNamespaces = difference(dependentNamespaces, selectedNamespaces); + const {packages} = dependencies ? dependencies : null; + return ( + + + +
    + this.dirPathInput = me} + dirPath={recentDirPaths[0]} + label="The source code of the listed namespaces will be saved into the directory:" + isAutoComplete={true} + options={recentDirPaths} + onEnterKey={() => {}} + /> +
    + +
    + + +
    + +
    +
    Selected namespaces:
    +
    + {selectedNamespaces.map((item, index) => { + return ( +
    +
    + {item} +
    +
    + ); + })} +
    +
    + {addedNamespaces && addedNamespaces.length > 0 && +
    +
    Linked namespaces:
    +
    + {addedNamespaces.map((item, index) => { + return ( +
    +
    + {item} +
    +
    + ); + })} +
    +
    + } +
    +
    + +
    + + +
    +
    Node modules dependency:
    +
    + {packages && packages.length > 0 && packages.map((item, index) => { + return ( +
    +
    + {item.name} +
    +
    + {'Version: ' + item.version} +
    +
    + ); + })} + {(!packages || packages.length <= 0) && +

    None

    + } +
    +
    + +
    +
    + ); + } +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/extractor/containers/ExtractList/reducer.js b/src-client/modules/extractor/containers/ExtractList/reducer.js new file mode 100644 index 00000000..c4cb15b8 --- /dev/null +++ b/src-client/modules/extractor/containers/ExtractList/reducer.js @@ -0,0 +1,28 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + return state; +} + diff --git a/src-client/modules/extractor/containers/ExtractList/sagas.js b/src-client/modules/extractor/containers/ExtractList/sagas.js new file mode 100644 index 00000000..5231030b --- /dev/null +++ b/src-client/modules/extractor/containers/ExtractList/sagas.js @@ -0,0 +1,19 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// main saga +export default function* mainSaga() { +}; diff --git a/src-client/modules/extractor/containers/ExtractList/selectors.js b/src-client/modules/extractor/containers/ExtractList/selectors.js new file mode 100644 index 00000000..bf30f8ba --- /dev/null +++ b/src-client/modules/extractor/containers/ExtractList/selectors.js @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; +import { + selectedNamespacesSelector, + dependentNamespacesSelector, + dependenciesSelector, + recentDirPathsSelector, + selectedPagesSelector +} from 'modules/extractor/containers/Extractor/selectors'; + +export const modelSelector = createStructuredSelector({ + selectedNamespaces: selectedNamespacesSelector, + dependentNamespaces: dependentNamespacesSelector, + dependencies: dependenciesSelector, + recentDirPaths: recentDirPathsSelector, + selectedPages: selectedPagesSelector, +}); diff --git a/src-client/modules/extractor/containers/Extractor/actions.js b/src-client/modules/extractor/containers/Extractor/actions.js new file mode 100644 index 00000000..43a52a3a --- /dev/null +++ b/src-client/modules/extractor/containers/Extractor/actions.js @@ -0,0 +1,72 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {bindActionCreators} from 'redux'; +import {coockiesApi} from 'api'; +import {hideExtractor} from 'modules/app/containers/AppContainer/actions'; +import {failed} from 'modules/app/containers/AppMessage/actions'; + +export const STAGE1 = 'STAGE1'; +export const STAGE2 = 'STAGE2'; + +export const STEP_TO_STAGE = "Extractor/STEP_TO_STAGE"; +export const PREEXTRACT = "Extractor/PREEXTRACT"; +export const EXTRACT = "Extractor/EXTRACT"; + +export const SET_SELECTED_NAMESPACES = "Extractor/SET_SELECTED_NAMESPACES"; +export const SET_SELECTED_PAGES = "Extractor/SET_SELECTED_PAGES"; +export const SET_PREEXTRACTED_DATA = "Extractor/SET_PREEXTRACTED_DATA"; +export const CLEAR_DATA = "Extractor/CLEAR_DATA"; + +export const stepToStage = (stage) => ({type: STEP_TO_STAGE, payload: stage}); + +export const setSelectedNamespaces = (namespaces) => (dispatch, getState) => { + dispatch({type: SET_SELECTED_NAMESPACES, payload: namespaces}); +}; + +export const setSelectedPages = (pages) => (dispatch, getState) => { + dispatch({type: SET_SELECTED_PAGES, payload: pages}); +}; + +export const preExtract = (namespaces, pages) => (dispatch, getState) => { + dispatch({type: PREEXTRACT, payload: {namespaces, pages}}); +}; + +export const setPreExtractedData = (preExtractedData) => (dispatch, getState) => { + const recentDirPaths = coockiesApi.getRecentInstallerDirPaths(); + dispatch({type: SET_PREEXTRACTED_DATA, payload: Object.assign({}, preExtractedData, {recentDirPaths})}); +}; + +export const extract = (namespaces, dependencies, pages, dirPath) => (dispatch, getState) => { + if (!dirPath || dirPath.length <= 0) { + dispatch(failed('Enter the directory path where source code will be extracted.')); + } else { + coockiesApi.addToRecentInstallerDirPaths(dirPath); + dispatch({type: EXTRACT, payload: {namespaces, dependencies, pages, dirPath}}); + } +}; + +export const clearData = () => ({type: CLEAR_DATA}); + +export const hide = () => (dispatch, getState) => { + dispatch(hideExtractor()); + dispatch(clearData()); + dispatch(stepToStage(STAGE1)); +}; + +export const containerActions = (dispatch) => bindActionCreators({ + hide, stepToStage +}, dispatch); diff --git a/src-client/modules/extractor/containers/Extractor/index.js b/src-client/modules/extractor/containers/Extractor/index.js new file mode 100644 index 00000000..448143d9 --- /dev/null +++ b/src-client/modules/extractor/containers/Extractor/index.js @@ -0,0 +1,155 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions, STAGE1, STAGE2 } from './actions.js'; + +import { ButtonGroup, Button } from 'react-bootstrap'; + +import NamespaceList from 'modules/extractor/containers/NamespaceList'; +import ExtractList from 'modules/extractor/containers/ExtractList'; + +const toolbarLabelStyle = { + margin: '0 1em' +}; +const labelSectionStyle = { + width: '35%', + margin: '0 2em' +}; +const centerSectionStyle = { + width: '30%', + display: 'flex', + justifyContent: 'center' +}; +const toolbarSectionStyle = { + width: '100%', + display: 'flex', + justifyContent: 'center', + marginBottom: '1em' +}; + +const TITLE_STEP_1 = 'Select namespaces and pages to extract'; +const TITLE_STEP_2 = 'Review dependencies'; +const TITLE_STEP_3 = 'Extract namespaces'; + +class Container extends Component { + + constructor(props) { + super(props); + this.handleOnStep = this.handleOnStep.bind(this); + this.handleClose = this.handleClose.bind(this); + } + + componentDidMount() { + this.containerElement.scrollTop = 0; + } + + componentDidUpdate() { + this.containerElement.scrollTop = 0; + } + + handleOnStep(e) { + e.stopPropagation(); + e.preventDefault(); + const stage = e.currentTarget.dataset.stage; + if(stage){ + const {stepToStage} = this.props; + stepToStage(e.currentTarget.dataset.stage); + } + } + + handleClose(e) { + e.stopPropagation(); + e.preventDefault(); + this.props.hide(); + } + + render(){ + + const { stage } = this.props; + + const closeButton = ( + + ); + + let backStepLabel = null; + let nextStepLabel = null; + let toolbar = null; + let header = null; + let content = null; + if(stage === STAGE1){ + nextStepLabel = ( +
    {TITLE_STEP_2}
    + ); + toolbar = ( + + {closeButton} + + ); + header = (

    {TITLE_STEP_1}

    ); + content = (); + } else if(stage === STAGE2){ + backStepLabel = ( +
    {TITLE_STEP_1}
    + ); + nextStepLabel = ( +
    {TITLE_STEP_3}
    + ); + toolbar = ( + + + {closeButton} + + ); + header = (

    {TITLE_STEP_2}

    ); + content = (); + } + return ( +
    this.containerElement = me} + id="containerElement" + style={{position: 'absolute', top: '0px', left: '0px', right: '0px', bottom: '0px', overflow: 'auto'}} + > +
    +
    +
    +
    {backStepLabel}
    +
    {header}
    +
    {nextStepLabel}
    +
    +
    {toolbar}
    +
    +
    +
    + {content} +
    +
    + ); + } + +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/extractor/containers/Extractor/reducer.js b/src-client/modules/extractor/containers/Extractor/reducer.js new file mode 100644 index 00000000..1d71cefe --- /dev/null +++ b/src-client/modules/extractor/containers/Extractor/reducer.js @@ -0,0 +1,69 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + stage: actions.STAGE1, + selectedNamespaces: [], + selectedPages: [], + dependentNamespaces: [], + dependencies: {}, + recentDirPaths: [], +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + if (type === actions.STEP_TO_STAGE) { + return Object.assign({}, state, { + stage: payload + }); + } + + if (type === actions.SET_SELECTED_NAMESPACES) { + return Object.assign({}, state, { + selectedNamespaces: [].concat(payload), + }); + } + + if (type === actions.SET_SELECTED_PAGES) { + return Object.assign({}, state, { + selectedPages: [].concat(payload), + }); + } + + if (type === actions.SET_PREEXTRACTED_DATA) { + return Object.assign({}, state, { + dependentNamespaces: [].concat(payload.namespaces), + dependencies: Object.assign({}, payload.dependencies), + recentDirPaths: [].concat(payload.recentDirPaths), + }); + } + + if (type === actions.CLEAR_DATA) { + return Object.assign({}, state, { + dependentNamespaces: [], + dependencies: {}, + selectedNamespaces: [], + selectedPages: [], + }); + } + + return state; +}; + diff --git a/src-client/modules/extractor/containers/Extractor/sagas.js b/src-client/modules/extractor/containers/Extractor/sagas.js new file mode 100644 index 00000000..10d75f6c --- /dev/null +++ b/src-client/modules/extractor/containers/Extractor/sagas.js @@ -0,0 +1,79 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fork, take, call, put, cancel } from 'redux-saga/effects'; +import { SagaCancellationException } from 'redux-saga'; +import * as actions from './actions.js'; +import * as spinnerActions from 'modules/app/containers/AppSpinner/actions'; +import * as messageActions from 'modules/app/containers/AppMessage/actions'; +import * as generatorListActions from 'modules/generator/containers/GeneratorList/actions'; +import * as appContainerActions from 'modules/app/containers/AppContainer/actions'; +import * as deskPageActions from 'modules/workspace/containers/DeskPage/actions'; +import * as clipboardIndicatorActions from 'modules/workspace/containers/ClipboardIndicator/actions'; +import * as libraryPanelActions from 'modules/workspace/containers/LibraryPanel/actions'; +import * as selectionBreadcrumbsActions from 'modules/workspace/containers/SelectionBreadcrumbs/actions'; +import { serverApi, graphApi, coockiesApi } from 'api'; +import * as historyActions from 'modules/workspace/containers/HistoryControls/actions'; + +function* preExtract () { + while (true) { + const {payload: {namespaces, pages}} = yield take(actions.PREEXTRACT); + yield put(spinnerActions.started('Searching dependencies')); + try { + const preExtractedData = yield call(serverApi.preExtractNamespaces, namespaces, pages); + yield put(actions.setPreExtractedData(preExtractedData)); + yield put(actions.stepToStage(actions.STAGE2)); + } catch (error) { + yield put(messageActions.failed((error.message ? error.message : error))); + } + yield put(spinnerActions.done('Searching dependencies')); + } +} + +function* extract () { + while (true) { + const {payload: {namespaces, dependencies, pages, dirPath}} = yield take(actions.EXTRACT); + yield put(spinnerActions.started('Extracting the source code')); + try { + yield call( + serverApi.extractNamespaces, + namespaces, + dependencies, + pages, + dirPath + ); + yield put(actions.hide()); + yield put( + messageActions.success( + 'The source code has been extracted successfully. Find extracted namespaces in ' + dirPath + ) + ); + } catch (error) { + yield put( + messageActions.failed( + 'Extracting the source code. ' + (error.message ? error.message : error) + ) + ); + } + yield put(spinnerActions.done('Extracting the source code')); + } +} + +// main saga +export default function* mainSaga () { + yield fork(preExtract); + yield fork(extract); +}; diff --git a/src-client/modules/extractor/containers/Extractor/selectors.js b/src-client/modules/extractor/containers/Extractor/selectors.js new file mode 100644 index 00000000..b858d299 --- /dev/null +++ b/src-client/modules/extractor/containers/Extractor/selectors.js @@ -0,0 +1,28 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; +import { userAccountSelector } from 'modules/app/containers/AppContainer/selectors'; + +export const selectedNamespacesSelector = state => state.extractor.selectedNamespaces; +export const selectedPagesSelector = state => state.extractor.selectedPages; +export const dependentNamespacesSelector = state => state.extractor.dependentNamespaces; +export const dependenciesSelector = state => state.extractor.dependencies; +export const recentDirPathsSelector = state => state.extractor.recentDirPaths; + +export const modelSelector = createStructuredSelector({ + stage: state => state.extractor.stage +}); diff --git a/src-client/modules/extractor/containers/NamespaceList/actions.js b/src-client/modules/extractor/containers/NamespaceList/actions.js new file mode 100644 index 00000000..538ec19d --- /dev/null +++ b/src-client/modules/extractor/containers/NamespaceList/actions.js @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; +import { setSelectedNamespaces, setSelectedPages, preExtract } from 'modules/extractor/containers/Extractor/actions'; + +export const containerActions = (dispatch) => bindActionCreators({ + setSelectedNamespaces, setSelectedPages, preExtract +}, dispatch); diff --git a/src-client/modules/extractor/containers/NamespaceList/index.js b/src-client/modules/extractor/containers/NamespaceList/index.js new file mode 100644 index 00000000..6bbd075c --- /dev/null +++ b/src-client/modules/extractor/containers/NamespaceList/index.js @@ -0,0 +1,186 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { includes, findIndex } from 'lodash'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +import { Grid, Row, Col } from 'react-bootstrap'; +import { Panel, ListGroup, ListGroupItem } from 'react-bootstrap'; + +const labelContainerStyle = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +}; + +const labelStyle = { + margin: 0, + whiteSpace: 'wrap', + wordBreak: 'break-all' +}; + +const checkBoxLabelStyle = { + width: '1.5em', + minWidth: '1.5em', + flexGrow: 0, +}; + +let checkBoxStyle = { + margin: 0, +}; + +class Container extends Component { + + constructor (props) { + super(props); + this.handleSelectNamespace = this.handleSelectNamespace.bind(this); + this.handleSelectPage = this.handleSelectPage.bind(this); + this.handlePreExtract = this.handlePreExtract.bind(this); + } + + handleSelectNamespace (e) { + const namespace = e.currentTarget.dataset.namespace; + const selectedNamespaces = [].concat(this.props.selectedNamespaces); + const foundIndex = findIndex(selectedNamespaces, i => i === namespace); + if (foundIndex >= 0) { + selectedNamespaces.splice(foundIndex, 1); + } else { + selectedNamespaces.push(namespace); + } + this.props.setSelectedNamespaces(selectedNamespaces); + } + + handleSelectPage (e) { + const pagePath = e.currentTarget.dataset.pagepath; + const selectedPages = [].concat(this.props.selectedPages); + const foundIndex = findIndex(selectedPages, i => i === pagePath); + if (foundIndex >= 0) { + selectedPages.splice(foundIndex, 1); + } else { + selectedPages.push(pagePath); + } + this.props.setSelectedPages(selectedPages); + } + + handlePreExtract (e) { + e.stopPropagation(); + e.preventDefault(); + const {preExtract, selectedNamespaces, selectedPages} = this.props; + preExtract(selectedNamespaces, selectedPages); + } + + render () { + const {availableNamespaces, availablePages, selectedNamespaces, selectedPages} = this.props; + let namespacesItems = []; + let pagesItems = []; + if (availableNamespaces && availableNamespaces.length > 0) { + availableNamespaces.forEach((item, index) => { + namespacesItems.push( + +
    +
    + +
    +
    + {item} +
    +
    +
    + ); + }); + } + if (availablePages && availablePages.length > 0) { + availablePages.forEach((item, index) => { + pagesItems.push( + +
    +
    + +
    +
    + {item.pagePath} +
    +
    +
    + ); + }); + } + + return ( + + + + + + {namespacesItems} + + + + + + + {pagesItems} + + + + + + +
    + +
    + +
    +
    + ); + } +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/extractor/containers/NamespaceList/reducer.js b/src-client/modules/extractor/containers/NamespaceList/reducer.js new file mode 100644 index 00000000..c4cb15b8 --- /dev/null +++ b/src-client/modules/extractor/containers/NamespaceList/reducer.js @@ -0,0 +1,28 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + return state; +} + diff --git a/src-client/modules/extractor/containers/NamespaceList/sagas.js b/src-client/modules/extractor/containers/NamespaceList/sagas.js new file mode 100644 index 00000000..5231030b --- /dev/null +++ b/src-client/modules/extractor/containers/NamespaceList/sagas.js @@ -0,0 +1,19 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// main saga +export default function* mainSaga() { +}; diff --git a/src-client/modules/extractor/containers/NamespaceList/selectors.js b/src-client/modules/extractor/containers/NamespaceList/selectors.js new file mode 100644 index 00000000..4e053d5b --- /dev/null +++ b/src-client/modules/extractor/containers/NamespaceList/selectors.js @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; +import { availableNamespacesSelector } from 'modules/workspace/containers/LibraryPanel/selectors'; +import { pagesSelector } from 'modules/workspace/containers/DeskPage/selectors'; +import { selectedNamespacesSelector, selectedPagesSelector } from 'modules/extractor/containers/Extractor/selectors'; + +export const modelSelector = createStructuredSelector({ + availableNamespaces: availableNamespacesSelector, + availablePages: pagesSelector, + selectedNamespaces: selectedNamespacesSelector, + selectedPages: selectedPagesSelector, +}); diff --git a/src-client/modules/extractor/index.js b/src-client/modules/extractor/index.js new file mode 100644 index 00000000..58faca9d --- /dev/null +++ b/src-client/modules/extractor/index.js @@ -0,0 +1,19 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export Extractor from 'modules/extractor/containers/Extractor'; +export NamespaceList from 'modules/extractor/containers/NamespaceList'; +export ExtractList from 'modules/extractor/containers/ExtractList'; diff --git a/src-client/modules/generator/containers/ComponentNameForm/actions.js b/src-client/modules/generator/containers/ComponentNameForm/actions.js new file mode 100644 index 00000000..8eb68b56 --- /dev/null +++ b/src-client/modules/generator/containers/ComponentNameForm/actions.js @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; + +import { failed } from 'modules/app/containers/AppMessage/actions'; +import { pregenerate } from 'modules/generator/containers/Generator/actions'; + +export const containerActions = (dispatch) => bindActionCreators({ + pregenerate +}, dispatch); \ No newline at end of file diff --git a/src-client/modules/generator/containers/ComponentNameForm/index.js b/src-client/modules/generator/containers/ComponentNameForm/index.js new file mode 100644 index 00000000..dcb6fa58 --- /dev/null +++ b/src-client/modules/generator/containers/ComponentNameForm/index.js @@ -0,0 +1,173 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import validator from 'validator'; +import {isEmpty} from 'lodash'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +import { Grid, Row, Col, Button } from 'react-bootstrap'; +import { InputTextStateful } from 'components'; + +const cellBoxStyle = { + display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' +}; + +const validateName = (value) => { + return value + && value.length > 0 + && validator.isAlphanumeric(value); +}; + +const validateNonEmptyName = (value) => { + if (value && value.length > 0) { + return validator.isAlphanumeric(value); + } + return true; +}; + +class Container extends Component { + + constructor(props) { + super(props); + this.handleOnSubmit = this.handleOnSubmit.bind(this); + } + + handleOnSubmit(e) { + e.stopPropagation(); + e.preventDefault(); + const {generatorName, generatorDirPath, selectedComponentModel} = this.props; + const {namespaceInput, componentNameInput} = this; + const namespace = namespaceInput ? namespaceInput.getValue() : ''; + const componentName = componentNameInput.getValue(); + this.props.pregenerate( + generatorName, + generatorDirPath, + namespace, + componentName, + selectedComponentModel + ); + } + + render() { + const { + componentName, + namespace, + availableComponentNames, + availableNamespaces + } = this.props; + + let groupDataOptions = []; + if (availableNamespaces && availableNamespaces.length > 0) { + availableNamespaces.forEach((name, index) => { + groupDataOptions.push( + + ) + }); + } + let componentsDataOptions = []; + if (availableComponentNames && availableComponentNames.length > 0) { + availableComponentNames.forEach((name, index) => { + componentsDataOptions.push( + + ) + }); + } + let content = ( + + + +
    +
    +
    + + this.componentNameInput = me} + type="text" + list="components" + value={componentName} + autoComplete="on" + /> + + {componentsDataOptions} + + + + this.namespaceInput = me} + type="text" + list="groups" + value={namespace} + autoComplete="on" + /> + + {groupDataOptions} + +
    + +
    + +
    +
    + +
    +
    + ); + return ( +
    + {content} +
    + ); + } + +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/generator/containers/ComponentNameForm/reducer.js b/src-client/modules/generator/containers/ComponentNameForm/reducer.js new file mode 100644 index 00000000..4c8a58c4 --- /dev/null +++ b/src-client/modules/generator/containers/ComponentNameForm/reducer.js @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + return state; + +} + + diff --git a/src-client/modules/generator/containers/ComponentNameForm/sagas.js b/src-client/modules/generator/containers/ComponentNameForm/sagas.js new file mode 100644 index 00000000..c21070d5 --- /dev/null +++ b/src-client/modules/generator/containers/ComponentNameForm/sagas.js @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// main saga +export default function* mainSaga() { + //yield [fork(loadProject)]; + +}; diff --git a/src-client/modules/generator/containers/ComponentNameForm/selectors.js b/src-client/modules/generator/containers/ComponentNameForm/selectors.js new file mode 100644 index 00000000..62e51c9c --- /dev/null +++ b/src-client/modules/generator/containers/ComponentNameForm/selectors.js @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; +import { + availableComponentNamesSelector, + availableNamespacesSelector +} from 'modules/workspace/containers/LibraryPanel/selectors'; +import { + generatorNameSelector, + generatorDirPathSelector, + componentNameSelector, + namespaceSelector +} from 'modules/generator/containers/Generator/selectors'; + +import { + selectedComponentModelSelector +} from 'modules/workspace/containers/SelectionBreadcrumbs/selectors'; + +export const modelSelector = createStructuredSelector({ + componentModel: state => state.metadataForm, + availableComponentNames: availableComponentNamesSelector, + availableNamespaces: availableNamespacesSelector, + generatorName: generatorNameSelector, + generatorDirPath: generatorDirPathSelector, + selectedComponentModel: selectedComponentModelSelector, + componentName: componentNameSelector, + namespace: namespaceSelector, +}); + diff --git a/src-client/modules/generator/containers/Generator/actions.js b/src-client/modules/generator/containers/Generator/actions.js new file mode 100644 index 00000000..9cd241a3 --- /dev/null +++ b/src-client/modules/generator/containers/Generator/actions.js @@ -0,0 +1,146 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import validator from 'validator'; +import {bindActionCreators} from 'redux'; +import {hideGenerator} from 'modules/app/containers/AppContainer/actions'; +import {failed} from 'modules/app/containers/AppMessage/actions'; + +export const STAGE1 = 'STAGE1'; +export const STAGE2 = 'STAGE2'; +export const STAGE3 = 'STAGE3'; +export const STAGE4 = 'STAGE4'; + +export const STEP_TO_STAGE = "Generator/STEP_TO_STAGE"; +export const LOAD_GENERATORS = "Generator/LOAD_GENERATORS"; +export const SET_GENERATED_DATA = "Generator/SET_GENERATED_DATA"; +export const PREGENERATE = "Generator/PREGENERATE"; +export const GENERATE = "Generator/GENERATE"; +export const SAVE_GENERATED = "Generator/SAVE_GENERATED"; + +export const SET_SELECTED_GENERATOR = "Generator/SET_SELECTED_GENERATOR"; +export const SET_COMPONENT_METADATA = "Generator/SET_COMPONENT_METADATA"; +export const SET_COMPONENT_NAME = "Generator/SET_COMPONENT_NAME"; + +export const stepToStage = (stage) => ({type: STEP_TO_STAGE, payload: stage}); +export const setComponentMetadata = (metaData, metaHelp) => (dispatch, getState) => { + dispatch({type: SET_COMPONENT_METADATA, payload: {metaData, metaHelp}}); +}; +export const setGeneratedData = (generatedData) => ({type: SET_GENERATED_DATA, payload: generatedData}); + +// Following methods go in wizard steps order + +export const loadGenerators = (options) => (dispatch, getState) => { + dispatch({type: LOAD_GENERATORS, payload: options}); + dispatch({type: STEP_TO_STAGE, payload: STAGE1}); +}; + +export const startGeneratorWizard = (generatorName, generatorDirPath) => (dispatch, getState) => { + dispatch({type: SET_SELECTED_GENERATOR, payload: {generatorName, generatorDirPath}}); + dispatch({type: STEP_TO_STAGE, payload: STAGE2}); +}; + +export const pregenerate = (generatorName, + generatorDirPath, + namespace, + componentName, + modelNode) => (dispatch, getState) => { + let canProceed = true; + + let firstChar = componentName.charAt(0).toUpperCase(); + componentName = firstChar + componentName.substr(1); + + if (namespace && namespace.length > 0 && !validator.isAlphanumeric(namespace)) { + dispatch(failed('Please enter alphanumeric value for namespace')); + canProceed = false; + } + if (!componentName || componentName.length <= 0 || !validator.isAlphanumeric(componentName)) { + dispatch(failed('Please enter alphanumeric value for component name')); + canProceed = false; + } + if (componentName === 'Component') { + dispatch(failed('Component name can not be "Component"')); + canProceed = false; + } + if (canProceed) { + const {libraryPanel} = getState(); + const {componentTree} = libraryPanel; + + // console.log('Component tree: ', JSON.stringify(componentTree, null, 4)); + if (componentTree) { + if (namespace && namespace.length > 0) { + if (componentTree.modules) { + const module = componentTree.modules[namespace]; + if (module && module.components) { + const existingComponent = module.components[componentName]; + if (existingComponent) { + canProceed = confirm(`There is a component with the equal name in ${namespace}.\n\n ` + + `All reference to the component will be rewritten along with the source code files.\n`); + } + } + } + } else if (componentTree.components) { + const existingComponent = componentTree.components[componentName]; + if (existingComponent) { + canProceed = confirm(`There is a component with the equal name.\n\n ` + + `All reference to the component will be rewritten along with the source code files.\n`); + } + } + } + if (canProceed) { + dispatch({type: SET_COMPONENT_NAME, payload: {componentName, namespace}}); + dispatch( + { + type: PREGENERATE, + payload: {generatorName, generatorDirPath, namespace, componentName, modelNode} + } + ); + } + } + +}; + +export const generate = (generatorName, + generatorDirPath, + namespace, + componentName, + modelNode, + metaData) => (dispatch, getState) => { + dispatch(setComponentMetadata(metaData)); + dispatch( + { + type: GENERATE, + payload: {generatorName, generatorDirPath, namespace, componentName, modelNode, metaData} + } + ); +}; + +export const saveGenerated = (selectedKeys, files, dependencies, defaults) => (dispatch, getState) => { + const newModelNode = defaults && defaults.length > 0 ? defaults[0] : []; + dispatch({ + type: SAVE_GENERATED, + payload: {selectedKey: selectedKeys[0], newModelNode, files, dependencies} + }); +}; + +export const hide = () => (dispatch, getState) => { + dispatch(hideGenerator()); + dispatch(stepToStage(STAGE1)); +}; + +export const containerActions = (dispatch) => bindActionCreators({ + hide, stepToStage +}, dispatch); diff --git a/src-client/modules/generator/containers/Generator/index.js b/src-client/modules/generator/containers/Generator/index.js new file mode 100644 index 00000000..ac87e367 --- /dev/null +++ b/src-client/modules/generator/containers/Generator/index.js @@ -0,0 +1,206 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions, STAGE1, STAGE2, STAGE3, STAGE4 } from './actions.js'; + +import { ButtonGroup, Button } from 'react-bootstrap'; + +import GeneratorList from 'modules/generator/containers/GeneratorList'; +import MetadataForm from 'modules/generator/containers/MetadataForm'; +import ComponentNameForm from 'modules/generator/containers/ComponentNameForm'; +import SourceFilesList from 'modules/generator/containers/SourceFilesList'; + +const toolbarLabelStyle = { + margin: '0 1em' +}; +const labelSectionStyle = { + width: '35%', + margin: '0 2em' +}; +const centerSectionStyle = { + width: '30%', + display: 'flex', + justifyContent: 'center' +}; +const toolbarSectionStyle = { + width: '100%', + display: 'flex', + justifyContent: 'center', + marginBottom: '1em' +}; + +const TITLE_STEP_1 = 'Select component generator'; +const TITLE_STEP_2 = 'Enter component name'; +const TITLE_STEP_3 = 'Enter component metadata'; +const TITLE_STEP_4 = 'Preview generated source code'; +const TITLE_STEP_5 = 'Install generated source code'; + +class Container extends Component { + + constructor(props) { + super(props); + this.handleOnStep = this.handleOnStep.bind(this); + this.handleOnSignIn = this.handleOnSignIn.bind(this); + this.handleClose = this.handleClose.bind(this); + } + + componentDidMount() { + this.refs.containerElement.scrollTop = 0; + } + + componentDidUpdate() { + this.refs.containerElement.scrollTop = 0; + } + + handleOnStep(e) { + e.stopPropagation(); + e.preventDefault(); + const stage = e.currentTarget.dataset.stage; + if(stage){ + const {stepToStage} = this.props; + stepToStage(e.currentTarget.dataset.stage); + } + } + + handleOnSignIn(e) { + e.stopPropagation(); + e.preventDefault(); + this.props.showSignIn(); + } + + handleClose(e) { + e.stopPropagation(); + e.preventDefault(); + this.props.hide(); + } + + render(){ + + const { componentModel: {stage} } = this.props; + + const closeButton = ( + + ); + + let backStepLabel = null; + let nextStepLabel = null; + let toolbar = null; + let header = null; + let content = null; + if(stage === STAGE1){ + nextStepLabel = ( +
    {TITLE_STEP_2}
    + ); + toolbar = ( + + {closeButton} + + ); + header = (

    {TITLE_STEP_1}

    ); + content = (); + } else if(stage === STAGE2){ + backStepLabel = ( +
    {TITLE_STEP_1}
    + ); + nextStepLabel = ( +
    {TITLE_STEP_3}
    + ); + toolbar = ( + + + {closeButton} + + ); + header = (

    {TITLE_STEP_2}

    ); + content = (); + } else if(stage === STAGE3){ + backStepLabel = ( +
    {TITLE_STEP_2}
    + ); + nextStepLabel = ( +
    {TITLE_STEP_4}
    + ); + toolbar = ( + + + {closeButton} + + ); + header = (

    {TITLE_STEP_3}

    ); + content = (); + } else if(stage === STAGE4){ + backStepLabel = ( +
    {TITLE_STEP_3}
    + ); + nextStepLabel = ( +
    {TITLE_STEP_5}
    + ); + toolbar = ( + + + {closeButton} + + ); + header = (

    {TITLE_STEP_4}

    ); + content = (); + } + return ( +
    +
    +
    +
    +
    {backStepLabel}
    +
    {header}
    +
    {nextStepLabel}
    +
    +
    {toolbar}
    +
    +
    +
    + {content} +
    +
    + ); + } + +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/generator/containers/Generator/reducer.js b/src-client/modules/generator/containers/Generator/reducer.js new file mode 100644 index 00000000..d6ebf88e --- /dev/null +++ b/src-client/modules/generator/containers/Generator/reducer.js @@ -0,0 +1,72 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + generatedData: { + files: [], + dependencies: {}, + defaults: [], + }, + stage: actions.STAGE1, + generatorName: undefined, + generatorDirPath: undefined, + namespace: undefined, + componentName: undefined, + metaData: undefined, + metaHelp: undefined, +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + if(type === actions.STEP_TO_STAGE){ + return Object.assign({}, state, { + stage: payload + }); + } + + if(type === actions.SET_GENERATED_DATA){ + return Object.assign({}, state, { + generatedData: payload + }); + } + + + if(type === actions.SET_COMPONENT_METADATA){ + return Object.assign({}, state, { + metaData: payload.metaData || state.metaData, + metaHelp: payload.metaHelp || state.metaHelp, + }); + } + + if(type === actions.SET_COMPONENT_NAME){ + return Object.assign({}, state, { + namespace: payload.namespace, + componentName: payload.componentName, + }); + } + + if(type === actions.SET_SELECTED_GENERATOR){ + const {generatorName, generatorDirPath} = payload; + return Object.assign({}, state, {generatorName, generatorDirPath}); + } + + return state; +} + diff --git a/src-client/modules/generator/containers/Generator/sagas.js b/src-client/modules/generator/containers/Generator/sagas.js new file mode 100644 index 00000000..427dc188 --- /dev/null +++ b/src-client/modules/generator/containers/Generator/sagas.js @@ -0,0 +1,128 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fork, take, call, put, cancel } from 'redux-saga/effects'; +import { SagaCancellationException } from 'redux-saga'; +import * as actions from './actions.js'; +import * as spinnerActions from 'modules/app/containers/AppSpinner/actions'; +import * as messageActions from 'modules/app/containers/AppMessage/actions'; +import * as generatorListActions from 'modules/generator/containers/GeneratorList/actions'; +import * as appContainerActions from 'modules/app/containers/AppContainer/actions'; +import * as deskPageActions from 'modules/workspace/containers/DeskPage/actions'; +import * as clipboardIndicatorActions from 'modules/workspace/containers/ClipboardIndicator/actions'; +import * as libraryPanelActions from 'modules/workspace/containers/LibraryPanel/actions'; +import * as selectionBreadcrumbsActions from 'modules/workspace/containers/SelectionBreadcrumbs/actions'; +import { serverApi, graphApi, coockiesApi } from 'api'; +import * as historyActions from 'modules/workspace/containers/HistoryControls/actions'; + +function* pregenerate(){ + while(true){ + const {payload: { + generatorName, generatorDirPath, namespace, componentName, modelNode + }} = yield take(actions.PREGENERATE); + yield put(spinnerActions.started('Retrieving metadata')); + try { + const pregeneratedData = + yield call( + serverApi.pregenerate, + generatorName, + generatorDirPath, + namespace, + componentName, + modelNode + ); + yield put(actions.setComponentMetadata(pregeneratedData.metaData, pregeneratedData.metaHelp)); + yield put(actions.stepToStage(actions.STAGE3)); + let recentGenerators = coockiesApi.addToRecentGenerators(generatorName); + yield put(generatorListActions.setRecentGenerators(recentGenerators)); + } catch(error) { + yield put(messageActions.failed('Metadata retrieving has an error. ' + (error.message ? error.message : error))); + } + yield put(spinnerActions.done('Retrieving metadata')); + } +} + +function* generate(){ + while(true){ + const {payload: { + generatorName, generatorDirPath, namespace, componentName, modelNode, metaData + }} = yield take(actions.GENERATE); + yield put(spinnerActions.started('Generating the source code')); + try { + const generatedData = yield call( + serverApi.generate, + generatorName, + generatorDirPath, + namespace, + componentName, + modelNode, + metaData + ); + yield put(actions.setGeneratedData(generatedData)); + yield put(actions.stepToStage(actions.STAGE4)); + } catch(error) { + yield put(messageActions.failed('The source code generation has an error. ' + (error.message ? error.message : error))); + } + yield put(spinnerActions.done('Generating the source code')); + } +} + +function* saveGenerated(){ + while(true){ + const {payload: {selectedKey, newModelNode, files, dependencies}} = yield take(actions.SAVE_GENERATED); + yield put(spinnerActions.started('Installing & saving the source code')); + try { + yield call(serverApi.saveGenerated, files, dependencies); + graphApi.changeModelNodeType(selectedKey, newModelNode); + yield put(clipboardIndicatorActions.removeClipboardKeys()); + yield put(selectionBreadcrumbsActions.setSelectedKey(selectedKey)); + yield put(libraryPanelActions.loadComponents()); + yield put(deskPageActions.setReloadPageRequest()); + yield put(actions.hide()); + yield put(historyActions.pushHistory()); + } catch(error) { + yield put(messageActions.failed('Source code installation has an error. ' + (error.message ? error.message : error))); + } + yield put(spinnerActions.done('Installing & saving the source code')); + } +} + +function* loadGenerators(){ + while(true){ + yield take(actions.LOAD_GENERATORS); + yield put(spinnerActions.started('Loading generators')); + try { + let generatorsList = yield call(serverApi.getAvailableGeneratorsList); + const recentGenerators = coockiesApi.getRecentGenerators(); + yield put(generatorListActions.setGenerators(generatorsList)); + yield put(generatorListActions.setRecentGenerators(recentGenerators)); + yield put(appContainerActions.showGenerator()); + } catch(error) { + yield put(messageActions.failed('Generators loading has an error. ' + (error.message ? error.message : error))); + } + yield put(spinnerActions.done('Loading generators')); + } +} + +// main saga +export default function* mainSaga() { + yield fork(loadGenerators); + // yield fork(loadAllGenerators); + yield fork(pregenerate); + yield fork(generate); + // yield fork(remove); + yield fork(saveGenerated); +}; diff --git a/src-client/modules/generator/containers/Generator/selectors.js b/src-client/modules/generator/containers/Generator/selectors.js new file mode 100644 index 00000000..d070c026 --- /dev/null +++ b/src-client/modules/generator/containers/Generator/selectors.js @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; +import { userAccountSelector } from 'modules/app/containers/AppContainer/selectors'; + +export const generatorNameSelector = state => state.generator.generatorName; +export const generatorDirPathSelector = state => state.generator.generatorDirPath; +export const componentNameSelector = state => state.generator.componentName; +export const namespaceSelector = state => state.generator.namespace; +export const metaDataSelector = state => state.generator.metaData; +export const metaHelpSelector = state => state.generator.metaHelp; +export const generatedDataSelector = state => state.generator.generatedData; + +export const modelSelector = createStructuredSelector({ + componentModel: state => state.generator +}); diff --git a/src-client/modules/generator/containers/GeneratorBriefPanel/actions.js b/src-client/modules/generator/containers/GeneratorBriefPanel/actions.js new file mode 100644 index 00000000..72bce0cc --- /dev/null +++ b/src-client/modules/generator/containers/GeneratorBriefPanel/actions.js @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; + +import { startGeneratorWizard } from 'modules/generator/containers/Generator/actions'; +import { removeFromRecentGenerators } from 'modules/generator/containers/GeneratorList/actions'; + +export const containerActions = (dispatch) => bindActionCreators({ + startGeneratorWizard, removeFromRecentGenerators +}, dispatch); \ No newline at end of file diff --git a/src-client/modules/generator/containers/GeneratorBriefPanel/index.js b/src-client/modules/generator/containers/GeneratorBriefPanel/index.js new file mode 100644 index 00000000..2a973cc3 --- /dev/null +++ b/src-client/modules/generator/containers/GeneratorBriefPanel/index.js @@ -0,0 +1,169 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +import { Grid, Row, Col, Panel, Button } from 'react-bootstrap'; +import marked from 'marked'; + +const closeButtonStyle = { + position: 'absolute', + top: '-0.5em', + right: '-0.5em', + width: '1em', + height: '1em', + fontSize: '24px', + cursor: 'pointer', + zIndex: '100', + opacity: '0.5' +}; + +class Container extends Component { + + constructor (props) { + super(props); + this.handleOnSelect = this.handleOnSelect.bind(this); + this.handleExpand = this.handleExpand.bind(this); + this.state = { + isExpanded: false + }; + } + + shouldComponentUpdate (nextProps, nextState) { + return this.props.name === nextProps.name; + } + + handleOnSelect (e) { + e.preventDefault(); + e.stopPropagation(); + const {name, dirPath, startGeneratorWizard} = this.props; + startGeneratorWizard(name, dirPath); + } + + handleExpand (e) { + e.preventDefault(); + e.stopPropagation(); + const {isExpanded} = this.state; + if (isExpanded) { + const panelOffset = $(this.panel).offset(); + const $body = $('#containerElement'); + let diff = (panelOffset.top + $body.scrollTop()) - $body.offset().top; + let margin = 90; + $body.animate( + {scrollTop: (diff - margin)}, + 300 + ); + setTimeout(() => { + this.setState({isExpanded: !isExpanded}); + }, 300); + } else { + this.setState({isExpanded: !isExpanded}); + } + } + + render () { + const {isExpanded} = this.state; + const {name, readmeText, screenshotFilePath, isRecentPanel, removeFromRecentGenerators} = this.props; + + return ( +
    this.panel = me}> + + { isRecentPanel ? {removeFromRecentGenerators(name);}}/> + : null + } +
    +
    + +
    + {name} +
    + + + + + ); + } +} + +Container.defaultProps = { + name: '', + dirPath: undefined, + readmeFilePath: undefined, + screenshotFilePath: undefined, + readmeText: undefined, + isRecentPanel: false, +}; +Container.propTypes = { + name: PropTypes.string.isRequired, + dirPath: PropTypes.string.isRequired, + readmeFilePath: PropTypes.string.isRequired, + screenshotFilePath: PropTypes.string.isRequired, + readmeText: PropTypes.string.isRequired, + isRecentPanel: PropTypes.bool, +}; + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/generator/containers/GeneratorBriefPanel/reducer.js b/src-client/modules/generator/containers/GeneratorBriefPanel/reducer.js new file mode 100644 index 00000000..23641804 --- /dev/null +++ b/src-client/modules/generator/containers/GeneratorBriefPanel/reducer.js @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// import * as actions from './actions.js'; + +const initialState = { +}; + +export default (state = initialState, action = {}) => { + + // const {type, payload} = action; + + return state; + +} + + diff --git a/src-client/modules/generator/containers/GeneratorBriefPanel/sagas.js b/src-client/modules/generator/containers/GeneratorBriefPanel/sagas.js new file mode 100644 index 00000000..ce3a2221 --- /dev/null +++ b/src-client/modules/generator/containers/GeneratorBriefPanel/sagas.js @@ -0,0 +1,16 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + diff --git a/src-client/modules/generator/containers/GeneratorBriefPanel/selectors.js b/src-client/modules/generator/containers/GeneratorBriefPanel/selectors.js new file mode 100644 index 00000000..65c38d8a --- /dev/null +++ b/src-client/modules/generator/containers/GeneratorBriefPanel/selectors.js @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; + +export const modelSelector = createStructuredSelector({ + componentModel: state => state.generatorBriefPanel, +}); + diff --git a/src-client/modules/generator/containers/GeneratorList/actions.js b/src-client/modules/generator/containers/GeneratorList/actions.js new file mode 100644 index 00000000..0dc7ae8c --- /dev/null +++ b/src-client/modules/generator/containers/GeneratorList/actions.js @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; +import { coockiesApi } from 'api'; +import { loadGenerators } from 'modules/generator/containers/Generator/actions'; + +export const SET_GENERATORS = "GeneratorList/SET_GENERATORS"; +export const SET_RECENT_GENERATORS = "GeneratorList/SET_RECENT_GENERATORS"; +export const SET_SELECTED_TAB = "GeneratorList/SET_SELECTED_TAB"; + +export const setGenerators = (generators) => ({type: SET_GENERATORS, payload: {generators}}); + +export const setRecentGenerators = (recentGenerators) => ({type: SET_RECENT_GENERATORS, payload: recentGenerators}); + +export const setSelectedTab = (tabKey) => ({type: SET_SELECTED_TAB, payload: tabKey}); + +export const removeFromRecentGenerators = (generatorId) => (dispatch, getState) => { + let recentGenerators = coockiesApi.removeFromRecentGenerators(generatorId); + dispatch(setRecentGenerators(recentGenerators)); +}; + +export const containerActions = (dispatch) => bindActionCreators({ + setSelectedTab +}, dispatch); diff --git a/src-client/modules/generator/containers/GeneratorList/index.js b/src-client/modules/generator/containers/GeneratorList/index.js new file mode 100644 index 00000000..5cf38cdc --- /dev/null +++ b/src-client/modules/generator/containers/GeneratorList/index.js @@ -0,0 +1,123 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +import { Grid, Row, Col } from 'react-bootstrap'; +import { Tabs, Tab } from 'react-bootstrap'; +import { GeneratorKeyTitleView } from 'components'; +import GeneratorBriefPanel from 'modules/generator/containers/GeneratorBriefPanel'; + +class Container extends Component { + + constructor(props) { + super(props); + this.state = { + activePage: 1, + itemsPerPage: 7, + }; + this.handleTabSelect = this.handleTabSelect.bind(this); + } + + handleTabSelect(eventKey){ + if(eventKey){ + this.props.setSelectedTab(eventKey); + } + } + + render(){ + const { + componentModel: {generators, recentGenerators, selectedTabKey} + } = this.props; + + let generatorPanelList = []; + if (selectedTabKey === 1) { + if(generators && generators.length > 0) { + generators.forEach((item, index) => { + generatorPanelList.push( + + ); + }); + } + } else if (selectedTabKey === 2){ + if (generators && generators.length > 0 && recentGenerators && recentGenerators.length > 0) { + generators.forEach((item, index) => { + if (recentGenerators.indexOf(item.name) >= 0) { + generatorPanelList.push( + + ); + } + }); + } + } + + return ( + + + + + +
    + {generatorPanelList} +
    +
    + +
    + {generatorPanelList} +
    +
    +
    + +
    +
    + ); + } +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/generator/containers/GeneratorList/reducer.js b/src-client/modules/generator/containers/GeneratorList/reducer.js new file mode 100644 index 00000000..a5c6a4b5 --- /dev/null +++ b/src-client/modules/generator/containers/GeneratorList/reducer.js @@ -0,0 +1,49 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + generators: [], + recentGenerators: [], + selectedTabKey: 1, +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + if(type === actions.SET_GENERATORS){ + return Object.assign({}, state, { + generators: payload.generators || [], + }); + } + + if(type === actions.SET_RECENT_GENERATORS){ + return Object.assign({}, state, { + recentGenerators: payload || [] + }); + } + + if(type === actions.SET_SELECTED_TAB){ + return Object.assign({}, state, { + selectedTabKey: payload + }); + } + + return state; +} + diff --git a/src-client/modules/generator/containers/GeneratorList/sagas.js b/src-client/modules/generator/containers/GeneratorList/sagas.js new file mode 100644 index 00000000..727cb0f0 --- /dev/null +++ b/src-client/modules/generator/containers/GeneratorList/sagas.js @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//import { fork, take, call, put, cancel } from 'redux-saga/effects'; +//import { SagaCancellationException } from 'redux-saga'; + +// main saga +export default function* mainSaga() { +}; diff --git a/src-client/modules/generator/containers/GeneratorList/selectors.js b/src-client/modules/generator/containers/GeneratorList/selectors.js new file mode 100644 index 00000000..bc015684 --- /dev/null +++ b/src-client/modules/generator/containers/GeneratorList/selectors.js @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; + +export const modelSelector = createStructuredSelector({ + componentModel: state => state.generatorList, +}); diff --git a/src-client/modules/generator/containers/MetadataForm/actions.js b/src-client/modules/generator/containers/MetadataForm/actions.js new file mode 100644 index 00000000..748df17c --- /dev/null +++ b/src-client/modules/generator/containers/MetadataForm/actions.js @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; + +import { generate } from 'modules/generator/containers/Generator/actions'; + +export const containerActions = (dispatch) => bindActionCreators({ + generate +}, dispatch); \ No newline at end of file diff --git a/src-client/modules/generator/containers/MetadataForm/index.js b/src-client/modules/generator/containers/MetadataForm/index.js new file mode 100644 index 00000000..49ce6b0c --- /dev/null +++ b/src-client/modules/generator/containers/MetadataForm/index.js @@ -0,0 +1,126 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {isEmpty} from 'lodash'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +import { Grid, Row, Col, Button } from 'react-bootstrap'; +import { MetaOptionsContainer } from 'components'; + +const cellBoxStyle = { + display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' +}; + +class Container extends Component { + + constructor(props) { + super(props); + this.state = { + enableNamespaceInput: false, + }; + this.handleOnSubmit = this.handleOnSubmit.bind(this); + this.handleNamespaceCheck = this.handleNamespaceCheck.bind(this); + } + + handleOnSubmit(e) { + e.stopPropagation(); + e.preventDefault(); + let metadataObject = undefined; + const { + metaData, + generatorName, + generatorDirPath, + selectedComponentModel, + componentName, + namespace + } = this.props; + const {metadataOptions} = this; + if(metadataOptions) { + metadataObject = Object.assign({}, metaData, metadataOptions.getOptionsObject()); + } + this.props.generate( + generatorName, + generatorDirPath, + namespace, + componentName, + selectedComponentModel, + metadataObject + ); + } + + handleNamespaceCheck(e) { + this.setState({enableNamespaceInput: this.namespaceCheckbox.checked}); + } + + render() { + const { + metaData, metaHelp, + } = this.props; + + let content = ( + + + +
    +
    +
    + {!isEmpty(metaData) && +
    + this.metadataOptions = me} + optionsObject={metaData} + optionsHelpObject={metaHelp} + /> +
    + } +
    + +
    +
    +
    +
    + +
    +
    + ); + return ( +
    + {content} +
    + ); + } + +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/generator/containers/MetadataForm/reducer.js b/src-client/modules/generator/containers/MetadataForm/reducer.js new file mode 100644 index 00000000..4c8a58c4 --- /dev/null +++ b/src-client/modules/generator/containers/MetadataForm/reducer.js @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + return state; + +} + + diff --git a/src-client/modules/generator/containers/MetadataForm/sagas.js b/src-client/modules/generator/containers/MetadataForm/sagas.js new file mode 100644 index 00000000..cda2d005 --- /dev/null +++ b/src-client/modules/generator/containers/MetadataForm/sagas.js @@ -0,0 +1,106 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//import validator from 'validator'; +//import { SagaCancellationException } from 'redux-saga'; +//import { fork, take, call, put, race } from 'redux-saga/effects'; +//import * as actions from './actions.js'; +//import * as spinnerActions from '../AppSpinner/actions.js'; +//import * as messageActions from '../AppMessage/actions.js'; +////import * as deskActions from '../../workspace/Desk/actions.js'; +//import * as deskPageActions from '../../workspace/DeskPage/actions.js'; +//import { loadComponents } from '../../workspace/LibraryPanel/actions.js'; +//import { serverApi, cookies } from '../../../api'; +// +//const delay = ms => new Promise(resolve => setTimeout(() => resolve('timed out'), ms)); +// +//function* signInByToken(){ +// let tokenFromCookies = cookies.getItem("structor-market-token"); +// if (tokenFromCookies) { +// try{ +// const userCredentials = yield call(serverApi.initUserCredentialsByToken, tokenFromCookies); +// yield put(actions.signInDone(userCredentials)); +// } catch(e){ +// console.warn(e.message ? e.message : e.toString()); +// } +// } +//} +// +//function* signIn(){ +// while(true){ +// const {payload: {email, password, staySignedIn}} = yield take(actions.SIGN_IN); +// if(!email || email.length <= 0){ +// put(actions.signInFailed('Please enter e-mail address value')); +// } else if( !validator.isEmail(email) ){ +// put(actions.signInFailed('Please enter valid e-mail address value')); +// } else { +// yield put(spinnerActions.started(actions.SIGN_IN)); +// try{ +// const response = yield call(serverApi.initUserCredentials, email, password); +// if(staySignedIn === true){ +// docCookies.setItem("structor-market-token", response.token, 31536e3, "/"); +// } +// yield put(actions.signInDone(response)) +// } catch(e){ +// yield put(actions.signInFailed(e)); +// } +// yield put(spinnerActions.done(actions.SIGN_IN)); +// } +// } +//} +// +//function* loadProjectInfo(){ +// try{ +// return yield call(serverApi.getProjectInfo); +// } catch(error){ +// if(error instanceof SagaCancellationException){ +// yield put(messageActions.failed('Project initialization canceled.')); +// } else { +// yield put(messageActions.failed('Project initialization error. ' + (error.message ? error.message : error))); +// } +// } +//} +// +//function* loadProject(){ +// yield take(actions.GET_PROJECT_INFO); +// try { +// yield put(spinnerActions.started('Loading project')); +// yield call(signInByToken); +// const {timeout, response} = yield race({ +// response: call(loadProjectInfo), +// timeout: call(delay, 30000) +// }); +// if(response){ +// const {projectData: {model}, packageConfig, projectDirectoryStatus} = response; +// +// yield put(deskPageActions.loadModel(model)); +// yield put(actions.getProjectInfoDone({packageConfig, projectDirectoryStatus})); +// yield put(loadComponents()); +// +// } else if(timeout) { +// yield put(messageActions.timeout('Project initialization timeout.')); +// } +// } catch(error) { +// yield put(messageActions.failed('Project loading error. ' + (error.message ? error.message : error))); +// } +// yield put(spinnerActions.done('Loading project')); +//} + +// main saga +export default function* mainSaga() { + //yield [fork(loadProject)]; + +}; diff --git a/src-client/modules/generator/containers/MetadataForm/selectors.js b/src-client/modules/generator/containers/MetadataForm/selectors.js new file mode 100644 index 00000000..787b7ccd --- /dev/null +++ b/src-client/modules/generator/containers/MetadataForm/selectors.js @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; +import { + metaDataSelector, + metaHelpSelector, + generatorNameSelector, + generatorDirPathSelector, + componentNameSelector, + namespaceSelector +} from 'modules/generator/containers/Generator/selectors'; +import { + selectedComponentModelSelector +} from 'modules/workspace/containers/SelectionBreadcrumbs/selectors'; + +export const modelSelector = createStructuredSelector({ + metaData: metaDataSelector, + metaHelp: metaHelpSelector, + generatorName: generatorNameSelector, + generatorDirPath: generatorDirPathSelector, + selectedComponentModel: selectedComponentModelSelector, + componentName: componentNameSelector, + namespace: namespaceSelector, +}); + diff --git a/src-client/modules/generator/containers/SourceFilesList/actions.js b/src-client/modules/generator/containers/SourceFilesList/actions.js new file mode 100644 index 00000000..a88de14c --- /dev/null +++ b/src-client/modules/generator/containers/SourceFilesList/actions.js @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; + +import { saveGenerated } from 'modules/generator/containers/Generator/actions'; + +export const containerActions = (dispatch) => bindActionCreators({ + saveGenerated +}, dispatch); \ No newline at end of file diff --git a/src-client/modules/generator/containers/SourceFilesList/index.js b/src-client/modules/generator/containers/SourceFilesList/index.js new file mode 100644 index 00000000..836c8477 --- /dev/null +++ b/src-client/modules/generator/containers/SourceFilesList/index.js @@ -0,0 +1,231 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {isEmpty} from 'lodash'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +import { Grid, Row, Col, Button, ListGroup, ListGroupItem } from 'react-bootstrap'; + +class Container extends Component { + + constructor(props) { + super(props); + this.state = { + activeFile: undefined + }; + this.handleSubmit = this.handleSubmit.bind(this); + this.handleChangePreview = this.handleChangePreview.bind(this); + } + + componentDidMount(){ + $(this.refs.sourceCodePane).children('pre').each((i, block) => { + hljs.highlightBlock(block); + }); + } + + componentDidUpdate(){ + $(this.refs.sourceCodePane).children('pre').each((i, block) => { + hljs.highlightBlock(block); + }); + } + + handleSubmit(e) { + e.stopPropagation(); + e.preventDefault(); + const {selectedKeys, generatedData, saveGenerated } = this.props; + const {files, dependencies, defaults} = generatedData; + saveGenerated(selectedKeys, files, dependencies, defaults); + } + + handleChangePreview(e){ + e.stopPropagation(); + e.preventDefault(); + this.setState({ + activeFile: e.currentTarget.dataset.filename + }); + const $body = $('#containerElement'); + $body.animate( + { scrollTop: 0 }, + 300 + ); + } + + render() { + + const { generatedData } = this.props; + let { activeFile } = this.state; + const {files, dependencies } = generatedData; + + const cellBoxStyle = { + display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' + }; + + let previewSourceCode; + let previewFilePath; + + let fileItemsList = []; + let fileStructorItemsList = []; + if (files && files.length > 0) { + activeFile = activeFile || files[0].outputFilePath; + let active; + let filePathParts; + let itemLabel; + let itemElement; + files.forEach((file, index) => { + active = file.outputFilePath === activeFile; + filePathParts = file.outputFilePath.split('/'); + if (filePathParts.length > 2) { + itemLabel = `${filePathParts[filePathParts.length - 2]}/${filePathParts[filePathParts.length - 1]}`; + } else { + itemLabel = file.outputFileName; + } + itemElement = ( + + {itemLabel} + { active ? null : + + + + } + + ); + if (file.outputFilePath.indexOf('.structor') >= 0) { + fileStructorItemsList.push(itemElement); + } else { + fileItemsList.push(itemElement); + } + if(active){ + previewSourceCode = file.sourceCode; + previewFilePath = file.outputFilePath; + } + }); + } + let dependenciesItem; + if(dependencies && !isEmpty(dependencies)){ + dependenciesItem = ( + + Dependencies + { 'dependencies' === activeFile ? null : + + + + } + + ); + if('dependencies' === activeFile){ + previewSourceCode = JSON.stringify(dependencies || {}, null, 4); + previewFilePath = 'None'; + } + } + return ( +
    + + + +
    Output file path:  {previewFilePath}
    + +
    + + + +
    +
    +

    Component files

    + + {fileItemsList} + + { dependenciesItem && +
    +

    npm & resources dependencies

    + + {dependenciesItem} + +
    + } +

    Structor specific files

    + + {fileStructorItemsList} + +
    + +
    +
    +
    + + + + +
    +
    +

    Source code preview

    +
    +
    
    +                                            {previewSourceCode}
    +                                        
    +
    +
    +
    + +
    +
    +
    + ); + } + +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/generator/containers/SourceFilesList/reducer.js b/src-client/modules/generator/containers/SourceFilesList/reducer.js new file mode 100644 index 00000000..83c46ec0 --- /dev/null +++ b/src-client/modules/generator/containers/SourceFilesList/reducer.js @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + return state; + +} + + diff --git a/src-client/modules/generator/containers/SourceFilesList/sagas.js b/src-client/modules/generator/containers/SourceFilesList/sagas.js new file mode 100644 index 00000000..cda2d005 --- /dev/null +++ b/src-client/modules/generator/containers/SourceFilesList/sagas.js @@ -0,0 +1,106 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//import validator from 'validator'; +//import { SagaCancellationException } from 'redux-saga'; +//import { fork, take, call, put, race } from 'redux-saga/effects'; +//import * as actions from './actions.js'; +//import * as spinnerActions from '../AppSpinner/actions.js'; +//import * as messageActions from '../AppMessage/actions.js'; +////import * as deskActions from '../../workspace/Desk/actions.js'; +//import * as deskPageActions from '../../workspace/DeskPage/actions.js'; +//import { loadComponents } from '../../workspace/LibraryPanel/actions.js'; +//import { serverApi, cookies } from '../../../api'; +// +//const delay = ms => new Promise(resolve => setTimeout(() => resolve('timed out'), ms)); +// +//function* signInByToken(){ +// let tokenFromCookies = cookies.getItem("structor-market-token"); +// if (tokenFromCookies) { +// try{ +// const userCredentials = yield call(serverApi.initUserCredentialsByToken, tokenFromCookies); +// yield put(actions.signInDone(userCredentials)); +// } catch(e){ +// console.warn(e.message ? e.message : e.toString()); +// } +// } +//} +// +//function* signIn(){ +// while(true){ +// const {payload: {email, password, staySignedIn}} = yield take(actions.SIGN_IN); +// if(!email || email.length <= 0){ +// put(actions.signInFailed('Please enter e-mail address value')); +// } else if( !validator.isEmail(email) ){ +// put(actions.signInFailed('Please enter valid e-mail address value')); +// } else { +// yield put(spinnerActions.started(actions.SIGN_IN)); +// try{ +// const response = yield call(serverApi.initUserCredentials, email, password); +// if(staySignedIn === true){ +// docCookies.setItem("structor-market-token", response.token, 31536e3, "/"); +// } +// yield put(actions.signInDone(response)) +// } catch(e){ +// yield put(actions.signInFailed(e)); +// } +// yield put(spinnerActions.done(actions.SIGN_IN)); +// } +// } +//} +// +//function* loadProjectInfo(){ +// try{ +// return yield call(serverApi.getProjectInfo); +// } catch(error){ +// if(error instanceof SagaCancellationException){ +// yield put(messageActions.failed('Project initialization canceled.')); +// } else { +// yield put(messageActions.failed('Project initialization error. ' + (error.message ? error.message : error))); +// } +// } +//} +// +//function* loadProject(){ +// yield take(actions.GET_PROJECT_INFO); +// try { +// yield put(spinnerActions.started('Loading project')); +// yield call(signInByToken); +// const {timeout, response} = yield race({ +// response: call(loadProjectInfo), +// timeout: call(delay, 30000) +// }); +// if(response){ +// const {projectData: {model}, packageConfig, projectDirectoryStatus} = response; +// +// yield put(deskPageActions.loadModel(model)); +// yield put(actions.getProjectInfoDone({packageConfig, projectDirectoryStatus})); +// yield put(loadComponents()); +// +// } else if(timeout) { +// yield put(messageActions.timeout('Project initialization timeout.')); +// } +// } catch(error) { +// yield put(messageActions.failed('Project loading error. ' + (error.message ? error.message : error))); +// } +// yield put(spinnerActions.done('Loading project')); +//} + +// main saga +export default function* mainSaga() { + //yield [fork(loadProject)]; + +}; diff --git a/src-client/modules/generator/containers/SourceFilesList/selectors.js b/src-client/modules/generator/containers/SourceFilesList/selectors.js new file mode 100644 index 00000000..b644f28a --- /dev/null +++ b/src-client/modules/generator/containers/SourceFilesList/selectors.js @@ -0,0 +1,26 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; + +import {selectedKeysSelector} from 'modules/workspace/containers/SelectionBreadcrumbs/selectors'; +import {generatedDataSelector} from 'modules/generator/containers/Generator/selectors'; + +export const modelSelector = createStructuredSelector({ + selectedKeys: selectedKeysSelector, + generatedData: generatedDataSelector, +}); + diff --git a/src-client/modules/generator/index.js b/src-client/modules/generator/index.js new file mode 100644 index 00000000..835e6403 --- /dev/null +++ b/src-client/modules/generator/index.js @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export ComponentNameForm from 'modules/generator/containers/ComponentNameForm'; +export Generator from 'modules/generator/containers/Generator'; +export GeneratorBriefPanel from 'modules/generator/containers/GeneratorBriefPanel'; +export GeneratorList from 'modules/generator/containers/GeneratorList'; +export MetadataForm from 'modules/generator/containers/MetadataForm'; +export SourceFilesList from 'modules/generator/containers/SourceFilesList'; diff --git a/src-client/modules/installer/containers/Installer/actions.js b/src-client/modules/installer/containers/Installer/actions.js new file mode 100644 index 00000000..dde69615 --- /dev/null +++ b/src-client/modules/installer/containers/Installer/actions.js @@ -0,0 +1,93 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {bindActionCreators} from 'redux'; +import {hideInstaller} from 'modules/app/containers/AppContainer/actions'; +import { + showModal as showDirPathModal +} from 'modules/installer/containers/SelectDirectoryModal/actions'; +import { + showModal as confirmModal +} from 'modules/app/containers/ConfirmationModal/actions'; + +export const GET_MARKET_INDEX_LIST = "Installer/GET_MARKET_INDEX_LIST"; +export const SET_MARKET_INDEX_LIST = "Installer/SET_MARKET_INDEX_LIST"; + +export const PRE_INSTALL_NAMESPACES = "Installer/PRE_INSTALL_NAMESPACES"; +export const CANCEL_INSTALL_NAMESPACES = "Installer/CANCEL_INSTALL_NAMESPACES"; +export const INSTALL_NAMESPACES = "Installer/INSTALL_NAMESPACES"; + +export const SEARCH_NAMESPACES = "Installer/SEARCH_NAMESPACES"; +export const CLEAR_SEARCH_NAMESPACES = "Installer/CLEAR_SEARCH_NAMESPACES"; +export const SHOW_MORE_NAMESPACES = "Installer/SHOW_MORE_NAMESPACES"; + +export const getMarketIndexList = () => ({type: GET_MARKET_INDEX_LIST}); +export const setMarketIndexList = (indexList) => ({type: SET_MARKET_INDEX_LIST, payload: {indexList}}); + +export const installFromDir = (dirPath) => (dispatch, getState) => { + dispatch({type: PRE_INSTALL_NAMESPACES, payload: {dirPath}}); +}; + +export const installFromUrl = (url) => (dispatch, getState) => { + dispatch({type: PRE_INSTALL_NAMESPACES, payload: {url}}); +}; + +export const install = (installationOptions) => (dispatch, getState) => { + if (installationOptions) { + const {existingNamespaceDirs, namespacesSrcDirPath} = installationOptions; + if (existingNamespaceDirs && existingNamespaceDirs.length > 0) { + let namespacesString = ''; + existingNamespaceDirs.forEach(item => { + namespacesString += '* ' + item + '\n'; + }); + dispatch(confirmModal( + 'The project already has the following namespaces:\n\n' + + namespacesString + '\n\n' + + '**Warning:** The corresponding directories along with global reducer and sagas will be rewritten', + () => { + dispatch({type: INSTALL_NAMESPACES, payload: {namespacesSrcDirPath}}); + }, + () => { + dispatch({type: CANCEL_INSTALL_NAMESPACES}); + } + )); + } else { + dispatch({type: INSTALL_NAMESPACES, payload: {namespacesSrcDirPath}}); + } + } +}; + +export const showMore = () => ({type: SHOW_MORE_NAMESPACES}); + +export const search = (searchText) => (dispatch, getState) => { + if (searchText && searchText.length > 0) { + dispatch({type: SEARCH_NAMESPACES, payload: searchText}); + } else { + dispatch(clearSearch()); + } +}; + +export const clearSearch = () => ({type: CLEAR_SEARCH_NAMESPACES}); + +export const containerActions = (dispatch) => bindActionCreators({ + hideInstaller, + showDirPathModal, + installFromUrl, + getMarketIndexList, + showMore, + search, + clearSearch +}, dispatch); diff --git a/src-client/modules/installer/containers/Installer/index.js b/src-client/modules/installer/containers/Installer/index.js new file mode 100644 index 00000000..d644e2ce --- /dev/null +++ b/src-client/modules/installer/containers/Installer/index.js @@ -0,0 +1,278 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { debounce } from 'lodash'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; +import imgSrc from 'assets/app/css/img/umylogo-white.svg'; + +import { ButtonGroup, Button } from 'react-bootstrap'; +import { NamespaceCard } from 'components'; + +const containerStyle = { + position: 'absolute', + top: '0px', + left: '0px', + right: '0px', + bottom: '0px', + overflow: 'auto', + display: 'flex', +}; +const toolbarContainerStyle = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', +}; +const toolbarLabelStyle = { + margin: '0 1em' +}; +const logoSectionStyle = { + width: '10em', + padding: '0 2em', +}; +const closeSectionStyle = { + width: '10em', + padding: '0 2em', +}; +const centerSectionStyle = { + display: 'flex', + flexGrow: 2, + justifyContent: 'center' +}; +const toolbarSectionStyle = { + width: '100%', + display: 'flex', + justifyContent: 'center', + margin: '2em 0 2em 0', +}; +const galleryStyle = { + display: 'flex', + flexDirection: 'row', + flexFlow: 'wrap', + justifyContent: 'center', + alignContent: 'stretch', +}; +const namespaceCardStyle = { + margin: '1em 1em 0 1em', + // width: '45%', + flexGrow: 0, +}; +const toolbarStyle = { + display: 'flex', + alignItems: 'center', + flexDirection: 'row', +}; + +class Container extends Component { + + constructor (props) { + super(props); + this.handleClose = this.handleClose.bind(this); + this.handleBrowseFiles = this.handleBrowseFiles.bind(this); + this.handleInstall = this.handleInstall.bind(this); + this.handleShowMore = this.handleShowMore.bind(this); + this.handleChangeSearch = this.handleChangeSearch.bind(this); + this.handleClearSearch = this.handleClearSearch.bind(this); + this.state = { + selectedFile: null, + searchText: this.props.searchText, + }; + } + + componentDidMount () { + this.containerElement.scrollTop = 0; + this.delayedChangeSearchValue = debounce(text => { + if (this.props.search) { + this.props.search(text); + } + }, 500); + this.props.getMarketIndexList(); + } + + handleClose (e) { + e.stopPropagation(); + e.preventDefault(); + this.props.hideInstaller(); + } + + handleBrowseFiles (e) { + this.props.showDirPathModal(); + } + + handleInstall (url) { + this.props.installFromUrl(url); + } + + handleShowMore (e) { + e.stopPropagation(); + e.preventDefault(); + const panelOffset = $(this.showMoreButton).offset(); + this.props.showMore(); + const $body = $(this.containerElement); + let diff = (panelOffset.top + $body.scrollTop()) - $body.offset().top; + let margin = 130; + $body.animate( + {scrollTop: (diff - margin)}, + 300 + ); + setTimeout(() => { + this.setState({isExpanded: !isExpanded}); + }, 300); + } + + handleChangeSearch (e) { + this.delayedChangeSearchValue(this.inputSearchElement.value); + this.setState({searchText: this.inputSearchElement.value}); + } + + handleClearSearch (e) { + this.props.clearSearch(); + this.setState({searchText: ''}); + } + + render () { + const {filteredIndexList, limit} = this.props; + const {searchText} = this.state; + let namespacesCards = []; + for (let i = 0; i < filteredIndexList.length; i++) { + if (i >= limit) { + break; + } + const {gitHubRepo, gitHubOwner} = filteredIndexList[i]; + namespacesCards.push( + + ); + } + return ( +
    this.containerElement = me} + id="containerElement" + style={containerStyle} + > +
    +
    +
    +
    +
    +
    +
    +
    +

    Structor

    +
    +
    + +
    +
    +

    Market

    +
    +
    +
    +
    +
    +

    + +

    +

    + Close +

    +
    +
    +
    +
    +
    +
    + {filteredIndexList.length} items found +
    +
    +
    + this.inputSearchElement = me} + type="text" + className="form-control" + placeholder="Filter by words in description..." + value={searchText || ''} + onChange={this.handleChangeSearch} + /> + + + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + {namespacesCards} +
    + {filteredIndexList && filteredIndexList.length > limit && +
    +
    this.showMoreButton = me} + className="btn btn-primary" + onClick={this.handleShowMore} + > + Listed {namespacesCards.length} items. Show More... +
    +
    + } +
    +
    + ); + } + +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/installer/containers/Installer/reducer.js b/src-client/modules/installer/containers/Installer/reducer.js new file mode 100644 index 00000000..156eee47 --- /dev/null +++ b/src-client/modules/installer/containers/Installer/reducer.js @@ -0,0 +1,73 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + marketIndexList: [], + filteredIndexList: [], + searchText: null, + limit: 10, +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + if (type === actions.SET_MARKET_INDEX_LIST) { + return Object.assign({}, state, { + marketIndexList: [].concat(payload.indexList), + filteredIndexList: [].concat(payload.indexList), + }); + } + + if (type === actions.SHOW_MORE_NAMESPACES) { + return Object.assign({}, state, {limit: state.limit + 10}); + } + + if (type === actions.SEARCH_NAMESPACES) { + const searchWords = payload.trim().split(' '); + if (searchWords && searchWords.length > 0) { + const filteredIndexList = state.marketIndexList.filter(i => { + let foundCount = 0; + let trimmedWord; + searchWords.forEach(word => { + trimmedWord = word.trim(); + if (trimmedWord.length > 0 && + i.description && + i.description.toUpperCase().indexOf(trimmedWord.toUpperCase()) >= 0) { + foundCount++; + } + }); + return foundCount > 0; + }); + return Object.assign({}, state, { + filteredIndexList: filteredIndexList || [], + searchText: payload, + }); + } + } + + if (type === actions.CLEAR_SEARCH_NAMESPACES) { + return Object.assign({}, state, { + filteredIndexList: [].concat(state.marketIndexList), + searchText: '', + }); + } + + return state; +} + diff --git a/src-client/modules/installer/containers/Installer/sagas.js b/src-client/modules/installer/containers/Installer/sagas.js new file mode 100644 index 00000000..d365342a --- /dev/null +++ b/src-client/modules/installer/containers/Installer/sagas.js @@ -0,0 +1,115 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {fork, take, call, put, cancel} from 'redux-saga/effects'; +import {SagaCancellationException} from 'redux-saga'; +import {serverApi} from 'api'; +import * as actions from './actions.js'; +import * as spinnerActions from 'modules/app/containers/AppSpinner/actions'; +import * as messageActions from 'modules/app/containers/AppMessage/actions'; +import * as deskPageActions from 'modules/workspace/containers/DeskPage/actions'; +import * as libraryPanelActions from 'modules/workspace/containers/LibraryPanel/actions'; +import * as appContainerActions from 'modules/app/containers/AppContainer/actions'; + +const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); + +function* getMarketIndexList() { + while (true) { + yield take(actions.GET_MARKET_INDEX_LIST); + yield put(spinnerActions.started('Loading packages')); + try { + const indexList = yield call(serverApi.getMarketList); + yield put(actions.setMarketIndexList(indexList)); + } catch (error) { + yield put(messageActions.failed('Loading packages. ' + (error.message ? error.message : error))); + } + yield put(spinnerActions.done('Loading packages')); + } +} + +// function* getMarketRepos(indexList) { +// if (indexList && indexList.length > 0) { +// for (let i = 0; i < indexList.length; i++) { +// const {gitHubRepo, gitHubOwner} = indexList[i]; +// yield put(actions.getMarketRepoInfo(i, gitHubRepo, gitHubOwner)); +// try { +// const repoData = yield call(serverApi.getGHRepoInfo, gitHubRepo, gitHubOwner); +// yield put(actions.setMarketRepoInfo(i, repoData)); +// } catch (error) { +// yield put(messageActions.failed('Loading GH repo info. ' + (error.message ? error.message : error))); +// } +// } +// } +// } + +function* installNamespaces() { + while (true) { + const {payload: {namespacesSrcDirPath}} = yield take(actions.INSTALL_NAMESPACES); + yield put(spinnerActions.started('Installing namespaces')); + try { + yield call(serverApi.installFromLocalDir, namespacesSrcDirPath); + yield call(delay, 10000); + const model = yield call(serverApi.getProjectModel); + yield put(deskPageActions.loadModel(model || {})); + yield put(appContainerActions.hideInstaller()); + yield put(deskPageActions.reloadPage()); + yield put(messageActions.success('Namespaces have been installed successfully.')); + } catch (error) { + yield put(messageActions.failed('Installing namespaces. ' + (error.message ? error.message : error))); + } + yield put(spinnerActions.done('Installing namespaces')); + } +} + +function* cancelInstall() { + while (true) { + yield take(actions.CANCEL_INSTALL_NAMESPACES); + yield put(spinnerActions.started('Clean temp data')); + try { + yield call(serverApi.cancelInstall); + } catch (error) { + yield put(messageActions.failed('Clean temp data. ' + (error.message ? error.message : error))); + } + yield put(spinnerActions.done('Clean temp data')); + } +} + +function* preInstallNamespaces() { + while (true) { + const {payload: {dirPath, url}} = yield take(actions.PRE_INSTALL_NAMESPACES); + yield put(spinnerActions.started('Checking the source')); + try { + let installationOptions; + if (dirPath) { + installationOptions = yield call(serverApi.preInstallFromLocalDir, dirPath); + } else if (url) { + installationOptions = yield call(serverApi.preInstallFromUrl, url); + } + yield put(actions.install(installationOptions)); + } catch (error) { + yield put(messageActions.failed('Checking the source. ' + (error.message ? error.message : error))); + } + yield put(spinnerActions.done('Checking the source')); + } +} + +// main saga +export default function* mainSaga() { + yield fork(getMarketIndexList); + yield fork(preInstallNamespaces); + yield fork(installNamespaces); + yield fork(cancelInstall); +}; diff --git a/src-client/modules/installer/containers/Installer/selectors.js b/src-client/modules/installer/containers/Installer/selectors.js new file mode 100644 index 00000000..1c99dcd5 --- /dev/null +++ b/src-client/modules/installer/containers/Installer/selectors.js @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect' +// import { userAccountSelector } from 'modules/app/containers/AppContainer/selectors'; +import { versionSelector } from 'modules/app/containers/AppContainer/selectors'; + +// export const filteredNamespacesSelector = state => state.installer.filteredNamespaces; + +export const modelSelector = createStructuredSelector({ + filteredIndexList: state => state.installer.filteredIndexList, + searchText: state => state.installer.searchText, + limit: state => state.installer.limit, + marketIndexList: state => state.installer.marketIndexList, + version: versionSelector, +}) diff --git a/src-client/modules/installer/containers/SelectDirectoryModal/actions.js b/src-client/modules/installer/containers/SelectDirectoryModal/actions.js new file mode 100644 index 00000000..d58d3aca --- /dev/null +++ b/src-client/modules/installer/containers/SelectDirectoryModal/actions.js @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; +import { coockiesApi } from 'api'; +import { installFromDir } from 'modules/installer/containers/Installer/actions'; + +export const HIDE_MODAL = 'SelectDirectoryModal/HIDE_MODAL'; +export const SHOW_MODAL = 'SelectDirectoryModal/SHOW_MODAL'; +export const SUBMIT_MODAL = 'SelectDirectoryModal/SUBMIT_MODAL'; + +export const hideModal = () => ({type: HIDE_MODAL}); +export const showModal = () => (dispatch, getState) => { + const recentDirPaths = coockiesApi.getRecentInstallerDirPaths(); + dispatch({type: SHOW_MODAL, payload: {recentDirPaths}}); +}; +export const submitModal = (dirPath) => (dispatch, getState) => { + coockiesApi.addToRecentInstallerDirPaths(dirPath); + dispatch({type: SUBMIT_MODAL, payload: {dirPath}}); + dispatch(installFromDir(dirPath)); +}; + +export const containerActions = (dispatch) => bindActionCreators({ + hideModal, submitModal +}, dispatch); diff --git a/src-client/modules/installer/containers/SelectDirectoryModal/index.js b/src-client/modules/installer/containers/SelectDirectoryModal/index.js new file mode 100644 index 00000000..7683ec95 --- /dev/null +++ b/src-client/modules/installer/containers/SelectDirectoryModal/index.js @@ -0,0 +1,113 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +import { Modal, Button, Alert } from 'react-bootstrap'; +import { DirPathInput } from 'components'; + +class Container extends Component { + + constructor (props) { + super(props); + this.handleCancel = this.handleCancel.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.state = {error: ''}; + } + + componentDidUpdate (prevProps, prevState) { + if (this.props.show && !prevProps.show) { + this.dirPathInput.focus(); + } + } + + handleCancel (e) { + if (e) { + e.stopPropagation(); + e.preventDefault(); + } + const {hideModal} = this.props; + hideModal(); + this.clearError(); + } + + handleSubmit (e) { + if (e) { + e.stopPropagation(); + e.preventDefault(); + } + const dirPath = this.dirPathInput.getValue(); + if (dirPath && dirPath.length > 0) { + const {submitModal} = this.props; + submitModal(dirPath); + this.clearError(); + } else { + this.setState({error: 'Directory path should not be empty'}); + } + } + + clearError () { + this.setState({error: ''}); + } + + render () { + const {show, dirPath, recentDirPaths} = this.props; + const {error} = this.state; + return ( + + + Directory Path + + + {error && + {error} + } + this.dirPathInput = me} + dirPath={dirPath} + label="Namespaces package directory path" + isAutoComplete={true} + options={recentDirPaths} + onEnterKey={this.handleSubmit} + /> + + + + + + + ); + } + +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/installer/containers/SelectDirectoryModal/reducer.js b/src-client/modules/installer/containers/SelectDirectoryModal/reducer.js new file mode 100644 index 00000000..265c71d9 --- /dev/null +++ b/src-client/modules/installer/containers/SelectDirectoryModal/reducer.js @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + show: false, + dirPath: '', + recentDirPaths: [], +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + if(type === actions.HIDE_MODAL){ + return Object.assign({}, state, { + show: false, + }); + } + + if(type === actions.SHOW_MODAL){ + return Object.assign({}, state, { + show: true, + recentDirPaths: payload.recentDirPaths, + }); + } + + if(type === actions.SUBMIT_MODAL){ + return Object.assign({}, state, { + show: false, + dirPath: payload.dirPath, + recentDirPaths: [].concat(state.recentDirPaths, [payload.dirPath]), + }); + } + + return state; +} + diff --git a/src-client/modules/installer/containers/SelectDirectoryModal/sagas.js b/src-client/modules/installer/containers/SelectDirectoryModal/sagas.js new file mode 100644 index 00000000..5231030b --- /dev/null +++ b/src-client/modules/installer/containers/SelectDirectoryModal/sagas.js @@ -0,0 +1,19 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// main saga +export default function* mainSaga() { +}; diff --git a/src-client/modules/installer/containers/SelectDirectoryModal/selectors.js b/src-client/modules/installer/containers/SelectDirectoryModal/selectors.js new file mode 100644 index 00000000..b846927d --- /dev/null +++ b/src-client/modules/installer/containers/SelectDirectoryModal/selectors.js @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; + +export const modelSelector = createStructuredSelector({ + show: state => state.selectDirectoryModal.show, + dirPath: state => state.selectDirectoryModal.dirPath, + recentDirPaths: state => state.selectDirectoryModal.recentDirPaths, +}); + diff --git a/src-client/modules/installer/index.js b/src-client/modules/installer/index.js new file mode 100644 index 00000000..72036b84 --- /dev/null +++ b/src-client/modules/installer/index.js @@ -0,0 +1,18 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export Installer from './containers/Installer'; +export SelectDirectoryModal from './containers/SelectDirectoryModal'; diff --git a/src-client/modules/workspace/containers/BlueprintControls/actions.js b/src-client/modules/workspace/containers/BlueprintControls/actions.js new file mode 100644 index 00000000..843dcce7 --- /dev/null +++ b/src-client/modules/workspace/containers/BlueprintControls/actions.js @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; +import { coockiesApi } from 'api'; +import { updatePage } from 'modules/workspace/containers/DeskPage/actions'; + +export const SET_SHOW_BUTTONS = 'BlueprintControls/SET_SHOW_BUTTONS'; + +export const setShowButtons = (showBlueprintButtons) => (dispatch, getState) => { + coockiesApi.saveDeskSettings({showBlueprintButtons}); + dispatch({type: SET_SHOW_BUTTONS, payload: showBlueprintButtons}); + dispatch(updatePage()); +}; + +export const containerActions = (dispatch) => bindActionCreators({ + setShowButtons +}, dispatch); diff --git a/src-client/modules/workspace/containers/BlueprintControls/index.js b/src-client/modules/workspace/containers/BlueprintControls/index.js new file mode 100644 index 00000000..ca8a2a12 --- /dev/null +++ b/src-client/modules/workspace/containers/BlueprintControls/index.js @@ -0,0 +1,67 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +const buttonIconStyle = { + margin: '0 0.5em' +}; +const buttonLabelStyle = { + marginRight: '0.5em' +}; + +class Container extends Component { + + constructor (props) { + super(props); + this.toggleShowButtons = this.toggleShowButtons.bind(this); + } + + toggleShowButtons (e) { + e.stopPropagation(); + e.preventDefault(); + const {setShowButtons, showBlueprintButtons} = this.props; + setShowButtons(!showBlueprintButtons); + } + + render () { + const {showBlueprintButtons} = this.props; + return ( +
    + +
    + ); + } +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/workspace/containers/BlueprintControls/reducer.js b/src-client/modules/workspace/containers/BlueprintControls/reducer.js new file mode 100644 index 00000000..c9603197 --- /dev/null +++ b/src-client/modules/workspace/containers/BlueprintControls/reducer.js @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + showBlueprintButtons: true, +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + if (type === actions.SET_SHOW_BUTTONS) { + return Object.assign({}, state, { + showBlueprintButtons: payload, + }); + } + + return state; +}; + diff --git a/src-client/modules/workspace/containers/BlueprintControls/sagas.js b/src-client/modules/workspace/containers/BlueprintControls/sagas.js new file mode 100644 index 00000000..5231030b --- /dev/null +++ b/src-client/modules/workspace/containers/BlueprintControls/sagas.js @@ -0,0 +1,19 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// main saga +export default function* mainSaga() { +}; diff --git a/src-client/modules/workspace/containers/BlueprintControls/selectors.js b/src-client/modules/workspace/containers/BlueprintControls/selectors.js new file mode 100644 index 00000000..9d6a20ff --- /dev/null +++ b/src-client/modules/workspace/containers/BlueprintControls/selectors.js @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; + +export const showBlueprintButtonsSelector = state => state.blueprintControls.showBlueprintButtons; + +export const modelSelector = createStructuredSelector({ + showBlueprintButtons: showBlueprintButtonsSelector, +}); + diff --git a/src-client/modules/workspace/containers/ClipboardControls/actions.js b/src-client/modules/workspace/containers/ClipboardControls/actions.js new file mode 100644 index 00000000..1e9fd433 --- /dev/null +++ b/src-client/modules/workspace/containers/ClipboardControls/actions.js @@ -0,0 +1,297 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; +import { graphApi, utilsStore } from 'api'; +import { updateMarked, updatePage } from 'modules/workspace/containers/DeskPage/actions'; +import { + removeSelectedKeys, + setSelectedKeys +} from 'modules/workspace/containers/SelectionBreadcrumbs/actions'; +import { + removeClipboardKeys, + CLIPBOARD_COPY, + CLIPBOARD_CUT, + CLIPBOARD_NEW +} from 'modules/workspace/containers/ClipboardIndicator/actions'; +import { pushHistory } from 'modules/workspace/containers/HistoryControls/actions'; +import { + quickBeforeModal, + quickFirstModal, + quickLastModal, + quickAfterModal, + quickReplaceModal +} from 'modules/workspace/containers/QuickAppendModal/actions'; +import { setHighlightSelectedKey } from 'modules/workspace/containers/SelectionBreadcrumbs/actions'; + +const setTargetKey = (targetKey, selectedKeys) => { + let selectedNode; + if (selectedKeys && selectedKeys.length > 0) { + selectedKeys.forEach(key => { + selectedNode = graphApi.getNode(key); + if (selectedNode && selectedNode.selected) { + selectedNode.selected = undefined; + } + }); + } + let nextGraphNode = graphApi.getNode(targetKey); + if (nextGraphNode) { + nextGraphNode.selected = true; + } +}; + +export const pasteBefore = (targetKey, componentNames) => (dispatch, getState) => { + const { + clipboardIndicator: {clipboardMode, clipboardKeys}, + libraryPanel: {componentTree} + } = getState(); + let resultKeys; + if (targetKey) { + dispatch(setSelectedKeys([targetKey])); + dispatch(setHighlightSelectedKey(targetKey, false)); + } + if (clipboardKeys && clipboardKeys.length > 0) { + dispatch(pushHistory()); + if (clipboardMode === CLIPBOARD_CUT) { + resultKeys = graphApi.cutPasteBeforeOrAfter(false); + dispatch(removeClipboardKeys()); + } else if (clipboardMode === CLIPBOARD_COPY) { + resultKeys = graphApi.copyPasteBeforeOrAfter(false); + } else if (clipboardMode === CLIPBOARD_NEW) { + resultKeys = graphApi.fromBufferBeforeOrAfter(false); + dispatch(removeClipboardKeys()); + } + } else { + const variantModel = utilsStore.getComponentTupleModel(componentTree, componentNames); + if(variantModel){ + dispatch(pushHistory()); + resultKeys = [].concat(graphApi.quickBeforeOrAfter(variantModel, false)); + } else { + console.error('Quick add before: model for variant key was not found'); + } + } + if (resultKeys && resultKeys.length > 0) { + dispatch(setSelectedKeys(resultKeys)); + dispatch(updatePage()); + } +}; + +export const pasteAfter = (targetKey, componentNames) => (dispatch, getState) => { + const { + clipboardIndicator: {clipboardMode, clipboardKeys}, + libraryPanel: {componentTree} + } = getState(); + let resultKeys; + if (targetKey) { + dispatch(setSelectedKeys([targetKey])); + dispatch(setHighlightSelectedKey(targetKey, false)); + } + if (clipboardKeys && clipboardKeys.length > 0) { + dispatch(pushHistory()); + if (clipboardMode === CLIPBOARD_CUT) { + resultKeys = graphApi.cutPasteBeforeOrAfter(true); + dispatch(removeClipboardKeys()); + } else if (clipboardMode === CLIPBOARD_COPY) { + resultKeys = graphApi.copyPasteBeforeOrAfter(true); + } else if (clipboardMode === CLIPBOARD_NEW) { + resultKeys = graphApi.fromBufferBeforeOrAfter(true); + dispatch(removeClipboardKeys()); + } + } else { + const variantModel = utilsStore.getComponentTupleModel(componentTree, componentNames); + if(variantModel){ + dispatch(pushHistory()); + resultKeys = [].concat(graphApi.quickBeforeOrAfter(variantModel, true)); + } else { + console.error('Quick add after: model for variant key was not found'); + } + } + if (resultKeys && resultKeys.length > 0) { + dispatch(setSelectedKeys(resultKeys)); + dispatch(updatePage()); + } +}; + +export const pasteFirst = (targetKey, componentNames) => (dispatch, getState) => { + const { + clipboardIndicator: {clipboardMode, clipboardKeys}, + libraryPanel: {componentTree} + } = getState(); + let resultKeys; + if (targetKey) { + dispatch(setSelectedKeys([targetKey])); + dispatch(setHighlightSelectedKey(targetKey, false)); + } + if (clipboardKeys && clipboardKeys.length > 0) { + dispatch(pushHistory()); + if (clipboardMode === CLIPBOARD_CUT) { + resultKeys = graphApi.cutPasteFirstOrLast(true); + dispatch(removeClipboardKeys()); + } else if (clipboardMode === CLIPBOARD_COPY) { + resultKeys = graphApi.copyPasteFirstOrLast(true); + } else if (clipboardMode === CLIPBOARD_NEW) { + resultKeys = graphApi.fromBufferFirstOrLast(true); + dispatch(removeClipboardKeys()); + } + } else { + const variantModel = utilsStore.getComponentTupleModel(componentTree, componentNames); + if(variantModel){ + dispatch(pushHistory()); + resultKeys = [].concat(graphApi.quickFirstOrLast(variantModel, true)); + } else { + console.error('Quick add as first: model for variant key was not found'); + } + } + if (resultKeys && resultKeys.length > 0) { + dispatch(setSelectedKeys(resultKeys)); + dispatch(updatePage()); + } +}; + +export const pasteLast = (targetKey, componentNames) => (dispatch, getState) => { + const { + clipboardIndicator: {clipboardMode, clipboardKeys}, + libraryPanel: {componentTree} + } = getState(); + let resultKeys; + if (targetKey) { + dispatch(setSelectedKeys([targetKey])); + dispatch(setHighlightSelectedKey(targetKey, false)); + } + if (clipboardKeys && clipboardKeys.length > 0) { + dispatch(pushHistory()); + if (clipboardMode === CLIPBOARD_CUT) { + resultKeys = graphApi.cutPasteFirstOrLast(false); + dispatch(removeClipboardKeys()); + } else if (clipboardMode === CLIPBOARD_COPY) { + resultKeys = graphApi.copyPasteFirstOrLast(false); + } else if (clipboardMode === CLIPBOARD_NEW) { + resultKeys = graphApi.fromBufferFirstOrLast(false); + dispatch(removeClipboardKeys()); + } + } else { + const variantModel = utilsStore.getComponentTupleModel(componentTree, componentNames); + if(variantModel){ + dispatch(pushHistory()); + resultKeys = [].concat(graphApi.quickFirstOrLast(variantModel, false)); + } else { + console.error('Quick add as first: model for variant key was not found'); + } + } + if (resultKeys && resultKeys.length > 0) { + dispatch(setSelectedKeys(resultKeys)); + dispatch(updatePage()); + } +}; + +export const pasteReplace = (targetKey, componentNames) => (dispatch, getState) => { + const { + clipboardIndicator: {clipboardMode, clipboardKeys}, + libraryPanel: {componentTree} + } = getState(); + let resultKeys; + if (targetKey) { + dispatch(setSelectedKeys([targetKey])); + dispatch(setHighlightSelectedKey(targetKey, false)); + } + if (clipboardKeys && clipboardKeys.length > 0) { + dispatch(pushHistory()); + if (clipboardMode === CLIPBOARD_CUT) { + resultKeys = graphApi.cutPasteReplace(); + dispatch(removeClipboardKeys()); + } else if (clipboardMode === CLIPBOARD_COPY) { + resultKeys = graphApi.copyPasteReplace(); + } else if (clipboardMode === CLIPBOARD_NEW) { + resultKeys = graphApi.fromBufferReplace(); + dispatch(removeClipboardKeys()); + } + } else { + const variantModel = utilsStore.getComponentTupleModel(componentTree, componentNames); + if(variantModel){ + dispatch(pushHistory()); + resultKeys = [].concat(graphApi.quickReplace(variantModel)); + } else { + console.error('Quick replace: model for variant key was not found'); + } + } + if (resultKeys && resultKeys.length > 0) { + dispatch(setSelectedKeys(resultKeys)); + dispatch(updatePage()); + } +}; + +export const handleBefore = (targetKey) => (dispatch, getState) => { + const { + clipboardIndicator: {clipboardKeys}, + } = getState(); + if (clipboardKeys && clipboardKeys.length > 0) { + dispatch(pasteBefore(targetKey)); + } else { + dispatch(quickBeforeModal(targetKey)); + } +}; + +export const handleFirst = (targetKey) => (dispatch, getState) => { + const { + clipboardIndicator: {clipboardKeys}, + } = getState(); + if (clipboardKeys && clipboardKeys.length > 0) { + dispatch(pasteFirst(targetKey)); + } else { + dispatch(quickFirstModal(targetKey)); + } +}; + +export const handleLast = (targetKey) => (dispatch, getState) => { + const { + clipboardIndicator: {clipboardKeys}, + } = getState(); + if (clipboardKeys && clipboardKeys.length > 0) { + dispatch(pasteLast(targetKey)); + } else { + dispatch(quickLastModal(targetKey)); + } +}; + +export const handleAfter = (targetKey) => (dispatch, getState) => { + const { + clipboardIndicator: {clipboardKeys}, + } = getState(); + if (clipboardKeys && clipboardKeys.length > 0) { + dispatch(pasteAfter(targetKey)); + } else { + dispatch(quickAfterModal(targetKey)); + } +}; + +export const handleReplace = (targetKey) => (dispatch, getState) => { + const { + clipboardIndicator: {clipboardKeys}, + } = getState(); + if (clipboardKeys && clipboardKeys.length > 0) { + dispatch(pasteReplace(targetKey)); + } else { + dispatch(quickReplaceModal(targetKey)); + } +}; + +export const containerActions = (dispatch) => bindActionCreators({ + handleBefore, + handleFirst, + handleLast, + handleAfter, + handleReplace +}, dispatch); diff --git a/src-client/modules/workspace/containers/ClipboardControls/index.js b/src-client/modules/workspace/containers/ClipboardControls/index.js new file mode 100644 index 00000000..cde45314 --- /dev/null +++ b/src-client/modules/workspace/containers/ClipboardControls/index.js @@ -0,0 +1,130 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +const wideButtonLabelStyle = { + margin: '0 0.5em', + fontSize: '11px' +}; + +class Container extends Component { + + constructor (props) { + super(props); + this.handleButtonClick = this.handleButtonClick.bind(this); + } + + handleButtonClick (e) { + e.preventDefault(); + e.stopPropagation(); + const {handleBefore, handleFirst, handleLast, handleAfter, handleReplace} = this.props; + const funcSignature = e.currentTarget.dataset.func; + switch (funcSignature) { + case 'pasteBefore': + handleBefore(); + break; + case 'pasteAfter': + handleAfter(); + break; + case 'pasteFirst': + handleFirst(); + break; + case 'pasteLast': + handleLast(); + break; + case 'pasteReplace': + handleReplace(); + break; + default: + break; + } + } + + render () { + const {selectionBreadcrumbsModel: {selectedKeys}} = this.props; + + let controlGroup = ( +
    + + + + + +
    + ); + return ( +
    + {controlGroup} +
    + ); + } +} + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/workspace/containers/ClipboardControls/reducer.js b/src-client/modules/workspace/containers/ClipboardControls/reducer.js new file mode 100644 index 00000000..57a7eb32 --- /dev/null +++ b/src-client/modules/workspace/containers/ClipboardControls/reducer.js @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = {}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + return state; +}; + diff --git a/src-client/modules/workspace/containers/ClipboardControls/sagas.js b/src-client/modules/workspace/containers/ClipboardControls/sagas.js new file mode 100644 index 00000000..71842895 --- /dev/null +++ b/src-client/modules/workspace/containers/ClipboardControls/sagas.js @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// main saga +export default function* mainSaga () { + //yield [fork(getProjectInfo)]; + +}; diff --git a/src-client/modules/workspace/containers/ClipboardControls/selectors.js b/src-client/modules/workspace/containers/ClipboardControls/selectors.js new file mode 100644 index 00000000..1dc2de41 --- /dev/null +++ b/src-client/modules/workspace/containers/ClipboardControls/selectors.js @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; + +export const modelSelector = createStructuredSelector({ + componentModel: state => state.clipboardControls, + clipboardIndicatorModel: state => state.clipboardIndicator, + selectionBreadcrumbsModel: state => state.selectionBreadcrumbs + //deskModel: state => state.desk +}); + diff --git a/src-client/modules/workspace/containers/ClipboardIndicator/actions.js b/src-client/modules/workspace/containers/ClipboardIndicator/actions.js new file mode 100644 index 00000000..6f36c19e --- /dev/null +++ b/src-client/modules/workspace/containers/ClipboardIndicator/actions.js @@ -0,0 +1,96 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; +import { graphApi } from 'api'; +import { + updateMarked, + updatePage +} from 'modules/workspace/containers/DeskPage/actions'; +import { + removeSelectedKeys, + setSelectedKeys +} from 'modules/workspace/containers/SelectionBreadcrumbs/actions'; + +export const CLIPBOARD_EMPTY = 'Empty'; +export const CLIPBOARD_NEW = 'New'; +export const CLIPBOARD_COPY = 'Copy'; +export const CLIPBOARD_CUT = 'Cut'; + +export const RESET_KEYS = "ClipboardControls/RESET_KEYS"; + +export const setForCuttingKeys = (keys) => (dispatch, getState) => { + dispatch(removeClipboardKeys()); + let newKeys = []; + if(keys && keys.length > 0){ + keys.forEach(key => { + graphApi.setForCutting(key); + newKeys.push(key); + }); + } + dispatch(removeSelectedKeys()); + dispatch({type: RESET_KEYS, payload: {keys: newKeys, mode: CLIPBOARD_CUT}}); +}; + +export const setForCopyingKeys = (keys) => (dispatch, getState) => { + dispatch(removeClipboardKeys()); + let newKeys = []; + if(keys && keys.length > 0){ + keys.forEach(key => { + graphApi.setForCopying(key); + newKeys.push(key); + }); + } + // dispatch(removeSelectedKeys()); + dispatch({type: RESET_KEYS, payload: {keys: newKeys, mode: CLIPBOARD_COPY}}); +}; + +export const setForNew = (model) => (dispatch, getState) => { + dispatch(removeClipboardKeys()); + const bufferKey = graphApi.setBuffer(model); + dispatch({type: RESET_KEYS, payload: {keys: [bufferKey], mode: CLIPBOARD_NEW}}); +}; + +export const removeClipboardKeys = () => (dispatch, getState) => { + const { clipboardIndicator: { clipboardKeys } } = getState(); + if(clipboardKeys && clipboardKeys.length > 0){ + clipboardKeys.forEach(key => { + graphApi.removeClipboardMarks(key); + }); + graphApi.removeBuffer(); + } + dispatch({type: RESET_KEYS, payload: {keys: [], mode: CLIPBOARD_EMPTY}}); + dispatch(updateMarked()); +}; + +export const resetClipboardKeys = () => (dispatch, getState) => { + const { clipboardIndicator: { clipboardKeys, clipboardMode } } = getState(); + let newKeys = []; + if(clipboardKeys && clipboardKeys.length > 0){ + let node; + clipboardKeys.forEach(key => { + node = graphApi.getNode(key); + if(node){ + newKeys.push(key); + } + }); + } + dispatch({type: RESET_KEYS, payload: {keys: newKeys, mode: clipboardMode}}); +}; + +export const containerActions = (dispatch) => bindActionCreators({ + removeClipboardKeys +}, dispatch); diff --git a/src-client/modules/workspace/containers/ClipboardIndicator/index.js b/src-client/modules/workspace/containers/ClipboardIndicator/index.js new file mode 100644 index 00000000..5e9cd9ad --- /dev/null +++ b/src-client/modules/workspace/containers/ClipboardIndicator/index.js @@ -0,0 +1,161 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { + containerActions, + CLIPBOARD_COPY, + CLIPBOARD_CUT, + CLIPBOARD_EMPTY, + CLIPBOARD_NEW +} from './actions.js'; +import { graphApi } from 'api'; + +const makeTitle = (componentName) => { + let titleComponentName = componentName; + if(titleComponentName && titleComponentName.length > 30){ + titleComponentName = titleComponentName.substr(0, 27) + '...'; + } + return titleComponentName; +}; + +class Container extends Component { + + constructor(props) { + super(props); + } + + render(){ + const { componentModel: {clipboardMode, clipboardKeys}} = this.props; + const { removeClipboardKeys } = this.props; + + const containerStyle = { + display: 'flex', + flexDirection: 'row', + flexWrap: 'nowrap', + alignItems: 'center' + //padding: '0px 0px 0px 10px' + }; + const controlsGroupStyle = { + padding: '0px', + margin: '0px 0.5em 0px 0px' + }; + let typeLabelStyle = { + padding: '3px 6px', + borderRadius: '3px', + cursor: 'pointer', + backgroundColor: 'rgb(227, 227, 227)', + color: 'rgb(107, 107, 107)', + marginRight: '0.3em', + textShadow: '0 1px 0px rgba(255, 255, 255, 0.8)' + }; + let activeStyle = { + padding: '2px 6px', + borderRadius: '3px'}; + let clipboardTypeLabel; + if(clipboardMode === CLIPBOARD_COPY){ + activeStyle.backgroundColor = '#f2fae3'; + activeStyle.color = '#659f13'; + activeStyle.border = '1px solid #659f13'; + clipboardTypeLabel = 'Copied in clipboard'; + } else if(clipboardMode === CLIPBOARD_CUT){ + activeStyle.backgroundColor = '#fffceb'; + activeStyle.color = '#e28327'; + activeStyle.border = '1px solid #e28327'; + clipboardTypeLabel = 'Cut in clipboard'; + } else if(clipboardMode === CLIPBOARD_NEW){ + activeStyle.backgroundColor = '#ebf7fd'; + activeStyle.color = '#2d7091'; + activeStyle.border = '1px solid #2d7091'; + clipboardTypeLabel = 'New in clipboard'; + } else { + clipboardTypeLabel = 'Empty clipboard'; + } + let clipboardContent = null; + if(clipboardKeys && clipboardKeys.length > 0){ + + if(clipboardKeys.length === 1){ + let clipboardNode = graphApi.getNode(clipboardKeys[0]); + if(clipboardNode){ + const {modelNode} = clipboardNode; + clipboardContent = ( + + {makeTitle(modelNode.type)} + {modelNode.namespace && + {'[' + makeTitle(modelNode.namespace) + ']'} + } + + ); + } + } else if(clipboardKeys.length > 0) { + const childrenMenuItems = []; + let clipboardNode; + clipboardKeys.forEach((key, index) => { + clipboardNode = graphApi.getNode(key); + if(clipboardNode){ + const {modelNode} = clipboardNode; + childrenMenuItems.push( +
  • + {e.stopPropagation(); e.preventDefault();}} + style={{display: 'flex', alignItems: 'center'}}> + {modelNode.type} + {modelNode.namespace && + {'[' + modelNode.namespace + ']'} + } + +
  • + ); + } + }); + clipboardContent = ( + + + Multiple...  + +
      + {childrenMenuItems} +
    +
    + ); + } + } + return ( +
    +
    + {removeClipboardKeys();}} + title="Click to remove items from clipboard."> + {clipboardMode !== CLIPBOARD_EMPTY ? : null } + {clipboardTypeLabel} + + {clipboardContent} +
    +
    + ); + } +} + + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/workspace/containers/ClipboardIndicator/reducer.js b/src-client/modules/workspace/containers/ClipboardIndicator/reducer.js new file mode 100644 index 00000000..23c09dba --- /dev/null +++ b/src-client/modules/workspace/containers/ClipboardIndicator/reducer.js @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + clipboardMode: actions.CLIPBOARD_EMPTY, + clipboardKeys:[] +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + if(type === actions.RESET_KEYS){ + return Object.assign({}, state, { + clipboardKeys: payload.keys, + clipboardMode: payload.mode + }); + } + + return state; +} + diff --git a/src-client/modules/workspace/containers/ClipboardIndicator/sagas.js b/src-client/modules/workspace/containers/ClipboardIndicator/sagas.js new file mode 100644 index 00000000..82db1c85 --- /dev/null +++ b/src-client/modules/workspace/containers/ClipboardIndicator/sagas.js @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// main saga +export default function* mainSaga() { + //yield [fork(getProjectInfo)]; + +}; diff --git a/src-client/modules/workspace/containers/ClipboardIndicator/selectors.js b/src-client/modules/workspace/containers/ClipboardIndicator/selectors.js new file mode 100644 index 00000000..9780a3c7 --- /dev/null +++ b/src-client/modules/workspace/containers/ClipboardIndicator/selectors.js @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStructuredSelector } from 'reselect'; + +export const clipboardMode = state => state.clipboardIndicator.clipboardMode; + +export const modelSelector = createStructuredSelector({ + componentModel: state => state.clipboardIndicator +}); + diff --git a/src-client/modules/workspace/containers/ComponentControls/actions.js b/src-client/modules/workspace/containers/ComponentControls/actions.js new file mode 100644 index 00000000..3d8838e6 --- /dev/null +++ b/src-client/modules/workspace/containers/ComponentControls/actions.js @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bindActionCreators } from 'redux'; +import { loadOptionsAndShowModal } from 'modules/workspace/containers/ComponentOptionsModal/actions'; +import { loadGenerators } from 'modules/generator/containers/Generator/actions'; +import { showModal as showSaveDefaultModelModal } from 'modules/workspace/containers/SaveDefaultModelModal/actions'; + +export const containerActions = (dispatch) => bindActionCreators({ + loadOptionsAndShowModal, loadGenerators, showSaveDefaultModelModal +}, dispatch); diff --git a/src-client/modules/workspace/containers/ComponentControls/index.js b/src-client/modules/workspace/containers/ComponentControls/index.js new file mode 100644 index 00000000..5bfc8b8e --- /dev/null +++ b/src-client/modules/workspace/containers/ComponentControls/index.js @@ -0,0 +1,100 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; + +const buttonLabelStyle = { + margin: '0 0.5em' +}; + +class Container extends Component { + + constructor(props) { + super(props); + this.onEdit = this.onEdit.bind(this); + this.onGenerate = this.onGenerate.bind(this); + this.onSaveDefaultModel = this.onSaveDefaultModel.bind(this); + } + + onEdit(e) { + e.stopPropagation(); + e.preventDefault(); + const {selectedComponents, loadOptionsAndShowModal} = this.props; + if (selectedComponents && selectedComponents.length === 1) { + loadOptionsAndShowModal(selectedComponents[0]); + } + } + + onGenerate(e) { + e.stopPropagation(); + e.preventDefault(); + const {loadGenerators} = this.props; + loadGenerators(); + } + + onSaveDefaultModel(e) { + e.stopPropagation(); + e.preventDefault(); + const {showSaveDefaultModelModal} = this.props; + showSaveDefaultModelModal(); + } + + render(){ + const {selectedComponents} = this.props; + const isEnabled = selectedComponents && selectedComponents.length === 1; + return ( +
    + + + +
    + ); + } +} + + +export default connect(modelSelector, containerActions)(Container); + diff --git a/src-client/modules/workspace/containers/ComponentControls/reducer.js b/src-client/modules/workspace/containers/ComponentControls/reducer.js new file mode 100644 index 00000000..f280aecd --- /dev/null +++ b/src-client/modules/workspace/containers/ComponentControls/reducer.js @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as actions from './actions.js'; + +const initialState = { + +}; + +export default (state = initialState, action = {}) => { + + const {type, payload} = action; + + return state; +} + diff --git a/src-client/modules/workspace/containers/ComponentControls/sagas.js b/src-client/modules/workspace/containers/ComponentControls/sagas.js new file mode 100644 index 00000000..82db1c85 --- /dev/null +++ b/src-client/modules/workspace/containers/ComponentControls/sagas.js @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// main saga +export default function* mainSaga() { + //yield [fork(getProjectInfo)]; + +}; diff --git a/src-client/modules/workspace/containers/ComponentControls/selectors.js b/src-client/modules/workspace/containers/ComponentControls/selectors.js new file mode 100644 index 00000000..c6bccce3 --- /dev/null +++ b/src-client/modules/workspace/containers/ComponentControls/selectors.js @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {forOwn, isObject} from 'lodash'; +import { createStructuredSelector } from 'reselect'; +import { selectedComponentsSelector } from 'modules/workspace/containers/SelectionControls/selectors'; + +export const modelSelector = createStructuredSelector({ + componentModel: state => state.componentControls, + selectedComponents: selectedComponentsSelector +}); + diff --git a/src-client/modules/workspace/containers/ComponentOptionsModal/actions.js b/src-client/modules/workspace/containers/ComponentOptionsModal/actions.js new file mode 100644 index 00000000..30a96dba --- /dev/null +++ b/src-client/modules/workspace/containers/ComponentOptionsModal/actions.js @@ -0,0 +1,80 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isEmpty } from 'lodash'; +import { bindActionCreators } from 'redux'; +import { graphApi, serverApi } from 'api'; +import { failed } from 'modules/app/containers/AppMessage/actions'; + +export const LOAD_OPTIONS_AND_SHOW_MODAL = "ComponentOptionsModal/LOAD_OPTIONS_AND_SHOW_MODAL"; +export const SET_OPTIONS = "ComponentOptionsModal/SET_OPTIONS"; +export const SAVE_SOURCE_CODE = "ComponentOptionsModal/SAVE_SOURCE_CODE"; +export const SAVE_SOURCE_CODE_DONE = "ComponentOptionsModal/SAVE_SOURCE_CODE_DONE"; +export const SAVE_SOURCE_CODE_TIMEOUT = "ComponentOptionsModal/SAVE_SOURCE_CODE_TIMEOUT"; +export const HIDE_MODAL = "ComponentOptionsModal/HIDE_MODAL"; +export const SHOW_MODAL = "ComponentOptionsModal/SHOW_MODAL"; + +export const loadOptionsAndShowModal = (componentObject) => ({ + type: LOAD_OPTIONS_AND_SHOW_MODAL, + payload: componentObject +}); +export const saveSourceCodeDone = () => ({type: SAVE_SOURCE_CODE_DONE}); +export const saveSourceCodeTimeout = () => ({type: SAVE_SOURCE_CODE_TIMEOUT}); +export const hideModal = () => ({type: HIDE_MODAL}); +export const showModal = () => ({type: SHOW_MODAL}); + +export const setOptions = (componentObject) => (dispatch, getState) => { + const {key, sourceFilePath, props, text, componentName, sourceCode, readmeText} = componentObject; + dispatch({ + type: SET_OPTIONS, payload: { + selectedKey: key, + sourceCode: sourceCode, + sourceFilePath: sourceFilePath, + sourceProps: props ? JSON.stringify(props, null, 4) : JSON.stringify({}), + sourceText: text, + readmeText: readmeText + } + }); +}; + +export const submitChanges = (componentObject) => (dispatch, getState) => { + const {componentOptionsModal:{ selectedKey, sourceFilePath }} = getState(); + const {sourceCode, sourceProps, sourceText} = componentObject; + let sourcePropsObj; + try{ + sourcePropsObj = JSON.parse(sourceProps); + if(sourcePropsObj.style && isEmpty(sourcePropsObj.style)) { + delete sourcePropsObj.style; + } + } catch(e){ + dispatch(failed('Parsing properties error. Error: ' + (e.message ? e.message : e))); + } + if (sourcePropsObj) { + dispatch({ + type: SAVE_SOURCE_CODE, payload: { + key: selectedKey, + sourceFilePath: sourceFilePath, + sourcePropsObj: sourcePropsObj, + text: sourceText, + sourceCode: sourceCode + } + }); + } +}; + +export const containerActions = (dispatch) => bindActionCreators({ + hideModal, submitChanges +}, dispatch); diff --git a/src-client/modules/workspace/containers/ComponentOptionsModal/index.js b/src-client/modules/workspace/containers/ComponentOptionsModal/index.js new file mode 100644 index 00000000..f6ebcecc --- /dev/null +++ b/src-client/modules/workspace/containers/ComponentOptionsModal/index.js @@ -0,0 +1,180 @@ +/* + * Copyright 2017 Alexander Pustovalov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import marked from 'marked'; +import { modelSelector } from './selectors.js'; +import { containerActions } from './actions.js'; +import { Modal, Tabs, Tab, Button } from 'react-bootstrap'; +import { AceEditor } from 'components'; + +class Container extends Component { + + constructor (props) { + super(props); + this.state = { + activeTabKey: 1 + }; + this.handleClose = this.handleClose.bind(this); + this.handleSave = this.handleSave.bind(this); + this.handleSelectTab = this.handleSelectTab.bind(this); + this.handleTextAreaKeyDown = this.handleTextAreaKeyDown.bind(this); + } + + handleSelectTab (eventKey) { + if (eventKey) { + this.setState({ + activeTabKey: eventKey + }); + } + } + + handleClose (e) { + e.stopPropagation(); + e.preventDefault(); + const {hideModal} = this.props; + hideModal(); + this.setState({ + activeTabKey: 1 + }); + } + + handleSave (e) { + e.stopPropagation(); + e.preventDefault(); + const {submitChanges} = this.props; + const {sourceCodeEditor, sourcePropsEditor, sourceTextInput} = this.refs; + const sourceCode = sourceCodeEditor ? sourceCodeEditor.getSourceCode() : undefined; + const sourceProps = sourcePropsEditor ? sourcePropsEditor.getSourceCode() : '{}'; + const sourceText = sourceTextInput ? sourceTextInput.value : undefined; + submitChanges({sourceCode, sourceProps, sourceText}); + this.setState({ + activeTabKey: 1 + }); + } + + handleTextAreaKeyDown (e) { + if (e.which === 13) { + this.handleSave(e); + } + } + + render () { + + const {componentModel: {sourceCode, sourceFilePath, sourceProps, sourceText, readmeText, show}, hideModal} = this.props; + + const containerStyle = { + marginTop: '1em', + width: '100%', + padding: '1em' + }; + let tabPanes = []; + + if (sourceText !== undefined) { + tabPanes.push( + +
    +