pull_request_ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +125 -0
- data/Rakefile +10 -0
- data/app/assets/config/pull_request_ai_manifest.js +1 -0
- data/app/assets/javascripts/application.js +248 -0
- data/app/assets/javascripts/notifications.js +76 -0
- data/app/assets/stylesheets/pull_request_ai/application.css +15 -0
- data/app/assets/stylesheets/pull_request_ai/blocs.css +4 -0
- data/app/assets/stylesheets/pull_request_ai/helpers.css +26 -0
- data/app/assets/stylesheets/pull_request_ai/inputs.css +64 -0
- data/app/assets/stylesheets/pull_request_ai/notification.css +71 -0
- data/app/assets/stylesheets/pull_request_ai/spinner.css +24 -0
- data/app/controllers/pull_request_ai/application_controller.rb +6 -0
- data/app/controllers/pull_request_ai/pull_request_ai_controller.rb +85 -0
- data/app/helpers/pull_request_ai/application_helper.rb +6 -0
- data/app/models/pull_request_ai/application_record.rb +7 -0
- data/app/views/layouts/pull_request_ai/application.html.erb +29 -0
- data/app/views/pull_request_ai/pull_request_ai/new.html.erb +70 -0
- data/config/initializers/rack_attack.rb +30 -0
- data/config/routes.rb +13 -0
- data/lib/pull_request_ai/bitbucket/client.rb +148 -0
- data/lib/pull_request_ai/client.rb +80 -0
- data/lib/pull_request_ai/engine.rb +10 -0
- data/lib/pull_request_ai/github/client.rb +124 -0
- data/lib/pull_request_ai/openAi/client.rb +81 -0
- data/lib/pull_request_ai/openAi/interpreter.rb +19 -0
- data/lib/pull_request_ai/repo/client.rb +43 -0
- data/lib/pull_request_ai/repo/file.rb +20 -0
- data/lib/pull_request_ai/repo/prompt.rb +33 -0
- data/lib/pull_request_ai/repo/reader.rb +118 -0
- data/lib/pull_request_ai/util/configuration.rb +49 -0
- data/lib/pull_request_ai/util/error.rb +28 -0
- data/lib/pull_request_ai/util/symbol_details.rb +14 -0
- data/lib/pull_request_ai/version.rb +5 -0
- data/lib/pull_request_ai.rb +52 -0
- data/lib/tasks/pull_request_ai_tasks.rake +5 -0
- metadata +153 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 747534e2b7a4327fb3a3eb986df97000e737072500f1e70839b2d61c5edeaf57
|
4
|
+
data.tar.gz: 2647f3525b95e48a268a05ded206e5a644e6a6e10494680af11f398dd9f4173d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c918aa851b69ba5b5f079bcc75ad308132083e3a83638786063917022dc1d6cc919c69524422d64b173f4e378b923d771a3a79d6c8caa7e392f6139325f95bf8
|
7
|
+
data.tar.gz: 6afa49e24926c1895088fb60704b6e5008239b1e2214e4900cbc3e313f8a4490561a5925915c357cd173196c3f09e4650286f5bfd504573f2af43abd8966a667
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2023 André Franco
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# PullRequestAi
|
2
|
+
This Rails Engine offers a way to request Pull Request descriptions from OpenAI chatGPT and optionally allows direct creation or updating of Pull Requests on GitHub or Bitbucket.
|
3
|
+
|
4
|
+
## Installation
|
5
|
+
Add this line to your application's Gemfile:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem "pull_request_ai"
|
9
|
+
```
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
```bash
|
13
|
+
$ bundle
|
14
|
+
```
|
15
|
+
|
16
|
+
OR, install it yourself as:
|
17
|
+
```bash
|
18
|
+
$ gem install pull_request_ai
|
19
|
+
```
|
20
|
+
|
21
|
+
## Contributing
|
22
|
+
Contribution directions go here.
|
23
|
+
|
24
|
+
## License
|
25
|
+
This Rails Engine is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
26
|
+
|
27
|
+
## Configuration
|
28
|
+
|
29
|
+
To configure this Rails Engine you can **just** set some specific Environment Variables or you can use the Rails Engine initializer class.
|
30
|
+
|
31
|
+
The minimum requirement that allows this Rails Engine to ask chatGPT Pull Request descriptions based on Git respository changes is the [OpenAI Key](https://platform.openai.com/account/usage).
|
32
|
+
|
33
|
+
Using **only** Environment Variable you need to set:
|
34
|
+
- [OPENAI_API_KEY](https://platform.openai.com/account/usage)
|
35
|
+
|
36
|
+
OR, if you choose to use the initializer:
|
37
|
+
```ruby
|
38
|
+
PullRequestAi.configure do |config|
|
39
|
+
config.openai_api_key = 'YOUR_OPENAI_API_KEY'
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
## Integrations
|
44
|
+
|
45
|
+
To enable direct creation or updating of Pull Requests this Rails Engine can integrate with GitHub or Bitbucket.
|
46
|
+
|
47
|
+
For GitHub you need to provide a [GitHub Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token).
|
48
|
+
And for Bitbucket you need to provide a [Bitbucket App Password](https://bitbucket.org/account/settings/app-passwords/) and your Bitbucket Username.
|
49
|
+
|
50
|
+
### GitHub Configuration
|
51
|
+
|
52
|
+
Using **only** Environment Variable you need to set:
|
53
|
+
- [GITHUB_ACCESS_TOKEN](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token)
|
54
|
+
|
55
|
+
OR, if you choose to use the initializer:
|
56
|
+
```ruby
|
57
|
+
PullRequestAi.configure do |config|
|
58
|
+
config.github_access_token = 'YOUR_GITHUB_ACCESS_TOKEN'
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
### Bitbucket Configuration
|
63
|
+
|
64
|
+
Using **only** Environment Variable you need to set:
|
65
|
+
- [BITBUCKET_APP_PASSWORD](https://bitbucket.org/account/settings/app-passwords/)
|
66
|
+
- [BITBUCKET_USERNAME](https://bitbucket.org/account/settings/)
|
67
|
+
|
68
|
+
OR, if you choose to use the initializer:
|
69
|
+
```ruby
|
70
|
+
PullRequestAi.configure do |config|
|
71
|
+
config.bitbucket_app_password = 'YOUR_BITBUCKET_APP_PASSWORD'
|
72
|
+
config.bitbucket_username = 'YOUR_BITBUCKET_USERNAME'
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
## Usage
|
77
|
+
|
78
|
+
To use the Rails Engine interface on the browser you need to mount the engine route into your project routes. To do that include on your `routes.rb` file the following:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
mount PullRequestAi::Engine => ''
|
82
|
+
```
|
83
|
+
|
84
|
+
Then you navigate to:
|
85
|
+
|
86
|
+
```
|
87
|
+
http://127.0.0.1:3000/rrtools/pull_request_ai
|
88
|
+
```
|
89
|
+
|
90
|
+
Another way to use this Rails Engine is through code, to do that create an instance of the main client object.
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
client = PullRequestAi::Client.new
|
94
|
+
```
|
95
|
+
|
96
|
+
This object offers the following actions:
|
97
|
+
- current_opened_pull_requests(base) - method that receives a base branch and based on the current branch will return any existing open pull request from GitHub.
|
98
|
+
- destination_branches - method without arguments that will return a list of all available remote branches.
|
99
|
+
- open_pull_request(base, title, description) - method that will communicate with the GitHub API to open a new pull request with the given parameters.
|
100
|
+
- update_pull_request(number, base, title, description) - method to update an existing pull request base, title, and description.
|
101
|
+
- flatten_current_changes(branch) - method that returns changes between the current branch and the given branch in a single string.
|
102
|
+
|
103
|
+
Notes about the return of these methods, all methods take advantage of [dry-monads](https://dry-rb.org/gems/dry-monads/1.3/).
|
104
|
+
|
105
|
+
## Extra Configurations
|
106
|
+
|
107
|
+
If you need you have access to some aditional configurations which are:
|
108
|
+
- openai_api_endpoint - The OpenAI API endpoint.
|
109
|
+
- github_api_endpoint - The GitHub API endpoint.
|
110
|
+
- bitbucket_api_endpoint - The Bitbucket API endpoint.
|
111
|
+
- model - The [`model`](https://platform.openai.com/docs/models/model-endpoint-compatibility) parameter allows the user to select which OpenAI model to use for Pull Request suggestions. The default model used by this Gem is `gpt-3.5-turbo`, which is the most accessible. However, if you have access to version 4, we recommend using the `gpt-4` model.
|
112
|
+
- temperature - The [`temperature`](https://platform.openai.com/docs/api-reference/completions/create#completions/create-temperature) parameter is an OpenAI API configuration that affects the randomness of the output. Higher values produce more random results, while lower values like 0.2 produce more focused and deterministic output.
|
113
|
+
|
114
|
+
The only way to configure these parameters is using the initializer, above it is listing as well their default values:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
PullRequestAi.configure do |config|
|
118
|
+
...
|
119
|
+
config.openai_api_endpoint = 'https://api.openai.com'
|
120
|
+
config.github_api_endpoint = 'https://api.github.com'
|
121
|
+
config.bitbucket_api_endpoint = 'https://api.bitbucket.org'
|
122
|
+
config.model = 'gpt-3.5-turbo'
|
123
|
+
config.temperature = 0.6
|
124
|
+
end
|
125
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
//= link_directory ../stylesheets/pull_request_ai .css
|
@@ -0,0 +1,248 @@
|
|
1
|
+
//= require_tree .
|
2
|
+
|
3
|
+
const reloadButton = document.getElementById('reload-btn');
|
4
|
+
const requestDescriptionButton = document.getElementById('request-description-btn');
|
5
|
+
const copyChatToClipboardButton = document.getElementById('copy-chat-to-clipboard-btn');
|
6
|
+
const copyChatToDescriptionButton = document.getElementById('copy-chat-to-description-btn');
|
7
|
+
const createPullRequestButton = document.getElementById('create-pull-request-btn');
|
8
|
+
const updatePullRequestButton = document.getElementById('update-pull-request-btn');
|
9
|
+
const copyDescriptionToClipboardButton = document.getElementById('copy-description-to-clipboard-btn');
|
10
|
+
const pullRequestWebsiteButton = document.getElementById('pull-request-website-btn');
|
11
|
+
|
12
|
+
const loadingContainer = document.getElementById('loading-container');
|
13
|
+
const prepareContainer = document.getElementById('prepare-container');
|
14
|
+
const chatDescriptionContainer = document.getElementById('chat-description-container');
|
15
|
+
const pullRequestContainer = document.getElementById('pull-request-container');
|
16
|
+
|
17
|
+
const branchField = document.getElementById('branch-field');
|
18
|
+
const typeField = document.getElementById('type-field');
|
19
|
+
const summaryField = document.getElementById('summary-field');
|
20
|
+
|
21
|
+
const chatDescriptionField = document.getElementById('chat-description-field');
|
22
|
+
|
23
|
+
const pullRequestNumberField = document.getElementById('pull-request-number-field');
|
24
|
+
const pullRequestTitleField = document.getElementById('pull-request-title-field');
|
25
|
+
const pullRequestDescriptionField = document.getElementById('pull-request-description-field');
|
26
|
+
|
27
|
+
async function jsonPost(path, data) {
|
28
|
+
showSpinner();
|
29
|
+
|
30
|
+
const response = await fetch(path, {
|
31
|
+
method: 'post',
|
32
|
+
body: JSON.stringify(data),
|
33
|
+
headers: {
|
34
|
+
'Content-Type': 'application/json',
|
35
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
36
|
+
},
|
37
|
+
credentials: 'same-origin'
|
38
|
+
});
|
39
|
+
|
40
|
+
if (!response.ok && response.status != 422) {
|
41
|
+
hideSpinner();
|
42
|
+
unlockSelectors();
|
43
|
+
disableSubmission();
|
44
|
+
throw new Error(`An error has occured: ${response.statusText}`);
|
45
|
+
}
|
46
|
+
return await response.json();
|
47
|
+
}
|
48
|
+
|
49
|
+
reloadButton.onclick = () => {
|
50
|
+
window.location.reload();
|
51
|
+
}
|
52
|
+
|
53
|
+
copyChatToClipboardButton.onclick = () => {
|
54
|
+
copyToClipboardValueFrom(
|
55
|
+
chatDescriptionField,
|
56
|
+
"Chat description copied to the Clipboard."
|
57
|
+
);
|
58
|
+
}
|
59
|
+
|
60
|
+
copyDescriptionToClipboardButton.onclick = () => {
|
61
|
+
copyToClipboardValueFrom(
|
62
|
+
pullRequestDescriptionField,
|
63
|
+
"Description copied to the Clipboard."
|
64
|
+
);
|
65
|
+
}
|
66
|
+
|
67
|
+
requestDescriptionButton.onclick = () => {
|
68
|
+
const data = { branch: branchField.value, type: typeField.value, summary: summaryField.value };
|
69
|
+
|
70
|
+
lockSelectors();
|
71
|
+
|
72
|
+
jsonPost('/rrtools/pull_request_ai/prepare', data).then(data => {
|
73
|
+
hideSpinner();
|
74
|
+
unlockSelectors();
|
75
|
+
if ('errors' in data) {
|
76
|
+
window.showNotification({ message: data.errors, theme: "error" });
|
77
|
+
}
|
78
|
+
else if ('notice' in data) {
|
79
|
+
window.showNotification({ message: data.notice, theme: "warning" });
|
80
|
+
}
|
81
|
+
else {
|
82
|
+
enableSubmission(data);
|
83
|
+
}
|
84
|
+
}).catch(errorMsg => {
|
85
|
+
window.showNotification({ message: errorMsg, theme: "error" });
|
86
|
+
});
|
87
|
+
}
|
88
|
+
|
89
|
+
copyChatToDescriptionButton.onclick = () => {
|
90
|
+
const chat = chatDescriptionField.value;
|
91
|
+
const current = pullRequestDescriptionField.value;
|
92
|
+
pullRequestDescriptionField.value = current + "\n\n" + chat;
|
93
|
+
window.showNotification({
|
94
|
+
message: "Chat description copied to the Pull Request description."
|
95
|
+
});
|
96
|
+
}
|
97
|
+
|
98
|
+
createPullRequestButton.onclick = () => {
|
99
|
+
const data = {
|
100
|
+
branch: branchField.value, title: pullRequestTitleField.value, description: pullRequestDescriptionField.value
|
101
|
+
};
|
102
|
+
jsonPost('/rrtools/pull_request_ai/create', data).then(data => {
|
103
|
+
hideSpinner();
|
104
|
+
processData(data, "Pull Request created successfully.");
|
105
|
+
}).catch(errorMsg => {
|
106
|
+
window.showNotification({ message: errorMsg, theme: "error" });
|
107
|
+
});
|
108
|
+
}
|
109
|
+
|
110
|
+
updatePullRequestButton.onclick = () => {
|
111
|
+
const data = {
|
112
|
+
number: pullRequestNumberField.value, branch: branchField.value, title: pullRequestTitleField.value, description: pullRequestDescriptionField.value
|
113
|
+
};
|
114
|
+
jsonPost('/rrtools/pull_request_ai/update', data).then(data => {
|
115
|
+
hideSpinner();
|
116
|
+
processData(data, "Pull Request updated successfully.");
|
117
|
+
}).catch(errorMsg => {
|
118
|
+
window.showNotification({ message: errorMsg, theme: "error" });
|
119
|
+
});
|
120
|
+
}
|
121
|
+
|
122
|
+
function showSpinner() {
|
123
|
+
loadingContainer.style.display = 'block';
|
124
|
+
}
|
125
|
+
|
126
|
+
function hideSpinner() {
|
127
|
+
loadingContainer.style.display = 'none';
|
128
|
+
}
|
129
|
+
|
130
|
+
function lockSelectors() {
|
131
|
+
branchField.setAttribute('disabled', '');
|
132
|
+
typeField.setAttribute('disabled', '');
|
133
|
+
summaryField.setAttribute('disabled', '');
|
134
|
+
requestDescriptionButton.setAttribute('disabled', '');
|
135
|
+
}
|
136
|
+
|
137
|
+
function unlockSelectors() {
|
138
|
+
branchField.removeAttribute('disabled');
|
139
|
+
typeField.removeAttribute('disabled');
|
140
|
+
summaryField.removeAttribute('disabled');
|
141
|
+
requestDescriptionButton.removeAttribute('disabled');
|
142
|
+
}
|
143
|
+
|
144
|
+
function copyToClipboardValueFrom(field, successMessage) {
|
145
|
+
if (navigator.clipboard) {
|
146
|
+
const text = field.value;
|
147
|
+
navigator.clipboard.writeText(text);
|
148
|
+
} else {
|
149
|
+
field.select();
|
150
|
+
document.execCommand("copy");
|
151
|
+
}
|
152
|
+
window.showNotification({ message: successMessage });
|
153
|
+
}
|
154
|
+
|
155
|
+
function processData(data, successMessage) {
|
156
|
+
if ('errors' in data) {
|
157
|
+
window.showNotification({ message: data.errors, theme: "error" });
|
158
|
+
}
|
159
|
+
else if ('notice' in data) {
|
160
|
+
window.showNotification({ message: data.notice, theme: "warning" });
|
161
|
+
}
|
162
|
+
else {
|
163
|
+
window.showNotification({ message: successMessage });
|
164
|
+
updateForm(data)
|
165
|
+
}
|
166
|
+
}
|
167
|
+
|
168
|
+
function enableSubmission(data) {
|
169
|
+
chatDescriptionField.value = data.description;
|
170
|
+
pullRequestWebsiteButton.classList.add('hide');
|
171
|
+
|
172
|
+
if (data.remote_enabled) {
|
173
|
+
// With Remote configured we always show the Pull Request form.
|
174
|
+
pullRequestContainer.classList.remove('hide');
|
175
|
+
if (data.open_pr) {
|
176
|
+
// With a Pull Request open we show the chat description on top with the button to copy to the form.
|
177
|
+
chatDescriptionContainer.classList.remove('hide');
|
178
|
+
copyChatToDescriptionButton.classList.remove('hide');
|
179
|
+
|
180
|
+
// Update the Pull Request form buttons accordingly.
|
181
|
+
createPullRequestButton.classList.add('hide');
|
182
|
+
updatePullRequestButton.classList.remove('hide');
|
183
|
+
|
184
|
+
// Fill the form with the existing values.
|
185
|
+
pullRequestNumberField.value = data.open_pr.number;
|
186
|
+
pullRequestTitleField.value = data.open_pr.title;
|
187
|
+
pullRequestDescriptionField.value = data.open_pr.description;
|
188
|
+
|
189
|
+
checkPullRequestVisibility(data.open_pr.link);
|
190
|
+
}
|
191
|
+
else {
|
192
|
+
// Without a Pull Request open we don't need to show the chat suggestion text area
|
193
|
+
// because we will use the form already filled with the suggestion.
|
194
|
+
chatDescriptionContainer.classList.add('hide');
|
195
|
+
|
196
|
+
// Update the Pull Request form buttons accordingly.
|
197
|
+
createPullRequestButton.classList.remove('hide');
|
198
|
+
updatePullRequestButton.classList.add('hide');
|
199
|
+
|
200
|
+
// Clear the form and fill the description with the chat suggestion.
|
201
|
+
pullRequestNumberField.value = '';
|
202
|
+
pullRequestTitleField.value = '';
|
203
|
+
pullRequestDescriptionField.value = data.description;
|
204
|
+
}
|
205
|
+
}
|
206
|
+
else {
|
207
|
+
// Without Remote configured we show the chat description with the copy button.
|
208
|
+
pullRequestContainer.classList.add('hide');
|
209
|
+
chatDescriptionContainer.classList.remove('hide');
|
210
|
+
copyChatToDescriptionButton.classList.add('hide');
|
211
|
+
}
|
212
|
+
}
|
213
|
+
|
214
|
+
function updateForm(data) {
|
215
|
+
if (data.number && data.title) {
|
216
|
+
chatDescriptionField.value = pullRequestDescriptionField.value;
|
217
|
+
|
218
|
+
// With Remote configured we always show the Pull Request form.
|
219
|
+
pullRequestContainer.classList.remove('hide');
|
220
|
+
|
221
|
+
// Update the Pull Request form buttons accordingly.
|
222
|
+
createPullRequestButton.classList.add('hide');
|
223
|
+
updatePullRequestButton.classList.remove('hide');
|
224
|
+
|
225
|
+
// Fill the form with the existing values.
|
226
|
+
pullRequestNumberField.value = data.number;
|
227
|
+
pullRequestTitleField.value = data.title;
|
228
|
+
pullRequestDescriptionField.value = data.description;
|
229
|
+
|
230
|
+
checkPullRequestVisibility(data.link);
|
231
|
+
}
|
232
|
+
}
|
233
|
+
|
234
|
+
function checkPullRequestVisibility(link) {
|
235
|
+
if (link && link.length > 0) {
|
236
|
+
pullRequestWebsiteButton.classList.remove('hide');
|
237
|
+
pullRequestWebsiteButton.onclick = function() {
|
238
|
+
window.open(link, "_blank");
|
239
|
+
};
|
240
|
+
} else {
|
241
|
+
pullRequestWebsiteButton.classList.add('hide');
|
242
|
+
}
|
243
|
+
}
|
244
|
+
|
245
|
+
function disableSubmission() {
|
246
|
+
chatDescriptionContainer.classList.add('hide');
|
247
|
+
pullRequestContainer.classList.add('hide');
|
248
|
+
}
|
@@ -0,0 +1,76 @@
|
|
1
|
+
(function Notifications(window) {
|
2
|
+
|
3
|
+
const partial = (fn, ...presetArgs) => (...laterArgs) => fn(...presetArgs, ...laterArgs);
|
4
|
+
|
5
|
+
const append = (el, ...children) => children.forEach(child => el.appendChild(child));
|
6
|
+
|
7
|
+
const isString = input => typeof input === 'string';
|
8
|
+
|
9
|
+
const createElement = (elementType, ...classNames) => {
|
10
|
+
const element = document.createElement(elementType);
|
11
|
+
if(classNames.length) {
|
12
|
+
classNames.forEach(currentClass => element.classList.add(currentClass));
|
13
|
+
}
|
14
|
+
return element;
|
15
|
+
};
|
16
|
+
|
17
|
+
const setInnerText = (element, text) => {
|
18
|
+
element.innerText = text;
|
19
|
+
return element;
|
20
|
+
};
|
21
|
+
|
22
|
+
const createTextElement = (elementType, ...classNames) => partial(setInnerText, createElement(elementType, ...classNames));
|
23
|
+
|
24
|
+
const createParagraph = (...classNames) => createTextElement('p', ...classNames);
|
25
|
+
|
26
|
+
const defaultOptions = {
|
27
|
+
showDuration: 3000,
|
28
|
+
theme: 'success'
|
29
|
+
};
|
30
|
+
|
31
|
+
function showNotification(options) {
|
32
|
+
if(typeof options.showDuration !== 'number') {
|
33
|
+
options.showDuration = defaultOptions.showDuration;
|
34
|
+
}
|
35
|
+
if(!isString(options.theme) || options.theme.length === 0) {
|
36
|
+
options.theme = defaultOptions.theme;
|
37
|
+
}
|
38
|
+
if(!isString(options.message) || options.message.length === 0) {
|
39
|
+
options.message = defaultOptions.theme;
|
40
|
+
}
|
41
|
+
|
42
|
+
const container = createNotificationContainer();
|
43
|
+
const notificationEl = createElement('div', 'notification', options.theme);
|
44
|
+
notificationEl.addEventListener('click', () => container.removeChild(notificationEl));
|
45
|
+
|
46
|
+
if(isString(options.title) && options.title.length > 0) {
|
47
|
+
append(notificationEl, createParagraph('notification-title')(options.title));
|
48
|
+
}
|
49
|
+
append(notificationEl, createParagraph('notification-message')(options.message));
|
50
|
+
append(container, notificationEl);
|
51
|
+
|
52
|
+
if(options.showDuration && options.showDuration > 0) {
|
53
|
+
const timeout = setTimeout(() => {
|
54
|
+
container.removeChild(notificationEl);
|
55
|
+
if(container.querySelectorAll('.notification').length === 0) {
|
56
|
+
document.body.removeChild(container);
|
57
|
+
}
|
58
|
+
}, options.showDuration);
|
59
|
+
|
60
|
+
notificationEl.addEventListener('click', () => clearTimeout(timeout));
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
function createNotificationContainer() {
|
65
|
+
let container = document.querySelector(`notification-container`);
|
66
|
+
if(!container) {
|
67
|
+
container = createElement('div', 'notification-container');
|
68
|
+
append(document.body, container);
|
69
|
+
}
|
70
|
+
return container;
|
71
|
+
}
|
72
|
+
|
73
|
+
if (!window.showNotification) {
|
74
|
+
window.showNotification = showNotification;
|
75
|
+
}
|
76
|
+
})(window);
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,26 @@
|
|
1
|
+
.hide { display: none; }
|
2
|
+
.block { display: block; }
|
3
|
+
.clear:after { content: ''; clear: both; display: block; }
|
4
|
+
|
5
|
+
.mt-5 { margin-top: 5px; }
|
6
|
+
|
7
|
+
.mt-10 { margin-top: 10px; }
|
8
|
+
.mt-20 { margin-top: 20px; }
|
9
|
+
.mt-40 { margin-top: 40px; }
|
10
|
+
.mr-10 { margin-right: 10px; }
|
11
|
+
.mr-20 { margin-right: 20px; }
|
12
|
+
.ml-10 { margin-left: 10px; }
|
13
|
+
|
14
|
+
.fw { width: 100% }
|
15
|
+
|
16
|
+
.w-760 { width: 760px; }
|
17
|
+
|
18
|
+
.mh-40 { min-height: 40px; }
|
19
|
+
.h-100 { height: 100px; }
|
20
|
+
.h-220 { height: 220px; }
|
21
|
+
|
22
|
+
.flex {
|
23
|
+
display: flex;
|
24
|
+
}
|
25
|
+
|
26
|
+
.red { color: #BD362F; }
|
@@ -0,0 +1,64 @@
|
|
1
|
+
|
2
|
+
input, select {
|
3
|
+
background: white;
|
4
|
+
border: 1px solid gray;
|
5
|
+
border-radius: 4px;
|
6
|
+
box-shadow: none;
|
7
|
+
color: black;
|
8
|
+
padding: 10px 12px;
|
9
|
+
}
|
10
|
+
|
11
|
+
button {
|
12
|
+
appearance: none;
|
13
|
+
background-color: #3d898b;
|
14
|
+
border: 1px solid rgba(27, 31, 35, .15);
|
15
|
+
border-radius: 6px;
|
16
|
+
box-shadow: rgba(27, 31, 35, .1) 0 1px 0;
|
17
|
+
box-sizing: border-box;
|
18
|
+
color: #fff;
|
19
|
+
cursor: pointer;
|
20
|
+
display: inline-block;
|
21
|
+
font-size: 14px;
|
22
|
+
font-weight: 500;
|
23
|
+
line-height: 20px;
|
24
|
+
padding: 6px 16px;
|
25
|
+
position: relative;
|
26
|
+
text-align: center;
|
27
|
+
text-decoration: none;
|
28
|
+
user-select: none;
|
29
|
+
-webkit-user-select: none;
|
30
|
+
touch-action: manipulation;
|
31
|
+
vertical-align: middle;
|
32
|
+
white-space: nowrap;
|
33
|
+
}
|
34
|
+
|
35
|
+
button:focus:not(:focus-visible):not(.focus-visible) {
|
36
|
+
box-shadow: none;
|
37
|
+
outline: none;
|
38
|
+
}
|
39
|
+
|
40
|
+
button:hover {
|
41
|
+
background-color: #2D7072;
|
42
|
+
}
|
43
|
+
|
44
|
+
button:focus {
|
45
|
+
box-shadow: rgba(46, 164, 79, .4) 0 0 0 3px;
|
46
|
+
outline: none;
|
47
|
+
}
|
48
|
+
|
49
|
+
button:disabled {
|
50
|
+
background-color: #a0a0a0;
|
51
|
+
border-color: rgba(27, 31, 35, .1);
|
52
|
+
color: rgba(255, 255, 255, 0.594);
|
53
|
+
cursor: default;
|
54
|
+
}
|
55
|
+
|
56
|
+
button:active {
|
57
|
+
background-color: #2D7072;
|
58
|
+
box-shadow: rgba(20, 70, 32, .2) 0 1px 0 inset;
|
59
|
+
}
|
60
|
+
|
61
|
+
textarea {
|
62
|
+
width: 760px;
|
63
|
+
resize: none;
|
64
|
+
}
|
@@ -0,0 +1,71 @@
|
|
1
|
+
.notification-container {
|
2
|
+
font-size: 14px;
|
3
|
+
box-sizing: border-box;
|
4
|
+
position: fixed;
|
5
|
+
z-index: 999999;
|
6
|
+
top: 12px;
|
7
|
+
right: 12px;
|
8
|
+
}
|
9
|
+
|
10
|
+
.notification-container .notification {
|
11
|
+
background: #ffffff;
|
12
|
+
transition: .3s ease;
|
13
|
+
position: relative;
|
14
|
+
pointer-events: auto;
|
15
|
+
overflow: hidden;
|
16
|
+
padding: 15px 15px 15px 50px;
|
17
|
+
margin: 0 0 6px;
|
18
|
+
width: 300px;
|
19
|
+
border-radius: 3px 3px 3px 3px;
|
20
|
+
box-shadow: 0 0 12px #999999;
|
21
|
+
color: #000000;
|
22
|
+
opacity: 0.9;
|
23
|
+
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=90);
|
24
|
+
filter: alpha(opacity=90);
|
25
|
+
background-position: 15px center !important;
|
26
|
+
background-repeat: no-repeat !important;
|
27
|
+
-webkit-user-select: none; /* Chrome all / Safari all */
|
28
|
+
-moz-user-select: none; /* Firefox all */
|
29
|
+
-ms-user-select: none; /* IE 10+ */
|
30
|
+
user-select: none; /* Likely future */
|
31
|
+
}
|
32
|
+
|
33
|
+
.notification:hover {
|
34
|
+
box-shadow: 0 0 12px #000000;
|
35
|
+
opacity: 1;
|
36
|
+
cursor: pointer;
|
37
|
+
}
|
38
|
+
|
39
|
+
.notification .notification-title {
|
40
|
+
font-weight: bold;
|
41
|
+
font-size: 16px;
|
42
|
+
text-align: left;
|
43
|
+
margin-top: 0;
|
44
|
+
margin-bottom: 6px;
|
45
|
+
word-wrap: break-word;
|
46
|
+
}
|
47
|
+
|
48
|
+
.notification .notification-message {
|
49
|
+
margin: 0;
|
50
|
+
text-align: left;
|
51
|
+
word-wrap: break-word;
|
52
|
+
}
|
53
|
+
|
54
|
+
.notification p {
|
55
|
+
color: #ffffff;
|
56
|
+
}
|
57
|
+
|
58
|
+
.notification-container .success {
|
59
|
+
background: #51A351;
|
60
|
+
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==");
|
61
|
+
}
|
62
|
+
|
63
|
+
.notification-container .warning {
|
64
|
+
background: #f87400;
|
65
|
+
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=");
|
66
|
+
}
|
67
|
+
|
68
|
+
.notification-container .error {
|
69
|
+
background: #BD362F;
|
70
|
+
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=");
|
71
|
+
}
|